Refactored code (and also optimizing code).

Changes while refactoring: The gateway now handles mail addresses case sensitivity compitable to RFC 2821 as default.
This commit is contained in:
fkrone 2015-03-02 13:13:30 +01:00
parent 60356f3a22
commit a766a9bd4a
2 changed files with 248 additions and 192 deletions

View File

@ -4,7 +4,7 @@
add_header = yes
# whether we should only sign emails if they are explicitly defined in
# the key mappings below ([keymap] section)
# the key mappings below ([keymap] section)
# this means gpg-mailgate won't automatically detect PGP recipients
keymap_only = no
@ -12,11 +12,12 @@ keymap_only = no
# (Default is to use older inline-style PGP encoding.)
mime_conversion = yes
# RFC 2821 defines that the user part of a mail address should be treated case sensitive.
# However in the real world this is ignored very often. This option enforces that
# email addresses are treated according to RFC 2821. If you encounter any problems
# based on the case sensitivity of mail addresses, set this setting to yes.
mail_case_sensitive = no
# RFC 2821 defines that the user part (User@domain.tld) of a mail address should be treated case sensitive.
# However, in the real world this is ignored very often. This option disables the RFC 2821
# compatibility so both the user part and the domain part are treated case insensitive.
# Disabling the compatibility is more convenient to users. So if you know that your
# recipients all ignore the RFC you could this to yes.
mail_case_insensensitive = no
[gpg]
# the directory where gpg-mailgate public keys are stored

View File

