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 <tglx@linutronix.de>
This commit is contained in:
Thomas Gleixner 2020-09-02 15:28:38 +02:00
parent ea361f973c
commit 3407aa262f
6 changed files with 80 additions and 24 deletions

View file

@ -260,9 +260,10 @@ The list base configuration for each list consists of the following items:
.. code-block:: yaml .. code-block:: yaml
listname: listname:
enabled: True enabled: True
moderated: True moderated: True
listid: ... attach_sender_info: False
listid: ...
archive: archive:
... ...
listaccount: listaccount:
@ -276,8 +277,9 @@ The list base items:
.. code-block:: yaml .. code-block:: yaml
listname: listname:
enabled: True enabled: True
moderated: True moderated: True
attach_sender_info: False
enabled: enabled:
@ -292,6 +294,16 @@ The list base items:
with a subscriber. Mails from non-subscribers are not delivered to the with a subscriber. Mails from non-subscribers are not delivered to the
list, they are delivered to the list administrator 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: listid:
Optional item to override the default list-id with a custom value. Optional item to override the default list-id with a custom value.

View file

@ -261,6 +261,7 @@ class archive_options(object):
list_defaults = { list_defaults = {
'enabled' : False, 'enabled' : False,
'moderated' : False, 'moderated' : False,
'attach_sender_info' : False,
} }
class list_config(object): class list_config(object):

View file

@ -175,7 +175,7 @@ class gpg_crypt(object):
msg_set_payload(msg, pl) msg_set_payload(msg, pl)
return msg return msg
def decrypt(self, msg): def decrypt(self, msg, sinfo):
''' '''
Try to handle received mail with PGP. Return decoded or plain mail Try to handle received mail with PGP. Return decoded or plain mail
''' '''

View file

@ -20,6 +20,41 @@ import time
import sys import sys
import re 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): def sanitize_headers(msg):
''' '''
Sanitize headers by keeping only the ones which are interesting Sanitize headers by keeping only the ones which are interesting

View file

@ -7,7 +7,7 @@
from remail.mail import msg_set_header, msg_force_msg_id, send_mail 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 msg_sanitize_incoming, msg_is_autoreply
from remail.mail import get_raw_email_addr, decode_addrs 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.smime import smime_crypt, RemailSmimeException
from remail.gpg import gpg_crypt, RemailGPGException from remail.gpg import gpg_crypt, RemailGPGException
@ -138,7 +138,7 @@ class maillist(object):
mbox.add(msg) mbox.add(msg)
mbox.close() mbox.close()
def decrypt_mail(self, msg): def decrypt_mail(self, msg, sinfo):
''' '''
Decrypt mail after sanitizing it from HTML and outlook magic Decrypt mail after sanitizing it from HTML and outlook magic
and decoding base64 transport. and decoding base64 transport.
@ -147,9 +147,9 @@ class maillist(object):
msg_plain = None msg_plain = None
if self.smime: if self.smime:
msg_plain = self.smime.decrypt(msg) msg_plain = self.smime.decrypt(msg, sinfo)
if not msg_plain: if not msg_plain:
msg_plain = self.gpg.decrypt(msg) msg_plain = self.gpg.decrypt(msg, sinfo)
return msg_plain return msg_plain
def disable_subscribers(self, addrs, msgid): def disable_subscribers(self, addrs, msgid):
@ -180,7 +180,11 @@ class maillist(object):
subj = prefix + msg['Subject'] subj = prefix + msg['Subject']
msg_set_header(msg, 'Subject', subj) 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 If the list is moderated make sure that the sender of a mail
is subscribed or an administrator. This checks also aliases. is subscribed or an administrator. This checks also aliases.
@ -188,10 +192,7 @@ class maillist(object):
if not self.config.moderated: if not self.config.moderated:
return False return False
mfrom = get_raw_email_addr(msg.get('From')) if not self.is_subscribed(mfrom):
r = self.config.subscribers.has_account(mfrom)
r = r or self.config.admins.has_account(mfrom)
if not r:
self.modsubject(msg, '[MODERATED] ') self.modsubject(msg, '[MODERATED] ')
dest.toadmins = True dest.toadmins = True
dest.accounts = self.config.admins dest.accounts = self.config.admins
@ -219,7 +220,7 @@ class maillist(object):
dest.toadmins = True dest.toadmins = True
dest.accounts = self.config.admins 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': Build 'From' string so the original 'From' is 'visible':
From: $LISTNAME for $ORIGINAL_FROM <$LISTADDRESS> From: $LISTNAME for $ORIGINAL_FROM <$LISTADDRESS>
@ -227,23 +228,25 @@ class maillist(object):
If $ORIGINAL_FROM does not contain a name, mangle the email If $ORIGINAL_FROM does not contain a name, mangle the email
address by replacing @ with _at_ address by replacing @ with _at_
''' '''
mfrom = msg.get('From').split('<')[0].replace('@', '_at_').strip()
return '%s for %s <%s>' % (self.config.name, mfrom, return '%s for %s <%s>' % (self.config.name, mfrom,
self.config.listaddrs.post) self.config.listaddrs.post)
def do_process_mail(self, msg, dest): def do_process_mail(self, msg, dest):
msgid = msg.get('Message-Id', '<No ID>')
msgto = msg.get('To')
msgfrom = get_raw_email_addr(msg.get('From'))
sinfo = sender_info(msg)
# Archive the incoming mail # Archive the incoming mail
self.archive_mail(msg, incoming=True) self.archive_mail(msg, incoming=True)
# Destination was already established. Check for bounces first # Destination was already established. Check for bounces first
self.check_bounces(msg, dest) self.check_bounces(msg, dest)
# Check for moderation # Check for moderation
self.moderate(msg, dest) self.moderate(msg, dest, msgfrom)
msgid = msg.get('Message-Id', '<No ID>')
msgto = msg.get('To')
try: try:
msg_plain = self.decrypt_mail(msg) msg_plain = self.decrypt_mail(msg, sinfo)
except Exception as ex: except Exception as ex:
txt = 'Failed to decrypt incoming %s to %s\n' %(msgid, msgto) txt = 'Failed to decrypt incoming %s to %s\n' %(msgid, msgto)
self.logger.log_exception(txt, ex) self.logger.log_exception(txt, ex)
@ -251,7 +254,12 @@ class maillist(object):
self.archive_mail(msg_plain, admin=dest.toadmin) 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(): for account in dest.accounts.values():
if not account.enabled: if not account.enabled:

View file

@ -158,7 +158,7 @@ class smime_crypt(object):
return None return None
def decrypt(self, msg): def decrypt(self, msg, sinfo):
try: try:
envto = msg.get('To', None) envto = msg.get('To', None)
msgid = msg.get('Message-Id', None) msgid = msg.get('Message-Id', None)