internlm/SWE-Fixer-Train-110K huggingface dataset:
Repository: Mailu/Mailu

Issue:
Alias behaviour changed in master
Hi,

I tried the last master version (I don't know the version I was using before, it was probably 8 weeks old).
With this version, the alias behavior has changed and it seems that catchall mailbox cannot be used anymore.

Previously, the behavior was the one described by Kaiyou in issue #155 : 

> [..]it is not the first alias that matches but the longest (and hopefully most specific). This behaviour was inspired by pattern matching in other applications and seems to be the norm.owever, it is not the first alias that matches but the longest (and hopefully most specific). This behaviour was inspired by pattern matching in other applications and seems to be the norm.

Now, it seems that the shortest alias is matched.

Here is my config:
name1.%@domain.tld  -> name1@domain.tld
name2.%@domain.tld  -> name2@domain.tld
%@domain.tld -> catchall@domain.tld

Before the mail sent to name1.whatever@domain.tld was delivered to mailbox name1@domain.tld, and all the mail that did not match (for example bar@domain.tld) where delivered to catchall@domain.tld.

Now every mail are delivered to the catchall mailbox.



Discussion:

Resolving patch:
diff --git a/core/admin/mailu/models.py b/core/admin/mailu/models.py
--- a/core/admin/mailu/models.py
+++ b/core/admin/mailu/models.py
@@ -426,7 +426,7 @@ def resolve(cls, localpart, domain_name):
                     )
                 )
             )
-        ).first()
+        ).order_by(cls.wildcard, sqlalchemy.func.char_length(cls.localpart).desc()).first()
 
 
 class Token(Base):


Test patch:
Repository: Mailu/Mailu

Issue:
Why a print statement in this code?
@kaiyou, according git blame this is yours. I wonder if it was included for debugging purposes and not taken out anymore?

https://github.com/Mailu/Mailu/blob/5c2439011413a114c470862f95206f8487fa4568/core/admin/mailu/internal/views/dovecot.py#L18


Discussion:

Resolving patch:
diff --git a/core/admin/mailu/internal/views/dovecot.py b/core/admin/mailu/internal/views/dovecot.py
--- a/core/admin/mailu/internal/views/dovecot.py
+++ b/core/admin/mailu/internal/views/dovecot.py
@@ -13,7 +13,6 @@ def dovecot_passdb_dict(user_email):
     allow_nets.append(app.config["SUBNET"])
     if app.config["POD_ADDRESS_RANGE"]:
         allow_nets.append(app.config["POD_ADDRESS_RANGE"])
