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
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.

View file

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

View file

@ -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
'''

View file

@ -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

View file

@ -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', '<No 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', '<No 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:

View file

@ -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)