[Remail] [patch v2 11/11] remail: Make From header mangling less convoluted

Thomas Gleixner tglx at linutronix.de
Sun Jun 18 22:13:54 CEST 2023


Due to the requirement to reencrypt the incoming mail, remail must rewrite
the mail headers and create a new email with the 'From:' header being the
list address. So remail implemented a scheme to mangle the original senders
name into the reencrypted mails 'From:' header.

That fell flat on its nose when there was an incoming mail which had no
name part and just consisted of the actual email address. Instead of
differentiating between these cases the authors sleep deprived and grump
laden brain decided to implement 'From:' mangling in the way it is now.

It converts the original sender mail address '[Name]
<mailname at sender.domain>' to:

  'list-name for mailname_at_sender.domain' <list-name at list.domain>

This is hard to read and follow. 

Use the common format:

  'Name via list-name' <list-name at list.domain>

Except for the case where the original 'From:' header is a plain email
address without a name. This will mangle it to:

  'mailname at sender.domain via list-name' <list-name at list.domain>

The 'via list-name' add on is technically not required but is useful to
visually differentiate the name in auto-completion.

Suggested-by: Linus Torvalds <torvalds at linux-foundation.org>
Signed-off-by: Thomas Gleixner <tglx at linutronix.de>
---
V1a: Remove the stray colon and fix the mistyped import
V2: Make it unconditional - Konstantin
    Quote non-ascii From correcly (formataddr() fails to do so)
---
 remail/maillist.py |   57 +++++++++++++++++++++++++++++++++++++++++------------
 1 file changed, 45 insertions(+), 12 deletions(-)

--- a/remail/maillist.py
+++ b/remail/maillist.py
@@ -14,7 +14,7 @@ from remail.gpg import gpg_crypt, Remail
 from remail.tracking import account_tracking
 from remail.config import accounts_config, gpg_config, smime_config
 
-from email.utils import make_msgid, formatdate, getaddresses
+from email.utils import make_msgid, formatdate, getaddresses, parseaddr
 from email.policy import EmailPolicy
 from flufl.bounce import all_failures
 
@@ -22,6 +22,10 @@ from ruamel.yaml import YAML
 
 import mailbox
 import os
+import re
+
+mustquote = re.compile(r'[][\\()<>@,:;".]')
+escapedquote = re.compile(r'[\\"]')
 
 class maillist(object):
     '''
@@ -228,24 +232,52 @@ import os
             dest.toadmins = True
             dest.accounts = self.config.admins
 
-    def mangle_from(self, msg, mfrom):
+    def mangle_from(self, msg, name, mfrom):
         '''
-        Build 'From' string so the original 'From' is 'visible':
-        From: $LISTNAME for $ORIGINAL_FROM <$LISTADDRESS>
+            If @name is non-empty then replace the 'From:' header with:
 
-        If $ORIGINAL_FROM does not contain a name, mangle the email
-        address by replacing @ with _at_
-        '''
-        mfrom = mfrom.replace('@','_at_')
-        return '%s for %s <%s>' % (self.config.name, mfrom,
-                                   self.config.listaddrs.post)
+              '@name via list-name' <list-name at list.domain>
+
+            If @name is empty mangle the email address @mfrom by replacing
+            '@' with at and setting the 'From:' header to:
+
+              '@mfrom via list-name' <list-name at list.domain>
+        '''
+        if not len(name):
+            name = mfrom.replace('@',' at ')
+
+        # Compose the display name
+        name = '%s via %s' %(name, self.config.name)
+
+        # Check whether it must be quoted
+        #
+        # email.utils.formataddr() fails to do so for non-ascii display
+        # names which causes some email-clients to display completely
+        # nonsensical names. Especially for the typical big corporate
+        # "name, surname" patterns as the resulting header entry becomes
+        #
+        #   From: =?utf-8?q?NAME?=, =?utf-8?q?SURNAME?= <n at doma.in>
+        # which some clients render as:
+        #  NAME@$SOMEMADEUPDOMAIN, SURNAME <n at doma.in>
+        #
+        # instead of
+        #   From: =?utf-8?q?NAME=2C_SURNAME?= <n.doma.in>
+        # which they correctly render as:
+        #  "NAME, SURNAME" <n at doma.in>
+        #
+        if mustquote.search(name):
+            name = escapedquote.sub(r'\\\g<0>', name)
+            name = '"%s"' %name
+
+        # The result is correcly handled by msg['From'] = result
+        return '%s <%s>' %(name, self.config.listaddrs.post)
 
     def do_process_mail(self, msg, dest):
 
         msgid = msg.get('Message-Id', '<No ID>')
         msgto = msg.get('To')
         origfrom = msg.get('From')
-        msgfrom = get_raw_email_addr(origfrom)
+        name, msgfrom = parseaddr(origfrom)
         sinfo = sender_info(msg)
 
         # Archive the incoming mail
@@ -264,7 +296,8 @@ import os
 
         self.archive_mail(msg_plain, admin=dest.toadmin)
 
-        mfrom = self.mangle_from(msg, msgfrom)
+        mfrom = self.mangle_from(msg, name, msgfrom)
+
         # Save sender information in the outgoing message?
         if self.config.attach_sender_info:
             # Only do so for non-subscribers



More information about the Remail mailing list