Repository: Mailu/Mailu

Issue:
Request: case unsensitive mailboxes
Hello,
It seems like the mail server is handling mails case sensitive, so "My.Cool@mail.box" will not be received by the mail box "my.cool@mail.box" which is not very intuitive. Is it possible to have case unsensitive mail boxes enabled (by config)?


Discussion:
Hmm, are you sure about this? Here I have everything lowercase... but I can definitely receive emails with upper cases both on the user and domain parts.
hmm... I begin to believe that my installation is cursed... Maybe I screwed up with some override configs... I will check that.
I disabled my overrides, it still happens. My catch-all address is getting the mails.
From: sender@domain.tld
To: Parts.Uppercase@domain.tld
Mail-Address for catch all: catchall@domain.tld

Logs:

```
smtp_1       | Jul 29 08:09:37 mail postfix[104]: Postfix is running with backwards-compatible default settings
smtp_1       | Jul 29 08:09:37 mail postfix[104]: See http://www.postfix.org/COMPATIBILITY_README.html for details
smtp_1       | Jul 29 08:09:37 mail postfix[104]: To disable backwards compatibility use "postconf compatibility_level=2" and "postfix reload"
smtp_1       | Jul 29 08:09:39 mail postfix[319]: Postfix is running with backwards-compatible default settings
smtp_1       | Jul 29 08:09:39 mail postfix[319]: See http://www.postfix.org/COMPATIBILITY_README.html for details
smtp_1       | Jul 29 08:09:39 mail postfix[319]: To disable backwards compatibility use "postconf compatibility_level=2" and "postfix reload"
smtp_1       | Jul 29 08:09:40 mail postfix/postfix-script[385]: starting the Postfix mail system
smtp_1       | Jul 29 08:09:40 mail postfix/master[386]: daemon started -- version 3.5.12, configuration /etc/postfix
smtp_1       | Jul 29 08:10:04 mail postfix/smtpd[398]: connect from localhost[127.0.0.1]
smtp_1       | Jul 29 08:10:04 mail postfix/smtpd[398]: disconnect from localhost[127.0.0.1] quit=1 commands=1
smtp_1       | Jul 29 08:10:35 mail postfix/smtpd[398]: connect from localhost[127.0.0.1]
smtp_1       | Jul 29 08:10:35 mail postfix/smtpd[398]: disconnect from localhost[127.0.0.1] quit=1 commands=1
smtp_1       | Jul 29 08:11:05 mail postfix/smtpd[398]: connect from localhost[127.0.0.1]
smtp_1       | Jul 29 08:11:05 mail postfix/smtpd[398]: disconnect from localhost[127.0.0.1] quit=1 commands=1
smtp_1       | Jul 29 08:11:35 mail postfix/smtpd[398]: connect from localhost[127.0.0.1]
smtp_1       | Jul 29 08:11:35 mail postfix/smtpd[398]: disconnect from localhost[127.0.0.1] quit=1 commands=1
smtp_1       | Jul 29 08:12:05 mail postfix/smtpd[398]: connect from localhost[127.0.0.1]
smtp_1       | Jul 29 08:12:05 mail postfix/smtpd[398]: disconnect from localhost[127.0.0.1] quit=1 commands=1
smtp_1       | Jul 29 08:12:21 mail postfix/smtpd[434]: connect from unknown[172.18.0.1]
smtp_1       | Jul 29 08:12:21 mail postfix/smtpd[434]: 5928612CB6C: client=unknown[ip.ip.ip.ip], sasl_method=XCLIENT, sasl_username=sender@domain.tld
smtp_1       | Jul 29 08:12:21 mail postfix/cleanup[438]: 5928612CB6C: replace: header Received: from AM0PR06MB5537.eurprd06.prod.outlook.com (unknown [ip.ip.ip.ip])??by mailserver.domain.tld (Postfix) with ESMTPA id 5928612CB6C??for <Parts.Uppercase@domain.tld>; Thu, 29 Jul 2021 08:12:21 +000 from unknown[ip.ip.ip.ip]; from=<sender@domain.tld> to=<Parts.Uppercase@domain.tld> proto=ESMTP helo=<AM0PR06MB5537.eurprd06.prod.outlook.com>: Received: from authenticated-user (mailserver.domain.tld [serverip.serverip.serverip.serverip])??by mailserver.domain.tld (Postfix) with ESMTPA id 5928612CB6C??for <Parts.Uppercase@domain.tld>; Thu, 29 Jul 2021 08:12:21 +0000 (UTC)
smtp_1       | Jul 29 08:12:21 mail postfix/cleanup[438]: 5928612CB6C: message-id=<AM0PR06MB5537158BD698B9EACA2E63C0FAEB9@AM0PR06MB5537.eurprd06.prod.outlook.com>
smtp_1       | Jul 29 08:12:21 mail postfix/qmgr[388]: 5928612CB6C: from=<sender@domain.tld>, size=1741, nrcpt=1 (queue active)
smtp_1       | Jul 29 08:12:21 mail postfix/smtpd[434]: disconnect from unknown[ip.ip.ip.ip] ehlo=2 xclient=0/1 mail=1 rcpt=1 data=1 quit=1 commands=6/7
smtp_1       | Jul 29 08:12:21 mail postfix/lmtp[439]: 5928612CB6C: to=<catchall@domain.tld>, orig_to=<Parts.Uppercase@domain.tld>, relay=172.18.0.7[172.18.0.7]:2525, delay=0.51, delays=0.38/0.02/0.01/0.1, dsn=2.0.0, status=sent (250 2.0.0 <catchall@domain.tld> hCHJKuViAmFRAAAAIuytJQ Saved)
smtp_1       | Jul 29 08:12:21 mail postfix/qmgr[388]: 5928612CB6C: removed
smtp_1       | Jul 29 08:12:35 mail postfix/smtpd[398]: connect from localhost[127.0.0.1]
smtp_1       | Jul 29 08:12:35 mail postfix/smtpd[398]: disconnect from localhost[127.0.0.1] quit=1 commands=1
smtp_1       | Jul 29 08:12:48 mail postfix/smtpd[434]: connect from unknown[172.18.0.1]
smtp_1       | Jul 29 08:12:48 mail postfix/smtpd[434]: 8761212C3BF: client=unknown[ip.ip.ip.ip], sasl_method=XCLIENT, sasl_username=sender@domain.tld
smtp_1       | Jul 29 08:12:48 mail postfix/cleanup[438]: 8761212C3BF: replace: header Received: from AM0PR06MB5537.eurprd06.prod.outlook.com (unknown [ip.ip.ip.ip])??by mailserver.domain.tld (Postfix) with ESMTPA id 8761212C3BF??for <Parts.Uppercase@domain.tld>; Thu, 29 Jul 2021 08:12:48 +000 from unknown[ip.ip.ip.ip]; from=<sender@domain.tld> to=<Parts.Uppercase@domain.tld> proto=ESMTP helo=<AM0PR06MB5537.eurprd06.prod.outlook.com>: Received: from authenticated-user (mailserver.domain.tld [serverip.serverip.serverip.serverip])??by mailserver.domain.tld (Postfix) with ESMTPA id 8761212C3BF??for <Parts.Uppercase@domain.tld>; Thu, 29 Jul 2021 08:12:48 +0000 (UTC)
smtp_1       | Jul 29 08:12:48 mail postfix/cleanup[438]: 8761212C3BF: message-id=<AM0PR06MB55379A9FD7E4A515BF6F605DFAEB9@AM0PR06MB5537.eurprd06.prod.outlook.com>
smtp_1       | Jul 29 08:12:48 mail postfix/qmgr[388]: 8761212C3BF: from=<sender@domain.tld>, size=1741, nrcpt=1 (queue active)
smtp_1       | Jul 29 08:12:48 mail postfix/smtpd[434]: disconnect from unknown[ip.ip.ip.ip] ehlo=2 xclient=0/1 mail=1 rcpt=1 data=1 quit=1 commands=6/7
smtp_1       | Jul 29 08:12:48 mail postfix/lmtp[439]: 8761212C3BF: to=<catchall@domain.tld>, orig_to=<Parts.Uppercase@domain.tld>, relay=172.18.0.7[172.18.0.7]:2525, delay=0.19, delays=0.08/0/0/0.11, dsn=2.0.0, status=sent (250 2.0.0 <catchall@domain.tld> EC/3IwBjAmFRAAAAIuytJQ Saved)
smtp_1       | Jul 29 08:12:48 mail postfix/qmgr[388]: 8761212C3BF: removed
smtp_1       | Jul 29 08:13:05 mail postfix/smtpd[398]: connect from localhost[127.0.0.1]
smtp_1       | Jul 29 08:13:05 mail postfix/smtpd[398]: disconnect from localhost[127.0.0.1] quit=1 commands=1
```

