[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:
Piotr F. Mieszkowski 2022-07-03 23:58:31 +02:00 committed by Gitea
parent 7849c55d9f
commit 6455c1a280
5 changed files with 142 additions and 11 deletions

View File

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

View File

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

View File

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

View File

@ -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
View 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}>"