From 3407aa262f3ba02884d7c83006b85ad60a32691a Mon Sep 17 00:00:00 2001 From: Thomas Gleixner Date: Wed, 2 Sep 2020 15:28:38 +0200 Subject: [PATCH] remail: Retrieve sender information For open lists and especially contact points the 'From' mangling is suboptimal as the senders email address is not contained in the mail itself. Due to re-encryption a eventual signature is not longer intact which means that the GPG key or the S/MIME certificate which are embedded into the signature are not transported either. Add infrastructure to collect sender information including key/certificate if available and attach it to the mail. The first attachment contains sender information and the second one if available contains the key or the certificate. The information is only stored when the config switch is enabled and the sender is not subscribed to the list. Signed-off-by: Thomas Gleixner --- Documentation/man5/remail.config.rst | 22 +++++++++++---- remail/config.py | 1 + remail/gpg.py | 2 +- remail/mail.py | 35 +++++++++++++++++++++++ remail/maillist.py | 42 +++++++++++++++++----------- remail/smime.py | 2 +- 6 files changed, 80 insertions(+), 24 deletions(-) diff --git a/Documentation/man5/remail.config.rst b/Documentation/man5/remail.config.rst index 564ecf0..a7035b6 100644 --- a/Documentation/man5/remail.config.rst +++ b/Documentation/man5/remail.config.rst @@ -260,9 +260,10 @@ The list base configuration for each list consists of the following items: .. code-block:: yaml listname: - enabled: True - moderated: True - listid: ... + enabled: True + moderated: True + attach_sender_info: False + listid: ... archive: ... listaccount: @@ -276,8 +277,9 @@ The list base items: .. code-block:: yaml listname: - enabled: True - moderated: True + enabled: True + moderated: True + attach_sender_info: False enabled: @@ -292,6 +294,16 @@ The list base items: with a subscriber. Mails from non-subscribers are not delivered to the list, they are delivered to the list administrator + attach_sender_info: + + Collects information about the sender, email-address, encryption method + and if the mail is signed the GPG key or S/MIME certificate contained + in the signature. This information is attached to the original mail as + two seperate attachments (text info and key/certificate) if this is + enabled and the sender is not subscribed to the list. This is for open + lists and especially contact points so that the subscribers are able + to contact the sender. + listid: Optional item to override the default list-id with a custom value. diff --git a/remail/config.py b/remail/config.py index f8400fe..a01aa78 100644 --- a/remail/config.py +++ b/remail/config.py @@ -261,6 +261,7 @@ class archive_options(object): list_defaults = { 'enabled' : False, 'moderated' : False, + 'attach_sender_info' : False, } class list_config(object): diff --git a/remail/gpg.py b/remail/gpg.py index 442c413..cf49ed3 100644 --- a/remail/gpg.py +++ b/remail/gpg.py @@ -175,7 +175,7 @@ class gpg_crypt(object): msg_set_payload(msg, pl) return msg - def decrypt(self, msg): + def decrypt(self, msg, sinfo): ''' Try to handle received mail with PGP. Return decoded or plain mail ''' diff --git a/remail/mail.py b/remail/mail.py index b14a2f3..d9efe68 100644 --- a/remail/mail.py +++ b/remail/mail.py @@ -20,6 +20,41 @@ import time import sys import re +class sender_info(object): + def __init__(self, msg): + self.mfrom = msg.get('From') + self.info = None + + def get_info(self): + info = 'Sent from: %s\n' %self.mfrom + if self.info: + info += self.info.get_info() + else: + info += 'No GPG/SMIME info available' + return info + + def store_in_msg(self, msg): + # Convert the message into a multipart/mixed message + ct = msg.get_content_type() + if ct != 'multipart/mixed': + msg.make_mixed() + + # Attach the sender information as plain/text + msg.add_attachment(self.get_info()) + + if not self.info: + return + + # Check whether the GPG key or the S/MIME cert is + # available and attach it. + fname, data, maintype, subtype = self.info.get_file() + if fname: + if maintype != 'plain': + msg.add_attachment(data, filename=fname, maintype=maintype, + subtype=subtype) + else: + msg.add_attachment(data, filename=fname) + def sanitize_headers(msg): ''' Sanitize headers by keeping only the ones which are interesting diff --git a/remail/maillist.py b/remail/maillist.py index 9a95795..1df7c31 100644 --- a/remail/maillist.py +++ b/remail/maillist.py @@ -7,7 +7,7 @@ from remail.mail import msg_set_header, msg_force_msg_id, send_mail from remail.mail import msg_sanitize_incoming, msg_is_autoreply from remail.mail import get_raw_email_addr, decode_addrs -from remail.mail import msg_from_string +from remail.mail import msg_from_string, sender_info from remail.smime import smime_crypt, RemailSmimeException from remail.gpg import gpg_crypt, RemailGPGException @@ -138,7 +138,7 @@ class maillist(object): mbox.add(msg) mbox.close() - def decrypt_mail(self, msg): + def decrypt_mail(self, msg, sinfo): ''' Decrypt mail after sanitizing it from HTML and outlook magic and decoding base64 transport. @@ -147,9 +147,9 @@ class maillist(object): msg_plain = None if self.smime: - msg_plain = self.smime.decrypt(msg) + msg_plain = self.smime.decrypt(msg, sinfo) if not msg_plain: - msg_plain = self.gpg.decrypt(msg) + msg_plain = self.gpg.decrypt(msg, sinfo) return msg_plain def disable_subscribers(self, addrs, msgid): @@ -180,7 +180,11 @@ class maillist(object): subj = prefix + msg['Subject'] msg_set_header(msg, 'Subject', subj) - def moderate(self, msg, dest): + def is_subscribed(self, mfrom): + r = self.config.subscribers.has_account(mfrom) + return r or self.config.admins.has_account(mfrom) + + def moderate(self, msg, dest, mfrom): ''' If the list is moderated make sure that the sender of a mail is subscribed or an administrator. This checks also aliases. @@ -188,10 +192,7 @@ class maillist(object): if not self.config.moderated: return False - mfrom = get_raw_email_addr(msg.get('From')) - r = self.config.subscribers.has_account(mfrom) - r = r or self.config.admins.has_account(mfrom) - if not r: + if not self.is_subscribed(mfrom): self.modsubject(msg, '[MODERATED] ') dest.toadmins = True dest.accounts = self.config.admins @@ -219,7 +220,7 @@ class maillist(object): dest.toadmins = True dest.accounts = self.config.admins - def mangle_from(self, msg): + def mangle_from(self, msg, mfrom): ''' Build 'From' string so the original 'From' is 'visible': From: $LISTNAME for $ORIGINAL_FROM <$LISTADDRESS> @@ -227,23 +228,25 @@ class maillist(object): If $ORIGINAL_FROM does not contain a name, mangle the email address by replacing @ with _at_ ''' - mfrom = msg.get('From').split('<')[0].replace('@', '_at_').strip() return '%s for %s <%s>' % (self.config.name, mfrom, self.config.listaddrs.post) def do_process_mail(self, msg, dest): + + msgid = msg.get('Message-Id', '') + msgto = msg.get('To') + msgfrom = get_raw_email_addr(msg.get('From')) + sinfo = sender_info(msg) + # Archive the incoming mail self.archive_mail(msg, incoming=True) # Destination was already established. Check for bounces first self.check_bounces(msg, dest) # Check for moderation - self.moderate(msg, dest) - - msgid = msg.get('Message-Id', '') - msgto = msg.get('To') + self.moderate(msg, dest, msgfrom) try: - msg_plain = self.decrypt_mail(msg) + msg_plain = self.decrypt_mail(msg, sinfo) except Exception as ex: txt = 'Failed to decrypt incoming %s to %s\n' %(msgid, msgto) self.logger.log_exception(txt, ex) @@ -251,7 +254,12 @@ class maillist(object): self.archive_mail(msg_plain, admin=dest.toadmin) - mfrom = self.mangle_from(msg) + mfrom = self.mangle_from(msg, msgfrom) + # Save sender information in the outgoing message? + if self.config.attach_sender_info: + # Only do so for non-subscribers + if not self.is_subscribed(msgfrom): + sinfo.store_in_msg(msg_plain) for account in dest.accounts.values(): if not account.enabled: diff --git a/remail/smime.py b/remail/smime.py index fd9e60c..a55c415 100644 --- a/remail/smime.py +++ b/remail/smime.py @@ -158,7 +158,7 @@ class smime_crypt(object): return None - def decrypt(self, msg): + def decrypt(self, msg, sinfo): try: envto = msg.get('To', None) msgid = msg.get('Message-Id', None)