Please stick to the reporting template.

I have tried and managed to receive emails sent to AdMiN@Test.Mailu.IO (see https://mailu.io/master/demo.html). Please replicate what you think is relevant of your configuration on the test server and submit a new report if you can reproduce it.

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
@@ -57,10 +57,9 @@ class IdnaEmail(db.TypeDecorator):
 
     def process_bind_param(self, value, dialect):
         """ encode unicode domain part of email address to punycode """
-        localpart, domain_name = value.rsplit('@', 1)
+        localpart, domain_name = value.lower().rsplit('@', 1)
         if '@' in localpart:
             raise ValueError('email local part must not contain "@"')
-        domain_name = domain_name.lower()
         return f'{localpart}@{idna.encode(domain_name).decode("ascii")}'
 
     def process_result_value(self, value, dialect):
@@ -277,6 +276,7 @@ def generate_dkim_key(self):
 
     def has_email(self, localpart):
         """ checks if localpart is configured for domain """
+        localpart = localpart.lower()
         for email in chain(self.users, self.aliases):
             if email.localpart == localpart:
                 return True
@@ -355,8 +355,8 @@ def email(self):
     @email.setter
     def email(self, value):
         """ setter for email - sets _email, localpart and domain_name at once """
-        self.localpart, self.domain_name = value.rsplit('@', 1)
-        self._email = value
+        self._email = value.lower()
+        self.localpart, self.domain_name = self._email.rsplit('@', 1)
 
     @staticmethod
     def _update_localpart(target, value, *_):
@@ -389,8 +389,7 @@ def sendmail(self, subject, body):
     def resolve_domain(cls, email):
         """ resolves domain alternative to real domain """
         localpart, domain_name = email.rsplit('@', 1) if '@' in email else (None, email)
-        alternative = Alternative.query.get(domain_name)
-        if alternative:
+        if alternative := Alternative.query.get(domain_name):
             domain_name = alternative.domain_name
         return (localpart, domain_name)
 
@@ -401,12 +400,14 @@ 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]
+        delim = os.environ.get('RECIPIENT_DELIMITER')
+        if delim in localpart:
+            localpart_stripped = localpart.rsplit(delim, 1)[0]
 
         user = User.query.get(f'{localpart}@{domain_name}')
         if not user and localpart_stripped:
             user = User.query.get(f'{localpart_stripped}@{domain_name}')
+
         if user:
             email = f'{localpart}@{domain_name}'
 
@@ -416,15 +417,15 @@ def resolve_destination(cls, localpart, domain_name, ignore_forward_keep=False):
                     destination.append(email)
             else:
                 destination = [email]
+
             return 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
 
-        if stripped_alias:
+        if stripped_alias := Alias.resolve(localpart_stripped, domain_name):
             return stripped_alias.destination
 
         if pure_alias:


Test patch: