[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 # https://docs.python.org/3/library/logging.config.html#logging-config-fileformat
config = /etc/gpg-lacre-logging.conf 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] [relay]
# the relay settings to use for Postfix # the relay settings to use for Postfix
# gpg-mailgate will submit email to this relay after it is done processing # 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 # List of mandatory configuration parameters. Each item on this list should be
# a pair: a section name and a parameter name. # a pair: a section name and a parameter name.
MANDATORY_CONFIG_ITEMS = [("relay", "host"), MANDATORY_CONFIG_ITEMS = [("relay", "host"),
("relay", "port")] ("relay", "port"),
("daemon", "host"),
("daemon", "port")]
# Global dict to keep configuration parameters. It's hidden behind several # Global dict to keep configuration parameters. It's hidden behind several
# utility functions to make it easy to replace it with ConfigParser object in # utility functions to make it easy to replace it with ConfigParser object in
@ -99,3 +101,8 @@ def validate_config():
def relay_params(): def relay_params():
"""Return a (HOST, PORT) tuple identifying the mail relay.""" """Return a (HOST, PORT) tuple identifying the mail relay."""
return (cfg["relay"]["host"], int(cfg["relay"]["port"])) 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 logging
import lacre import lacre
import lacre.config as conf import lacre.config as conf
import sys
from aiosmtpd.controller import Controller from aiosmtpd.controller import Controller
# Mail status constants. # Mail status constants.
@ -28,19 +29,30 @@ class MailEncryptionProxy:
"""Accept a message and either encrypt it or forward as-is.""" """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 now, just return an error because we're not ready to handle mail
for r, s in gate.delivery_plan(envelope.rcpt_tos): for recipient, operation in gate.delivery_plan(envelope.rcpt_tos):
print(r) new_message = operation.perform(envelope.body)
gate.send_msg(new_message, [recipient])
return RESULT_NOT_IMPLEMENTED return RESULT_NOT_IMPLEMENTED
def _init_controller(): def _init_controller():
proxy = MailEncryptionProxy() proxy = MailEncryptionProxy()
host, port = conf.relay_params() host, port = conf.daemon_params()
return Controller(proxy, hostname=host, port=port) 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(): def _main():
_validate_config()
controller = _init_controller() controller = _init_controller()
# starts the controller in a new thread # starts the controller in a new thread

View File

@ -34,6 +34,7 @@ from M2Crypto import BIO, SMIME, X509
import logging import logging
import lacre.text as text import lacre.text as text
import lacre.config as conf import lacre.config as conf
from lacre.mailop import KeepIntact
LOG = logging.getLogger(__name__) 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) encrypted_payloads = _encrypt_all_payloads_mime(raw_message_mime, gpg_to_cmdline_mime)
raw_message_mime.set_payload(encrypted_payloads) 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: if gpg_to_smtp_inline:
# Encrypt mail with PGP/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) encrypted_payloads = _encrypt_all_payloads_inline(raw_message_inline, gpg_to_cmdline_inline)
raw_message_inline.set_payload(encrypted_payloads) 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 return ungpg_to
@ -311,7 +312,7 @@ def _smime_encrypt(raw_message, recipients):
LOG.debug(f"Sending message from {from_addr} to {smime_to}") 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: if unsmime_to:
LOG.debug(f"Unable to find valid S/MIME certificates for {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 return payloads
def _send_msg(message, recipients): def send_msg(message, recipients):
"""Send MESSAGE to RECIPIENTS to the mail relay."""
global from_addr global from_addr
recipients = [_f for _f in recipients if _f] recipients = [_f for _f in recipients if _f]
@ -405,7 +407,7 @@ def _is_encrypted(raw_message):
def delivery_plan(recipients): def delivery_plan(recipients):
"""Generate a sequence of pairs: a recipient and their delivery strategy.""" """Generate a sequence of pairs: a recipient and their delivery strategy."""
for recipient in recipients: for recipient in recipients:
yield recipient, None yield recipient, KeepIntact(recipient)
def deliver_message(raw_message, from_address, to_addrs): 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 # There is no need for nested encryption
if _is_encrypted(raw_message): if _is_encrypted(raw_message):
LOG.debug("Message is already encrypted. Encryption aborted.") 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 return
# Encrypt mails for recipients with known public PGP keys # Encrypt mails for recipients with known public PGP keys
@ -434,7 +436,7 @@ def deliver_message(raw_message, from_address, to_addrs):
return return
# Send out mail to recipients which are left # 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): 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}>"