diff --git a/gpg-mailgate.conf.sample b/gpg-mailgate.conf.sample index 05f837b..154597f 100644 --- a/gpg-mailgate.conf.sample +++ b/gpg-mailgate.conf.sample @@ -75,6 +75,14 @@ mail_templates = /var/gpgmailgate/cron_templates # https://docs.python.org/3/library/logging.config.html#logging-config-fileformat config = /etc/gpg-lacre-logging.conf +[daemon] +# Advanced Content Filter section. +# +# Advanced filters differ from Simple ones by providing a daemon that handles +# requests, instead of starting a new process each time a message arrives. +host = 127.0.0.1 +port = 10025 + [relay] # the relay settings to use for Postfix # gpg-mailgate will submit email to this relay after it is done processing diff --git a/lacre/config.py b/lacre/config.py index 0933206..2178608 100644 --- a/lacre/config.py +++ b/lacre/config.py @@ -16,7 +16,9 @@ CONFIG_PATH_ENV = "GPG_MAILGATE_CONFIG" # List of mandatory configuration parameters. Each item on this list should be # a pair: a section name and a parameter name. MANDATORY_CONFIG_ITEMS = [("relay", "host"), - ("relay", "port")] + ("relay", "port"), + ("daemon", "host"), + ("daemon", "port")] # Global dict to keep configuration parameters. It's hidden behind several # utility functions to make it easy to replace it with ConfigParser object in @@ -99,3 +101,8 @@ def validate_config(): def relay_params(): """Return a (HOST, PORT) tuple identifying the mail relay.""" return (cfg["relay"]["host"], int(cfg["relay"]["port"])) + + +def daemon_params(): + """Return a (HOST, PORT) tuple to setup a server socket for Lacre daemon.""" + return (cfg["daemon"]["host"], int(cfg["daemon"]["port"])) diff --git a/lacre/daemon.py b/lacre/daemon.py index c87e058..2b0f3ee 100644 --- a/lacre/daemon.py +++ b/lacre/daemon.py @@ -3,6 +3,7 @@ import logging import lacre import lacre.config as conf +import sys from aiosmtpd.controller import Controller # Mail status constants. @@ -28,19 +29,30 @@ class MailEncryptionProxy: """Accept a message and either encrypt it or forward as-is.""" # for now, just return an error because we're not ready to handle mail - for r, s in gate.delivery_plan(envelope.rcpt_tos): - print(r) + for recipient, operation in gate.delivery_plan(envelope.rcpt_tos): + new_message = operation.perform(envelope.body) + gate.send_msg(new_message, [recipient]) return RESULT_NOT_IMPLEMENTED def _init_controller(): proxy = MailEncryptionProxy() - host, port = conf.relay_params() + host, port = conf.daemon_params() return Controller(proxy, hostname=host, port=port) +def _validate_config(): + missing = conf.validate_config() + if missing: + params = ", ".join([f"[{tup[0]}]{tup[1]}" for tup in missing]) + LOG.error(f"Following mandatory parameters are missing: {params}") + sys.exit(lacre.EX_CONFIG) + + def _main(): + _validate_config() + controller = _init_controller() # starts the controller in a new thread diff --git a/lacre/mailgate.py b/lacre/mailgate.py index 3869365..f15e748 100644 --- a/lacre/mailgate.py +++ b/lacre/mailgate.py @@ -34,6 +34,7 @@ from M2Crypto import BIO, SMIME, X509 import logging import lacre.text as text import lacre.config as conf +from lacre.mailop import KeepIntact LOG = logging.getLogger(__name__) @@ -145,7 +146,7 @@ def _gpg_encrypt(raw_message, recipients): 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) + send_msg(raw_message_mime.as_string(), gpg_to_smtp_mime) if gpg_to_smtp_inline: # Encrypt mail with PGP/INLINE @@ -162,7 +163,7 @@ def _gpg_encrypt(raw_message, recipients): 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) + send_msg(raw_message_inline.as_string(), gpg_to_smtp_inline) return ungpg_to @@ -311,7 +312,7 @@ def _smime_encrypt(raw_message, recipients): LOG.debug(f"Sending message from {from_addr} to {smime_to}") - _send_msg(out.read(), smime_to) + send_msg(out.read(), smime_to) if unsmime_to: LOG.debug(f"Unable to find valid S/MIME certificates for {unsmime_to}") @@ -375,7 +376,8 @@ def _get_first_payload(payloads): return payloads -def _send_msg(message, recipients): +def send_msg(message, recipients): + """Send MESSAGE to RECIPIENTS to the mail relay.""" global from_addr recipients = [_f for _f in recipients if _f] @@ -405,7 +407,7 @@ def _is_encrypted(raw_message): def delivery_plan(recipients): """Generate a sequence of pairs: a recipient and their delivery strategy.""" for recipient in recipients: - yield recipient, None + yield recipient, KeepIntact(recipient) def deliver_message(raw_message, from_address, to_addrs): @@ -420,7 +422,7 @@ def deliver_message(raw_message, from_address, to_addrs): # There is no need for nested encryption if _is_encrypted(raw_message): LOG.debug("Message is already encrypted. Encryption aborted.") - _send_msg(raw_message.as_string(), recipients_left) + send_msg(raw_message.as_string(), recipients_left) return # Encrypt mails for recipients with known public PGP keys @@ -434,7 +436,7 @@ def deliver_message(raw_message, from_address, to_addrs): return # Send out mail to recipients which are left - _send_msg(raw_message.as_string(), recipients_left) + send_msg(raw_message.as_string(), recipients_left) def exec_time_info(start_timestamp): diff --git a/lacre/mailop.py b/lacre/mailop.py new file mode 100644 index 0000000..b7b08bb --- /dev/null +++ b/lacre/mailop.py @@ -0,0 +1,102 @@ +"""Mail operations for a given recipient. + +There are 3 operations available: + +- OpenPGPEncrypt: to deliver the message to a recipient with an OpenPGP public + key available. + +- SMimeEncrypt: to deliver the message to a recipient with an S/MIME + certificate. + +- KeepIntact: a no-operation (implementation of the Null Object pattern), used + for messages already encrypted or those who haven't provided their keys or + certificates. +""" + +import logging +import GnuPG + + +LOG = logging.getLogger(__name__) + + +class MailOperation: + """Contract for an operation to be performed on a message.""" + + def __init__(self, recipient): + """Initialise the operation with a recipient.""" + self._recipient = recipient + + def perform(self, message): + """Perform this operation on MESSAGE. + + Return target message. + """ + raise NotImplementedError(self.__class__()) + + def recipient(self): + """Return recipient of the message.""" + return self._recipient + + +class OpenPGPEncrypt(MailOperation): + """OpenPGP-encrypt the message.""" + + def __init__(self, recipient, key, keyhome): + """Initialise encryption operation.""" + super().__init__(recipient) + self._key = key + self._keyhome = keyhome + + def perform(self, message): + """Encrypt MESSAGE with the given key.""" + enc = GnuPG.GPGEncryptor(self._keyhome) + enc.update(message) + encrypted, err = enc.encrypt() + if err: + LOG.error(f"Unable to encrypt message, delivering cleartext (gpg exit code: {err})") + return message + else: + return encrypted + + def __repr__(self): + """Generate a representation with just method and key.""" + return f"" + + +class SMimeEncrypt(MailOperation): + """S/MIME encryption operation.""" + + def __init__(self, recipient, email, certificate): + """Initialise S/MIME encryption for a given EMAIL and CERTIFICATE.""" + super().__init__(recipient) + self._email = email + self._cert = certificate + + def perform(self, message): + """Encrypt with a certificate.""" + LOG.warning(f"Delivering clear-text to {self._recipient}") + return message + + def __repr__(self): + """Generate a representation with just method and key.""" + return f"" + + +class KeepIntact(MailOperation): + """A do-nothing operation (Null Object implementation). + + This operation should be used for mail that's already encrypted. + """ + + def __init__(self, recipient): + """Initialise pass-through operation for a given recipient.""" + super().__init__(recipient) + + def perform(self, message): + """Return MESSAGE unmodified.""" + return message + + def __repr__(self): + """Return representation with just method and email.""" + return f""