-    print(allow_nets)
     return flask.jsonify({
         "password": None,
         "nopassword": "Y",


Test patch:
Repository: Mailu/Mailu

Issue:
Password field(s) of type "text" instead of "password"
In 1.5 [this line](https://github.com/Mailu/Mailu/blob/master/core/admin/mailu/ui/templates/fetch/create.html#L22) creates a field of type _text_ not _password_.


Discussion:

Resolving patch:
diff --git a/core/admin/mailu/ui/forms.py b/core/admin/mailu/ui/forms.py
--- a/core/admin/mailu/ui/forms.py
+++ b/core/admin/mailu/ui/forms.py
@@ -165,11 +165,11 @@ class FetchForm(flask_wtf.FlaskForm):
     protocol = fields.SelectField(_('Protocol'), choices=[
         ('imap', 'IMAP'), ('pop3', 'POP3')
     ])
-    host = fields.StringField(_('Hostname or IP'))
-    port = fields.IntegerField(_('TCP port'))
+    host = fields.StringField(_('Hostname or IP'), [validators.DataRequired()])
+    port = fields.IntegerField(_('TCP port'), [validators.DataRequired(), validators.NumberRange(min=0, max=65535)])
     tls = fields.BooleanField(_('Enable TLS'))
-    username = fields.StringField(_('Username'))
-    password = fields.StringField(_('Password'))
+    username = fields.StringField(_('Username'), [validators.DataRequired()])
+    password = fields.PasswordField(_('Password'), [validators.DataRequired()])
     keep = fields.BooleanField(_('Keep emails on the server'))
     submit = fields.SubmitField(_('Submit'))
 


Test patch:
Repository: Mailu/Mailu

Issue:
Wildcard aliases override concrete aliases + delimiter
After #778 , the following scenario behaved in a non-intuitive way:
Aliases:
* `foo@xxx.tld` ⇒ user**A**
* `%@xxx.tld` ⇒ user**B**
Sending to both `foo@xxx.tld` and `random@xxx.tld` work, however `foo+spam@xxx.tld` gets delivered to user**B**. This is not intuitive to the user.


Discussion:

Resolving patch:
diff --git a/core/admin/mailu/models.py b/core/admin/mailu/models.py
--- a/core/admin/mailu/models.py
+++ b/core/admin/mailu/models.py
@@ -261,14 +261,20 @@ def resolve_domain(cls, email):
     @classmethod
     def resolve_destination(cls, localpart, domain_name, ignore_forward_keep=False):
         localpart_stripped = None
+        stripped_alias = None
+
         if os.environ.get('RECIPIENT_DELIMITER') in localpart:
             localpart_stripped = localpart.rsplit(os.environ.get('RECIPIENT_DELIMITER'), 1)[0]
 
-        alias = Alias.resolve(localpart, domain_name)
-        if not alias and localpart_stripped:
-            alias = Alias.resolve(localpart_stripped, domain_name)
-        if alias:
-            return alias.destination
+        pure_alias = Alias.resolve(localpart, domain_name)
+        stripped_alias = Alias.resolve(localpart_stripped, domain_name)
+
+        if pure_alias and not pure_alias.wildcard:
+            return pure_alias.destination
+        elif stripped_alias:
+            return stripped_alias.destination
+        elif pure_alias:
+            return pure_alias.destination
 
         user = User.query.get('{}@{}'.format(localpart, domain_name))
         if not user and localpart_stripped:


Test patch:
Repository: Mailu/Mailu

Issue:
In user-admin for a domain, allow changing user fields without changing the password
Hi,

in
https://github.com/Mailu/Mailu/blob/a382f74680a749762cc14272c2b023cdd52ba1fd/core/admin/mailu/ui/forms.py#L79
it seems the password+confirm password field is always required. However, when modifying a user (e.g. changing their quota, disabling pop3, etc) it’s useful to be able to carry out these modifications without resetting the users password ….
For this, it’d be great if a left-blank field would be just ignored, and the password in the model not touched.


Discussion:
Strange thing is that this should drop the constraint. 

https://github.com/Mailu/Mailu/blob/be7dc1e6bd122c783805d1c647d9ceb03887b595/core/admin/mailu/ui/views/users.py#L58

I will look again into that in the morning.
Hi,

isn’t that the validator that’s run for handling a POST request server-sided? What I observed is that a client-side validator in the browser prohibits empty input before an actual request is issued.

Thanks for looking into this,
-Dario
Just hit this on a fresh 'master' install in preparation for the 1.6 release. Should probably be in the milestone for that?

Resolving patch:
diff --git a/core/admin/mailu/ui/forms.py b/core/admin/mailu/ui/forms.py
--- a/core/admin/mailu/ui/forms.py
+++ b/core/admin/mailu/ui/forms.py
@@ -84,7 +84,7 @@ class RelayForm(flask_wtf.FlaskForm):
 
 class UserForm(flask_wtf.FlaskForm):
     localpart = fields.StringField(_('E-mail'), [validators.DataRequired(), validators.Regexp(LOCALPART_REGEX)])
-    pw = fields.PasswordField(_('Password'), [validators.DataRequired()])
+    pw = fields.PasswordField(_('Password'))
     pw2 = fields.PasswordField(_('Confirm password'), [validators.EqualTo('pw')])
     quota_bytes = fields_.IntegerSliderField(_('Quota'), default=1000000000)
     enable_imap = fields.BooleanField(_('Allow IMAP access'), default=True)
diff --git a/core/admin/mailu/ui/views/users.py b/core/admin/mailu/ui/views/users.py
--- a/core/admin/mailu/ui/views/users.py
+++ b/core/admin/mailu/ui/views/users.py
@@ -23,6 +23,7 @@ def user_create(domain_name):
         return flask.redirect(
             flask.url_for('.user_list', domain_name=domain.name))
     form = forms.UserForm()
+    form.pw.validators = [wtforms.validators.DataRequired()]
     if domain.max_quota_bytes:
         form.quota_bytes.validators = [
             wtforms.validators.NumberRange(max=domain.max_quota_bytes)]
@@ -54,7 +55,6 @@ def user_edit(user_email):
     # Create the form
     form = forms.UserForm(obj=user)
     wtforms_components.read_only(form.localpart)
-    form.pw.validators = []
     form.localpart.validators = []
     if max_quota_bytes:
         form.quota_bytes.validators = [


Test patch:
Repository: Mailu/Mailu

Issue:
With DOMAIN_REGISTRATION set to `false`, the registration ui is enabled
Looks like it doesn’t matter what it’s set to — as long as its string pythonically evaluates to `True`, it’ll be shown. That’s AFAIK every string not `""`, right?

That said, it’s probably true for many other Boolean config flags, as no data-type aware casting is done in `core/admin/mailu/configuration.py`. Maybe a more general solution would be nice?


Discussion:

Resolving patch:
diff --git a/core/admin/mailu/configuration.py b/core/admin/mailu/configuration.py
--- a/core/admin/mailu/configuration.py
+++ b/core/admin/mailu/configuration.py
@@ -30,11 +30,11 @@
     'POSTMASTER': 'postmaster',
     'TLS_FLAVOR': 'cert',
     'AUTH_RATELIMIT': '10/minute;1000/hour',
-    'DISABLE_STATISTICS': 'False',
+    'DISABLE_STATISTICS': False,
     # Mail settings
     'DMARC_RUA': None,
     'DMARC_RUF': None,
-    'WELCOME': 'False',
+    'WELCOME': False,
     'WELCOME_SUBJECT': 'Dummy welcome topic',
     'WELCOME_BODY': 'Dummy welcome body',
     'DKIM_SELECTOR': 'dkim',
@@ -74,13 +74,21 @@ class ConfigManager(dict):
     def __init__(self):
         self.config = dict()
 
+    def __coerce_value(self, value):
+        if isinstance(value, str) and value.lower() in ('true','yes'):
+            return True
+        elif isinstance(value, str) and value.lower() in ('false', 'no'):
+            return False
+        return value
+
     def init_app(self, app):
         self.config.update(app.config)
         # get environment variables
         self.config.update({
-            key: os.environ.get(key, value)
+            key: self.__coerce_value(os.environ.get(key, value))
             for key, value in DEFAULT_CONFIG.items()
         })
+
         # automatically set the sqlalchemy string
         if self.config['DB_FLAVOR']:
             template = self.DB_TEMPLATES[self.config['DB_FLAVOR']]
diff --git a/core/admin/mailu/manage.py b/core/admin/mailu/manage.py
--- a/core/admin/mailu/manage.py
+++ b/core/admin/mailu/manage.py
@@ -31,7 +31,7 @@ def advertise():
         instance_id = str(uuid.uuid4())
         with open(app.config["INSTANCE_ID_PATH"], "w") as handle:
             handle.write(instance_id)
-    if app.config["DISABLE_STATISTICS"].lower() != "true":
+    if not app.config["DISABLE_STATISTICS"]:
         try:
             socket.gethostbyname(app.config["STATS_ENDPOINT"].format(instance_id))
         except:
diff --git a/core/admin/mailu/models.py b/core/admin/mailu/models.py
--- a/core/admin/mailu/models.py
+++ b/core/admin/mailu/models.py
@@ -410,7 +410,7 @@ def get_managed_emails(self, include_aliases=True):
         return emails
 
     def send_welcome(self):
-        if app.config["WELCOME"].lower() == "true":
+        if app.config["WELCOME"]:
             self.sendmail(app.config["WELCOME_SUBJECT"],
                 app.config["WELCOME_BODY"])
 


Test patch:
Repository: Mailu/Mailu

Issue:
Cannot create an Authorisation Token for an IPv6 address
On `/admin/ui/token/create` when one enters an IPv6 address it responds with "invalid ip address" because wtforms.validators.IPAddress understands IPv4 only.

I'm also not sure if the other parts that handle auth tokens support IPv6.


Discussion:

Resolving patch:
diff --git a/core/admin/mailu/ui/forms.py b/core/admin/mailu/ui/forms.py
--- a/core/admin/mailu/ui/forms.py
+++ b/core/admin/mailu/ui/forms.py
@@ -137,7 +137,7 @@ class TokenForm(flask_wtf.FlaskForm):
     raw_password = fields.HiddenField([validators.DataRequired()])
     comment = fields.StringField(_('Comment'))
     ip = fields.StringField(
-        _('Authorized IP'), [validators.Optional(), validators.IPAddress()]
+        _('Authorized IP'), [validators.Optional(), validators.IPAddress(ipv6=True)]
     )
     submit = fields.SubmitField(_('Save'))
 


Test patch:
Repository: Mailu/Mailu

Issue:
Auto-forward email validation bug
Getting `Invalid email address` when trying to set auto-forward destination to foo@example.amsterdam.

Following the code i got to this regex which i think does the trick.
https://github.com/Mailu/Mailu/blob/fa1e4b80231e4d52485b0d257227a02f31a48ac8/core/admin/mailu/ui/forms.py#L40


Discussion:

Resolving patch:
diff --git a/core/admin/mailu/ui/forms.py b/core/admin/mailu/ui/forms.py
--- a/core/admin/mailu/ui/forms.py
+++ b/core/admin/mailu/ui/forms.py
@@ -37,7 +37,7 @@ def __init__(self,message=_('Invalid email address.')):
         self.message = message
 
     def __call__(self, form, field):
-        pattern = re.compile(r'^([_a-z0-9\-]+)(\.[_a-z0-9\-]+)*@([a-z0-9\-]{2,}\.)*([a-z]{2,4})(,([_a-z0-9\-]+)(\.[_a-z0-9\-]+)*@([a-z0-9\-]{2,}\.)*([a-z]{2,4}))*$')
+        pattern = re.compile(r'^([_a-z0-9\-]+)(\.[_a-z0-9\-]+)*@([a-z0-9\-]{2,}\.)*([a-z]{2,})(,([_a-z0-9\-]+)(\.[_a-z0-9\-]+)*@([a-z0-9\-]{2,}\.)*([a-z]{2,}))*$')
         if not pattern.match(field.data.replace(" ", "")):
             raise validators.ValidationError(self.message)
 


Test patch:
Repository: Mailu/Mailu

Issue:
Document the new setup utility
Title says all


Discussion:

Resolving patch:
diff --git a/setup/server.py b/setup/server.py
--- a/setup/server.py
+++ b/setup/server.py
@@ -11,7 +11,7 @@
 import hashlib
 
 
-version = os.getenv("this_version")
+version = os.getenv("this_version", "master")
 static_url_path = "/" + version + "/static"
 app = flask.Flask(__name__, static_url_path=static_url_path)
 flask_bootstrap.Bootstrap(app)


Test patch:
Repository: Mailu/Mailu

Issue:
Missing wildcard option in alias flask command
Hi guys,

First of all, thank you for making this awesome Mailu project :+1: :100: 

I'm using command line to mass import some alias from an old server. There doesn't seem to have an option to toggle wildcard support:

```
Usage: flask mailu alias [OPTIONS] LOCALPART DOMAIN_NAME DESTINATION

  Create an alias

Options:
  --help  Show this message and exit.
```

Would be great if it can be done through CLI. Thanks again!


Discussion:

Resolving patch:
diff --git a/core/admin/mailu/manage.py b/core/admin/mailu/manage.py
--- a/core/admin/mailu/manage.py
+++ b/core/admin/mailu/manage.py
@@ -292,8 +292,9 @@ def alias_delete(email):
 @click.argument('localpart')
 @click.argument('domain_name')
 @click.argument('destination')
+@click.option('-w', '--wildcard', is_flag=True)
 @flask_cli.with_appcontext
-def alias(localpart, domain_name, destination):
+def alias(localpart, domain_name, destination, wildcard=False):
     """ Create an alias
     """
     domain = models.Domain.query.get(domain_name)
@@ -303,6 +304,7 @@ def alias(localpart, domain_name, destination):
     alias = models.Alias(
         localpart=localpart,
         domain=domain,
+        wildcard=wildcard,
         destination=destination.split(','),
         email="%s@%s" % (localpart, domain_name)
     )


Test patch:
Repository: Mailu/Mailu

Issue:
command documentation uses `_` where `-` should be used
https://mailu.io/1.6/cli.html shows commands

```
alias
alias_delete
user
user_import
user_delete
config_update
```

where `flask mailu` shows the following subcommands:

```
Commands:
  admin          Create an admin user
  advertise      Advertise this server against statistic services.
  alias          Create an alias
  alias-delete   delete alias
  config-update  sync configuration with data from YAML-formatted stdin
  domain
  setlimits      Set domain limits
  setmanager     Make a user manager of a domain
  user           Create a user
  user-delete    delete user
  user-import    Import a user along with password hash.
```

I think the documentation must be updated to use `-` rather than `_`.


Discussion:

Resolving patch:
diff --git a/core/admin/mailu/manage.py b/core/admin/mailu/manage.py
--- a/core/admin/mailu/manage.py
+++ b/core/admin/mailu/manage.py
@@ -64,7 +64,7 @@ def admin(localpart, domain_name, password):
 @click.argument('localpart')
 @click.argument('domain_name')
 @click.argument('password')
-@click.argument('hash_scheme')
+@click.argument('hash_scheme', required=False)
 @flask_cli.with_appcontext
 def user(localpart, domain_name, password, hash_scheme=None):
     """ Create a user
@@ -86,12 +86,14 @@ def user(localpart, domain_name, password, hash_scheme=None):
 
 
 @mailu.command()
-@click.option('-n', '--domain_name')
-@click.option('-u', '--max_users')
-@click.option('-a', '--max_aliases')
-@click.option('-q', '--max_quota_bytes')
+@click.argument('domain_name')
+@click.option('-u', '--max-users')
+@click.option('-a', '--max-aliases')
+@click.option('-q', '--max-quota-bytes')
 @flask_cli.with_appcontext
 def domain(domain_name, max_users=-1, max_aliases=-1, max_quota_bytes=0):
+    """ Create a domain
+    """
     domain = models.Domain.query.get(domain_name)
     if not domain:
         domain = models.Domain(name=domain_name)
@@ -126,7 +128,7 @@ def user_import(localpart, domain_name, password_hash, hash_scheme = None):
 
 @mailu.command()
 @click.option('-v', '--verbose')
-@click.option('-d', '--delete_objects')
+@click.option('-d', '--delete-objects')
 @flask_cli.with_appcontext
 def config_update(verbose=False, delete_objects=False):
     """sync configuration with data from YAML-formatted stdin"""


Test patch:
Repository: Mailu/Mailu

Issue:
Fetchmail sslproto configuration (typo?)
The fetchmail container repeatedly fires the following warning

`fetchmail: Invalid SSL protocol 'AUTO' specified, using default (SSLv23).`

From the fetchmail manpage, this appears to relate to the `--sslproto` switch. 

IMHO the default should be an all lowercase `auto`. Otherwise, an improvement suggestion would be to make this configurable through the admin interface.


Discussion:

Resolving patch:
diff --git a/services/fetchmail/fetchmail.py b/services/fetchmail/fetchmail.py
--- a/services/fetchmail/fetchmail.py
+++ b/services/fetchmail/fetchmail.py
@@ -22,7 +22,6 @@
     is "{user_email}"
     smtphost "{smtphost}"
     {options}
-    sslproto 'AUTO'
 """
 
 
@@ -54,7 +53,7 @@ def run(debug):
     for fetch in fetches:
         fetchmailrc = ""
         options = "options antispam 501, 504, 550, 553, 554"
-        options += " ssl" if fetch["tls"] else ""
+        options += " sslmode wrapped" if fetch["tls"] else ""
         options += " keep" if fetch["keep"] else " fetchall"
         fetchmailrc += RC_LINE.format(
             user_email=escape_rc_string(fetch["user_email"]),


Test patch:
Repository: Mailu/Mailu

Issue:
Typo in migration file
Looking at logs I found a typo in one of the migration files. I don't really know if it is possible to change migration files manually so I'm not writing a pull request but a bug report ;)

https://github.com/Mailu/Mailu/blob/c484c5326b17877bc66a454970cf1342f956d47e/core/admin/migrations/versions/5aeb5811408e_.py#L109

This should probably read relay_table.
Typo in migration file
Looking at logs I found a typo in one of the migration files. I don't really know if it is possible to change migration files manually so I'm not writing a pull request but a bug report ;)

https://github.com/Mailu/Mailu/blob/c484c5326b17877bc66a454970cf1342f956d47e/core/admin/migrations/versions/5aeb5811408e_.py#L109

This should probably read relay_table.


Discussion:

Resolving patch:
diff --git a/core/admin/migrations/versions/5aeb5811408e_.py b/core/admin/migrations/versions/5aeb5811408e_.py
--- a/core/admin/migrations/versions/5aeb5811408e_.py
+++ b/core/admin/migrations/versions/5aeb5811408e_.py
@@ -106,7 +106,7 @@ def upgrade():
     # lower relays
     for relay in connection.execute(relay_table.select()):
         connection.execute(relay_table.update().where(
-            relay_tbale.c.name == relay.name
+            relay_table.c.name == relay.name
         ).values(
             name=relay.name.lower()
         ))


Test patch:
Repository: Mailu/Mailu

Issue:
Admin whitelist_webmail hard coded
Hi,
I'm trying to send transactional email with Mailu SMTP from a backend server and it takes more than 15s.
I tracked down one problem in core/admin/mailu/internal/__init__.py:
def whitelist_webmail() uses socket.gethostbyname("webmail")

In my docker configuration there is no "webmail" host so the function socket.gethostbyname return nothing after 5s which slows down a lot the request /internal/auth/email
When I set "webmail" to a fake ip on the admin server the /internal/auth/email returns immediately.

Maybe it would be better to define a list of hostnames in the configuration file instead of using a hard coded "webmail" value. What do you think?

Thanks Mailu for the great work!
JB


Discussion:
https://github.com/Mailu/Mailu/blob/d3a5c7058927698cf66b9105bfb4daa95da33b85/core/admin/mailu/internal/__init__.py#L25-L26
@ionutfilip, so there is still a cheeky `gethostbyname` hidden somewhere. Can you please rewrite this snippet to use the resolved addresses, like you implemented with `mailustart` in PR #886.

Resolving patch:
diff --git a/core/admin/mailu/configuration.py b/core/admin/mailu/configuration.py
--- a/core/admin/mailu/configuration.py
+++ b/core/admin/mailu/configuration.py
@@ -46,6 +46,7 @@
     'WEBSITE': 'https://mailu.io',
     'WEB_ADMIN': '/admin',
     'WEB_WEBMAIL': '/webmail',
+    'WEBMAIL': 'none',
     'RECAPTCHA_PUBLIC_KEY': '',
     'RECAPTCHA_PRIVATE_KEY': '',
     # Advanced settings
@@ -79,6 +80,8 @@ def resolve_host(self):
         self.config['HOST_POP3'] = resolve(self.config['HOST_POP3'])
         self.config['HOST_AUTHSMTP'] = resolve(self.config['HOST_AUTHSMTP'])
         self.config['HOST_SMTP'] = resolve(self.config['HOST_SMTP'])
+        if self.config['WEBMAIL'] != 'none':
+            self.config['HOST_WEBMAIL'] = resolve(self.config['HOST_WEBMAIL'])
 
     def __coerce_value(self, value):
         if isinstance(value, str) and value.lower() in ('true','yes'):
diff --git a/core/admin/mailu/internal/__init__.py b/core/admin/mailu/internal/__init__.py
--- a/core/admin/mailu/internal/__init__.py
+++ b/core/admin/mailu/internal/__init__.py
@@ -1,6 +1,7 @@
 from flask_limiter import RateLimitExceeded
 
 from mailu import utils
+from flask import current_app as app
 
 import socket
 import flask
@@ -23,7 +24,7 @@ def rate_limit_handler(e):
 def whitelist_webmail():
     try:
         return flask.request.headers["Client-Ip"] ==\
-            socket.gethostbyname("webmail")
+            app.config["HOST_WEBMAIL"]
     except:
         return False
 


Test patch:
Repository: Mailu/Mailu

Issue:
Typo in migration file
Looking at logs I found a typo in one of the migration files. I don't really know if it is possible to change migration files manually so I'm not writing a pull request but a bug report ;)

https://github.com/Mailu/Mailu/blob/c484c5326b17877bc66a454970cf1342f956d47e/core/admin/migrations/versions/5aeb5811408e_.py#L109

This should probably read relay_table.


Discussion:

Resolving patch:
diff --git a/core/admin/migrations/versions/5aeb5811408e_.py b/core/admin/migrations/versions/5aeb5811408e_.py
--- a/core/admin/migrations/versions/5aeb5811408e_.py
+++ b/core/admin/migrations/versions/5aeb5811408e_.py
@@ -106,7 +106,7 @@ def upgrade():
     # lower relays
     for relay in connection.execute(relay_table.select()):
         connection.execute(relay_table.update().where(
-            relay_tbale.c.name == relay.name
+            relay_table.c.name == relay.name
         ).values(
             name=relay.name.lower()
         ))


Test patch: