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)