@ -48,17 +48,15 @@ for sect in _cfg.sections():
cfg[sect][name] = value
def log(msg):
if cfg.has_key('logging') and cfg['logging'].has_key('file'):
if cfg['logging']['file'] == "syslog":
if 'logging' in cfg and 'file' in cfg['logging']:
if cfg['logging'].get('file') == "syslog":
syslog.syslog(syslog.LOG_INFO | syslog.LOG_MAIL, msg)
else:
logfile = open(cfg['logging']['file'], 'a')
logfile.write(msg + "\n")
logfile.close()
verbose = cfg.has_key('logging') and cfg['logging'].has_key('verbose') and cfg['logging']['verbose'] == 'yes'
CERT_PATH = cfg['smime']['cert_path']+"/"
verbose = 'logging' in cfg and 'verbose' in cfg['logging'] and cfg['logging'].get('verbose') == 'yes'
# Read e-mail from stdin
raw = sys.stdin.read()
@ -66,43 +64,90 @@ raw_message = email.message_from_string( raw )
from_addr = raw_message['From']
to_addrs = sys.argv[1:]
def send_msg( message, recipients = None ):
if recipients == None:
recipients = to_addrs
recipients = filter(None, recipients)
if recipients:
log("Sending email to: <%s>" % '> <'.join( recipients ))
relay = (cfg['relay']['host'], int(cfg['relay']['port']))
smtp = smtplib.SMTP(relay[0], relay[1])
smtp.sendmail( from_addr, recipients, message )
else:
log("No recipient found");
def gpg_encrypt(raw_message, recipients):
def encrypt_payload( payload, gpg_to_cmdline ):
raw_payload = payload.get_payload(decode=True)
if "-----BEGIN PGP MESSAGE-----" in raw_payload and "-----END PGP MESSAGE-----" in raw_payload:
return payload
gpg = GnuPG.GPGEncryptor( cfg['gpg']['keyhome'], gpg_to_cmdline, payload.get_content_charset() )
gpg.update( raw_payload )
encrypted_data, returncode = gpg.encrypt()
if verbose:
log("Return code from encryption=%d (0 indicates success)." % returncode)
payload.set_payload( encrypted_data )
isAttachment = payload.get_param( 'attachment', None, 'Content-Disposition' ) is not None
if isAttachment:
filename = payload.get_filename()
if filename:
pgpFilename = filename + ".pgp"
if payload.get('Content-Disposition') is not None:
payload.set_param( 'filename', pgpFilename, 'Content-Disposition' )
if payload.get('Content-Type') is not None:
if payload.get_param( 'name' ) is not None:
payload.set_param( 'name', pgpFilename )
if payload.get('Content-Transfer-Encoding') is not None:
payload.replace_header( 'Content-Transfer-Encoding', "7bit" )
return payload
if not get_bool_from_cfg('gpg', 'keyhome'):
log("No valid entry for gpg keyhome. Encryption aborted.")
return recipients
keys = GnuPG.public_keys( cfg['gpg']['keyhome'] )
for fingerprint in keys:
keys[fingerprint] = sanitize_case_sense(keys[fingerprint])
gpg_to = list()
ungpg_to = list()
for to in recipients:
if to in keys.values() and not get_bool_from_cfg('default', 'keymap_only', 'yes'):
gpg_to.append( (to, to) )
elif get_bool_from_cfg('keymap') and to in cfg['keymap']:
log("Keymap has key '%s'" % cfg['keymap'][to] )
# Check we've got a matching key! If not, decline to attempt encryption.
if not cfg['keymap'][to] in keys:
log("Key '%s' in keymap not found in keyring for email address '%s'. Won't encrypt." % (cfg['keymap'][to], to))
ungpg_to.append(to)
else:
gpg_to.append( (to, cfg['keymap'][to]) )
else:
if verbose:
log("Recipient (%s) not in PGP domain list." % to)
ungpg_to.append(to)
if gpg_to != list():
log("Encrypting email to: %s" % ' '.join( map(lambda x: x[0], gpg_to) ))
gpg_to_smtp_mime = list()
gpg_to_cmdline_mime = list()
gpg_to_smtp_inline = list()
gpg_to_cmdline_inline = list()
for rcpt in gpg_to:
if get_bool_from_cfg('pgp_style', rcpt[0], 'mime'):
gpg_to_smtp_mime.append(rcpt[0])
gpg_to_cmdline_mime.extend(rcpt[1].split(','))
elif get_bool_from_cfg('pgp_style', rcpt[0], 'inline'):
gpg_to_smtp_inline.append(rcpt[0])
gpg_to_cmdline_inline.extend(rcpt[1].split(','))
else:
# Log message only if an unknown style is defined
if get_bool_from_cfg('pgp_style', rcpt[0]):
log("Style %s for recipient %s is not known. Use default as fallback." % (cfg['pgp_style'][rcpt[0]], rcpt[0]))
if get_bool_from_cfg('default', 'mime_conversion', 'yes'):
gpg_to_smtp_mime.append(rcpt[0])
gpg_to_cmdline_mime.extend(rcpt[1].split(','))
else:
gpg_to_smtp_inline.append(rcpt[0])
gpg_to_cmdline_inline.extend(rcpt[1].split(','))
if gpg_to_smtp_mime != list():
raw_message_mime = copy.deepcopy(raw_message)
if get_bool_from_cfg('default', 'add_header', 'yes'):
raw_message_mime['X-GPG-Mailgate'] = 'Encrypted by GPG Mailgate'
encrypted_payloads = encrypt_all_payloads_mime( raw_message_mime, gpg_to_cmdline_mime )
raw_message_mime.set_payload( encrypted_payloads )
send_msg( raw_message_mime.as_string(), gpg_to_smtp_mime )
if gpg_to_smtp_inline != list():
raw_message_inline = copy.deepcopy(raw_message)
if get_bool_from_cfg('default', 'add_header', 'yes'):
raw_message_inline['X-GPG-Mailgate'] = 'Encrypted by GPG Mailgate'
encrypted_payloads = encrypt_all_payloads_inline( raw_message_inline, gpg_to_cmdline_inline )
raw_message_inline.set_payload( encrypted_payloads )
send_msg( raw_message_inline.as_string(), gpg_to_smtp_inline )
return ungpg_to
def encrypt_all_payloads_inline( message, gpg_to_cmdline ):
encrypted_payloads = list()
if type( message.get_payload() ) == str:
return encrypt_payload( message, gpg_to_cmdline ).get_payload()
@ -112,36 +157,24 @@ def encrypt_all_payloads_inline( message, gpg_to_cmdline ):
encrypted_payloads.extend( encrypt_all_payloads_inline( payload, gpg_to_cmdline ) )
else:
encrypted_payloads.append( encrypt_payload( payload, gpg_to_cmdline ) )
return encrypted_payloads
def generate_attachment_pgp(message, submsg = None):
if submsg == None:
submsg = email.message.Message()
submsg.set_type("multipart/mixed")
submsg.set_param('inline', "", 'Content-Disposition' )
def encrypt_all_payloads_mime( message, gpg_to_cmdline ):
for payload in message.get_payload():
if( type( payload.get_payload() ) == list ):
submsg.attach(generate_attachment_pgp(payload, submsg))
else:
submsg.attach(payload)
return submsg
def encrypt_all_payloads_attachment_style( message, gpg_to_cmdline ):
encrypted_payloads = list()
# Convert a plain text email into PGP/MIME attachment style. Modeled after enigmail.
submsg1 = email.message.Message()
submsg1.set_payload("Version: 1\n")
submsg1.set_type("application/pgp-encrypted")
submsg1.set_param('PGP/MIME version identification', "", 'Content-Description' )
submsg2 = email.message.Message()
submsg2.set_type("application/octet-stream")
submsg2.set_param('name', "encrypted.asc")
submsg2.set_param('OpenPGP encrypted message', "", 'Content-Description' )
submsg2.set_param('inline', "", 'Content-Disposition' )
submsg2.set_param('filename', "encrypted.asc", 'Content-Disposition' )
if type ( message.get_payload() ) == str:
# WTF! It seems to swallow the first line. Not sure why. Perhaps
# it's skipping an imaginary blank line someplace. (ie skipping a header)
@ -149,10 +182,13 @@ def encrypt_all_payloads_attachment_style( message, gpg_to_cmdline ):
# This happens only on text only messages.
submsg2.set_payload("\n" + message.get_payload())
else:
submsg2.set_payload(generate_attachment_pgp(message).as_string())
processed_payloads = generate_message_from_payloads(message)
processed_payloads.set_type("multipart/mixed")
processed_payloads.set_param('inline', "", 'Content-Disposition' )
submsg2.set_payload(processed_payloads.as_string())
message.preamble = "This is an OpenPGP/MIME encrypted message (RFC 2440 and 3156)"
# Use this just to generate a MIME boundary string.
junk_msg = MIMEMultipart()
junk_str = junk_msg.as_string() # WTF! Without this, get_boundary() will return 'None'!
@ -166,58 +202,69 @@ def encrypt_all_payloads_attachment_style( message, gpg_to_cmdline ):
return [ submsg1, encrypt_payload(submsg2, gpg_to_cmdline) ]
# This method is not referenced
def get_msg( message ):
if not message.is_multipart():
return message.get_payload()
return '\n\n'.join( [str(m) for m in message.get_payload()] )
def encrypt_payload( payload, gpg_to_cmdline ):
raw_payload = payload.get_payload(decode=True)
if "-----BEGIN PGP MESSAGE-----" in raw_payload and "-----END PGP MESSAGE-----" in raw_payload:
if verbose:
log("Message is already pgp encrypted. No nested encryption needed.")
return payload
# No check is needed for cfg['gpg']['keyhome'] as this is already done in method gpg_encrypt
gpg = GnuPG.GPGEncryptor( cfg['gpg']['keyhome'], gpg_to_cmdline, payload.get_content_charset() )
gpg.update( raw_payload )
encrypted_data, returncode = gpg.encrypt()
if verbose:
log("Return code from encryption=%d (0 indicates success)." % returncode)
if returncode != 0:
log("Encrytion failed with return code %d. Encryption aborted.")
return payload
payload.set_payload( encrypted_data )
isAttachment = payload.get_param( 'attachment', None, 'Content-Disposition' ) is not None
if isAttachment:
filename = payload.get_filename()
if filename:
pgpFilename = filename + ".pgp"
if not (payload.get('Content-Disposition') is None):
payload.set_param( 'filename', pgpFilename, 'Content-Disposition' )
if not (payload.get('Content-Type') is None) and not (payload.get_param( 'name' ) is None):
payload.set_param( 'name', pgpFilename )
if not (payload.get('Content-Transfer-Encoding') is None):
payload.replace_header( 'Content-Transfer-Encoding', "7bit" )
return payload
def smime_encrypt( raw_message, recipients ):
def get_cert_for_email(to_addr):
files_in_directory = os.listdir(CERT_PATH)
for filename in files_in_directory:
file_path = os.path.join(CERT_PATH, filename)
if not os.path.isfile(file_path): continue
if cfg['default'].has_key('mail_case_sensitive') and cfg['default']['mail_case_sensitive'] == 'yes':
if filename == to_addr: return (file_path, to_addr)
else:
if filename.lower() == to_addr: return (file_path, to_addr)
# support foo+ignore@bar.com -> foo@bar.com
multi_email = re.match('^([^\+]+)\+([^@]+)@(.*)$', to_addr)
if multi_email:
fixed_up_email = "%s@%s" % (multi_email.group(1), multi_email.group(3))
log("Multi-email %s converted to %s" % (to_addr, fixed_up_email))
return get_cert_for_email(fixed_up_email)
return None
def to_smime_handler( raw_message, recipients = None ):
if recipients == None:
recipients = to_addrs
if not get_bool_from_cfg('smime', 'cert_path'):
log("No valid path for S/MIME certs found in config file. S/MIME encryption aborted.")
return recipients
cert_path = cfg['smime']['cert_path']+"/"
s = SMIME.SMIME()
sk = X509.X509_Stack()
normalized_recipient = []
unsmime_to = list(recipients)
smime_to = list()
unsmime_to = list()
for addr in recipients:
addr_addr = email.utils.parseaddr(addr)[1]
if cfg['default'].has_key('mail_case_sensitive') and cfg['default']['mail_case_sensitive'] == 'yes':
splitted_addr_addr = addr_addr.split('@')
addr_addr = splitted_addr_addr[0] + '@' + splitted_addr_addr[1].lower()
else:
addr_addr = addr_addr.lower()
cert_and_email = get_cert_for_email(addr_addr)
if cert_and_email:
cert_and_email = get_cert_for_email(addr, cert_path)
if not (cert_and_email is None):
(to_cert, normal_email) = cert_and_email
unsmime_to.remove(addr)
log("Found cert " + to_cert + " for " + addr + ": " + normal_email)
normalized_recipient.append((email.utils.parseaddr(addr)[0], normal_email))
if verbose:
log("Found cert " + to_cert + " for " + addr + ": " + normal_email)
smime_to.append(addr)
x509 = X509.load_cert(to_cert, format=X509.FORMAT_PEM)
sk.push(x509)
if len(normalized_recipient):
smime_to = [email.utils.formataddr(x) for x in normalized_recipient]
else:
unsmime_to.append(addr)
if smime_to != list():
s.set_x509_stack(sk)
s.set_cipher(SMIME.Cipher('aes_192_cbc'))
p7 = s.encrypt( BIO.MemoryBuffer(raw_message.as_string()) )
p7 = s.encrypt( BIO.MemoryBuffer( raw_message.as_string() ) )
# Output p7 in mail-friendly format.
out = BIO.MemoryBuffer()
out.write('From: ' + from_addr + '\n')
@ -228,106 +275,114 @@ def to_smime_handler( raw_message, recipients = None ):
out.write('Bcc: ' + raw_message['Bcc'] + '\n')
if raw_message['Subject']:
out.write('Subject: '+ raw_message['Subject'] + '\n')
if cfg['default'].has_key('add_header') and cfg['default']['add_header'] == 'yes':
if get_bool_from_cfg('default', 'add_header', 'yes'):
out.write('X-GPG-Mailgate: Encrypted by GPG Mailgate\n')
s.write(out, p7)
log("Sending message from " + from_addr + " to " + str(smime_to))
raw_msg = out.read()
send_msg(raw_msg, smime_to)
if len(unsmime_to):
log("Unable to find valid S/MIME certificates for " + str(unsmime_to))
send_msg(raw_message.as_string(), unsmime_to)
if verbose:
log("Sending message from " + from_addr + " to " + str(smime_to))
send_msg(out.read(), smime_to)
if unsmime_to != list():
if verbose:
log("Unable to find valid S/MIME certificates for " + str(unsmime_to))
return unsmime_to
def get_cert_for_email( to_addr, cert_path ):
files_in_directory = os.listdir(cert_path)
for filename in files_in_directory:
file_path = os.path.join(cert_path, filename)
if not os.path.isfile(file_path):
continue
if get_bool_from_cfg('default', 'mail_case_insensitive', 'yes'):
if filename.lower() == to_addr:
return (file_path, to_addr)
else:
if filename == to_addr:
return (file_path, to_addr)
# support foo+ignore@bar.com -> foo@bar.com
multi_email = re.match('^([^\+]+)\+([^@]+)@(.*)$', to_addr)
if multi_email:
fixed_up_email = "%s@%s" % (multi_email.group(1), multi_email.group(3))
if verbose:
log("Multi-email %s converted to %s" % (to_addr, fixed_up_email))
return get_cert_for_email(fixed_up_email)
return None
def get_bool_from_cfg( section, key = None, evaluation = None ):
keys = GnuPG.public_keys( cfg['gpg']['keyhome'] )
if cfg['default'].has_key('mail_case_sensitive') and cfg['default']['mail_case_sensitive'] == 'yes':
for fingerprint in keys:
splitted_address = keys[fingerprint].split('@')
keys[fingerprint] = splitted_address[0] + '@' + splitted_address[1].lower()
else:
for fingerprint in keys:
keys[fingerprint] = keys[fingerprint].lower()
gpg_to = list()
ungpg_to = list()
if not (key is None) and not (evaluation is None):
return section in cfg and cfg[section].get(key) == evaluation
for to in to_addrs:
if cfg['default'].has_key('mail_case_sensitive') and cfg['default']['mail_case_sensitive'] == 'yes':
splitted_to = to.split('@')
to = splitted_to[0] + '@' + splitted_to[1].lower()
else:
to = to.lower()
elif not (key is None) and (evaluation is None):
return section in cfg and not (cfg[section].get(key) is None)
if to in keys.values() and not ( cfg['default'].has_key('keymap_only') and cfg['default']['keymap_only'] == 'yes' ):
gpg_to.append( (to, to) )
elif cfg.has_key('keymap') and cfg['keymap'].has_key(to):
log("Keymap has key '%s'" % cfg['keymap'][to] )
# Check we've got a matching key! If not, decline to attempt encryption.
if not keys.has_key(cfg['keymap'][to]):
log("Key '%s' in keymap not found in keyring for email address '%s'. Won't encrypt." % (cfg['keymap'][to], to))
ungpg_to.append(to)
else:
gpg_to.append( (to, cfg['keymap'][to]) )
else:
if verbose:
log("Recipient (%s) not in PGP domain list." % to)
ungpg_to.append(to)
return section in cfg
if gpg_to == list():
if cfg['default'].has_key('add_header') and cfg['default']['add_header'] == 'yes':
raw_message['X-GPG-Mailgate'] = 'Not encrypted, public key not found'
if verbose:
log("No PGP encrypted recipients.")
to_smime_handler( raw_message )
exit()
def sanitize_case_sense( address ):
if ungpg_to != list():
to_smime_handler( raw_message, ungpg_to )
log("Encrypting email to: %s" % ' '.join( map(lambda x: x[0], gpg_to) ))
if cfg['default'].has_key('add_header') and cfg['default']['add_header'] == 'yes':
raw_message['X-GPG-Mailgate'] = 'Encrypted by GPG Mailgate'
gpg_to_smtp_mime = list()
gpg_to_cmdline_mime = list()
gpg_to_smtp_inline = list()
gpg_to_cmdline_inline = list()
for rcpt in gpg_to:
if cfg.has_key('pgp_style') and cfg['pgp_style'].has_key(rcpt[0]):
if cfg['pgp_style'][rcpt[0]] == 'mime':
gpg_to_smtp_mime.append(rcpt[0])
gpg_to_cmdline_mime.extend(rcpt[1].split(','))
elif cfg['pgp_style'][rcpt[0]] == 'inline':
gpg_to_smtp_inline.append(rcpt[0])
gpg_to_cmdline_inline.extend(rcpt[1].split(','))
else:
log("Style %s for recipient %s is not known. Use default as fallback." % (cfg['pgp_style'][rcpt[0]], rcpt[0]))
if cfg['default'].has_key('mime_conversion') and cfg['default']['mime_conversion'] == 'yes':
gpg_to_smtp_mime.append(rcpt[0])
gpg_to_cmdline_mime.extend(rcpt[1].split(','))
else:
gpg_to_smtp_inline.append(rcpt[0])
gpg_to_cmdline_inline.extend(rcpt[1].split(','))
elif cfg['default'].has_key('mime_conversion') and cfg['default']['mime_conversion'] == 'yes':
gpg_to_smtp_mime.append(rcpt[0])
gpg_to_cmdline_mime.extend(rcpt[1].split(','))
if get_bool_from_cfg('default', 'mail_case_insensitive', 'yes'):
address = address.lower()
else:
gpg_to_smtp_inline.append(rcpt[0])
gpg_to_cmdline_inline.extend(rcpt[1].split(','))
splitted_address = address.split('@')
address = splitted_address[0] + '@' + splitted_address[1].lower()
if gpg_to_smtp_mime != list():
return address
raw_message_mime = copy.deepcopy(raw_message)
encrypted_payloads = encrypt_all_payloads_attachment_style( raw_message_mime, gpg_to_cmdline_mime )
raw_message_mime.set_payload( encrypted_payloads )
def generate_message_from_payloads( payloads, submsg = None ):
send_msg( raw_message_mime.as_string(), gpg_to_smtp_mime )
if gpg_to_smtp_inline != list():
if submsg == None:
submsg = email.message.Message()
encrypted_payloads = encrypt_all_payloads_inline( raw_message, gpg_to_cmdline_inline )
raw_message.set_payload( encrypted_payloads )
for payload in payloads.get_payload():
if( type( payload.get_payload() ) == list ):
submsg.attach(generate_message_from_payloads(payload, submsg))
else:
submsg.attach(payload)
send_msg( raw_message.as_string(), gpg_to_smtp_inline )
return submsg
def send_msg( message, recipients ):
recipients = filter(None, recipients)
if recipients:
if not (get_bool_from_cfg('relay', 'host') and get_bool_from_cfg('relay', 'port')):
log("Missing settings for relay. Sending email aborted.")
return None
log("Sending email to: <%s>" % '> <'.join( recipients ))
relay = (cfg['relay']['host'], int(cfg['relay']['port']))
smtp = smtplib.SMTP(relay[0], relay[1])
smtp.sendmail( from_addr, recipients, message )
else:
log("No recipient found")
def sort_recipients( raw_message, from_addr, to_addrs ):
recipients_left = list()
for recipient in to_addrs:
recipients_left.append(sanitize_case_sense(recipient))
# Encrypt mails for recipients with known PGP keys
recipients_left = gpg_encrypt(raw_message, recipients_left)
if recipients_left == list():
return
# Encrypt mails for recipients with known S/MIME certificate
recipients_left = smime_encrypt(raw_message, recipients_left)
if recipients_left == list():
return
# Send out mail to recipients which are left
send_msg(raw_message.as_string(), recipients_left)
# Let's start
sort_recipients(raw_message, from_addr, to_addrs)