[daemon] Add configuration, implement no-op filter
- Add a "mailop" module to define mail operations. Each should inherit from MailOperation class (which just defines the contract). - Make lacre.mailgate.delivery_plan always return KeepIntact strategy to have a daemon that just forwards messages without modifying them. - Add sample configuration. - Include daemon configuration in mandatory parameter check.
This commit is contained in:
parent
7849c55d9f
commit
6455c1a280
5 changed files with 142 additions and 11 deletions
|
@ -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
|
||||
|
|
|
@ -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"]))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
102
lacre/mailop.py
Normal file
102
lacre/mailop.py
Normal file
|
@ -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"<OpenPGP {self._recipient} {self._key}>"
|
||||
|
||||
|
||||
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"<S/MIME {self._recipient}, {self._cert}>"
|
||||
|
||||
|
||||
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"<KeepIntact {self._recipient}>"
|
Loading…
Reference in a new issue