gpg-lacre/gpg-mailgate.py

333 lines
12 KiB
Python
Raw Normal View History

2010-01-20 21:53:25 +01:00
#!/usr/bin/python
#
# gpg-mailgate
#
# This file is part of the gpg-mailgate source code.
#
# gpg-mailgate is free software: you can redistribute it and/or modify
2013-10-02 20:27:28 +02:00
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# gpg-mailgate source code is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2013-10-02 20:27:28 +02:00
# GNU General Public License for more details.
#
2013-10-02 20:27:28 +02:00
# You should have received a copy of the GNU General Public License
# along with gpg-mailgate source code. If not, see <http://www.gnu.org/licenses/>.
#
2010-01-20 21:53:25 +01:00
from ConfigParser import RawConfigParser
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
2010-01-20 21:53:25 +01:00
import email
2013-04-14 13:13:52 +02:00
import email.message
import re
2010-01-20 21:53:25 +01:00
import GnuPG
import smtplib
import sys
import syslog
import traceback
import email.utils
import os
import copy
# imports for S/MIME
from M2Crypto import BIO, Rand, SMIME, X509
from email.mime.message import MIMEMessage
2010-01-20 21:53:25 +01:00
# Read configuration from /etc/gpg-mailgate.conf
_cfg = RawConfigParser()
_cfg.read('/etc/gpg-mailgate.conf')
cfg = dict()
for sect in _cfg.sections():
cfg[sect] = dict()
for (name, value) in _cfg.items(sect):
cfg[sect][name] = value
def log(msg):
if cfg.has_key('logging') and cfg['logging'].has_key('file'):
if cfg['logging']['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']+"/"
2010-01-20 21:53:25 +01:00
# Read e-mail from stdin
raw = sys.stdin.read()
raw_message = email.message_from_string( raw )
from_addr = raw_message['From']
to_addrs = sys.argv[1:]
2010-01-20 21:53:25 +01:00
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");
2010-01-20 21:53:25 +01:00
def encrypt_payload( payload, gpg_to_cmdline ):
raw_payload = payload.get_payload(decode=True)
2013-09-28 16:28:00 +02:00
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
def encrypt_all_payloads_inline( message, gpg_to_cmdline ):
2013-04-04 20:07:32 +02:00
encrypted_payloads = list()
if type( message.get_payload() ) == str:
return encrypt_payload( message, gpg_to_cmdline ).get_payload()
for payload in message.get_payload():
if( type( payload.get_payload() ) == list ):
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' )
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)
# Workaround it here by prepending a blank line.
# This happens only on text only messages.
submsg2.set_payload("\n" + message.get_payload())
else:
submsg2.set_payload(generate_attachment_pgp(message).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'!
boundary = junk_msg.get_boundary()
# This also modifies the boundary in the body of the message, ie it gets parsed.
if message.has_key('Content-Type'):
message.replace_header('Content-Type', "multipart/encrypted; protocol=\"application/pgp-encrypted\";\nboundary=\"%s\"\n" % boundary)
else:
message['Content-Type'] = "multipart/encrypted; protocol=\"application/pgp-encrypted\";\nboundary=\"%s\"\n" % boundary
return [ submsg1, encrypt_payload(submsg2, gpg_to_cmdline) ]
2013-04-04 20:07:32 +02:00
# This method is not referenced
2010-01-21 15:50:51 +01:00
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 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
s = SMIME.SMIME()
sk = X509.X509_Stack()
normalized_recipient = []
unsmime_to = list(recipients)
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:
(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))
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]
s.set_x509_stack(sk)
s.set_cipher(SMIME.Cipher('aes_192_cbc'))
p7 = s.encrypt( BIO.MemoryBuffer(raw_message.as_string()) )
# Output p7 in mail-friendly format.
out = BIO.MemoryBuffer()
out.write('From: ' + from_addr + '\n')
out.write('To: ' + raw_message['To'] + '\n')
if raw_message['Cc']:
out.write('Cc: ' + raw_message['Cc'] + '\n')
if raw_message['Bcc']:
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':
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)
return None
2010-01-21 15:50:51 +01:00
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()
2010-01-20 21:53:25 +01:00
gpg_to = list()
2010-01-20 22:09:34 +01:00
ungpg_to = list()
2010-01-20 21:53:25 +01:00
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()
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]) )
2010-01-20 22:09:34 +01:00
else:
if verbose:
log("Recipient (%s) not in PGP domain list." % to)
2010-01-20 22:09:34 +01:00
ungpg_to.append(to)
2010-01-20 21:53:25 +01:00
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()
2010-01-20 21:53:25 +01:00
2010-01-20 22:09:34 +01:00
if ungpg_to != list():
to_smime_handler( raw_message, ungpg_to )
2010-01-20 22:09:34 +01:00
log("Encrypting email to: %s" % ' '.join( map(lambda x: x[0], gpg_to) ))
2010-01-21 15:50:51 +01:00
2010-01-20 21:53:25 +01:00
if cfg['default'].has_key('add_header') and cfg['default']['add_header'] == 'yes':
2010-01-21 15:50:51 +01:00
raw_message['X-GPG-Mailgate'] = 'Encrypted by GPG Mailgate'
2010-01-20 21:53:25 +01:00
gpg_to_smtp_mime = list()
gpg_to_cmdline_mime = list()
gpg_to_smtp_inline = list()
gpg_to_cmdline_inline = list()
2010-01-20 22:09:34 +01:00
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(','))
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)
encrypted_payloads = encrypt_all_payloads_attachment_style( 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():
2010-01-20 22:09:34 +01:00
encrypted_payloads = encrypt_all_payloads_inline( raw_message, gpg_to_cmdline_inline )
raw_message.set_payload( encrypted_payloads )
send_msg( raw_message.as_string(), gpg_to_smtp_inline )