Implement Advanced Filter flow for cleartext and OpenPGP

- Polish implementation of mail operations (lacre/mailop.py).  Add two
strategies: InlineOpenPGPEncrypt and MimeOpenPGPEncrypt, to support two modes
of OpenPGP encryption.

- In delivery_plan, only use those strategies that actually make sense with
the recipients we'd got.

- Add flag_enabled predicate (lacre/config.py) to make configuration checks
easier / simpler.

- Handle TypeError errors in Advanced Filter, indicating a delivery failure
when they appear.

- Add type hints to some of the functions.
This commit is contained in:
Piotr F. Mieszkowski 2022-09-29 22:05:45 +02:00 committed by Gitea
parent ce6a0c5466
commit a2eeaeee9d
4 changed files with 46 additions and 28 deletions

View file

@ -81,6 +81,10 @@ def config_item_equals(section, key, value) -> bool:
return section in cfg and key in cfg[section] and cfg[section][key] == value return section in cfg and key in cfg[section] and cfg[section][key] == value
def flag_enabled(section, key) -> bool:
return config_item_equals(section, key, 'yes')
def validate_config(): def validate_config():
"""Check if configuration is complete. """Check if configuration is complete.

View file

@ -5,7 +5,9 @@ import lacre
import lacre.config as conf import lacre.config as conf
import sys import sys
from aiosmtpd.controller import Controller from aiosmtpd.controller import Controller
from aiosmtpd.smtp import Envelope
import asyncio import asyncio
import email
# Mail status constants. # Mail status constants.
# #
@ -26,14 +28,17 @@ import lacre.mailgate as gate
class MailEncryptionProxy: class MailEncryptionProxy:
"""A mail handler dispatching to appropriate mail operation.""" """A mail handler dispatching to appropriate mail operation."""
async def handle_DATA(self, server, session, envelope): async def handle_DATA(self, server, session, envelope: Envelope):
"""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 try:
message = email.message_from_bytes(envelope.content)
for operation in gate.delivery_plan(envelope.rcpt_tos): for operation in gate.delivery_plan(envelope.rcpt_tos):
LOG.debug(f"Sending mail via {operation}") LOG.debug(f"Sending mail via {operation!r}")
new_message = operation.perform(envelope.content) new_message = operation.perform(message)
gate.send_msg(new_message, operation.recipients(), envelope.mail_from) gate.send_msg(new_message, operation.recipients(), envelope.mail_from)
except TypeError as te:
LOG.exception("Got exception while processing", exc_info=te)
return RESULT_ERROR
return RESULT_NOT_IMPLEMENTED return RESULT_NOT_IMPLEMENTED

View file

@ -107,7 +107,7 @@ def _sort_gpg_recipients(gpg_to):
return gpg_to_smtp_mime, gpg_to_cmdline_mime, gpg_to_smtp_inline, gpg_to_cmdline_inline return gpg_to_smtp_mime, gpg_to_cmdline_mime, gpg_to_smtp_inline, gpg_to_cmdline_inline
def _gpg_encrypt_and_return(message, cmdline, to, encrypt_f): def _gpg_encrypt_and_return(message, cmdline, to, encrypt_f) -> str:
msg_copy = copy.deepcopy(message) msg_copy = copy.deepcopy(message)
_customise_headers(msg_copy) _customise_headers(msg_copy)
encrypted_payloads = encrypt_f(msg_copy, cmdline) encrypted_payloads = encrypt_f(msg_copy, cmdline)
@ -202,7 +202,7 @@ def _identify_gpg_recipients(recipients):
gpg_to.append(GpgRecipient(domain_key[0], domain_key[1])) gpg_to.append(GpgRecipient(domain_key[0], domain_key[1]))
continue continue
ungpg_to.append((to, to)) ungpg_to.append(to)
LOG.debug(f'Collected recipients; GPG: {gpg_to}; UnGPG: {ungpg_to}') LOG.debug(f'Collected recipients; GPG: {gpg_to}; UnGPG: {ungpg_to}')
return gpg_to, ungpg_to return gpg_to, ungpg_to
@ -480,7 +480,7 @@ def _get_first_payload(payloads):
return payloads return payloads
def send_msg(message, recipients, fromaddr=None): def send_msg(message: str, recipients, fromaddr=None):
"""Send MESSAGE to RECIPIENTS to the mail relay.""" """Send MESSAGE to RECIPIENTS to the mail relay."""
global from_addr global from_addr
@ -492,7 +492,7 @@ def send_msg(message, recipients, fromaddr=None):
LOG.info(f"Sending email to: {recipients!r}") LOG.info(f"Sending email to: {recipients!r}")
relay = conf.relay_params() relay = conf.relay_params()
smtp = smtplib.SMTP(relay[0], relay[1]) smtp = smtplib.SMTP(relay[0], relay[1])
if conf.config_item_equals('relay', 'starttls', 'yes'): if conf.flag_enabled('relay', 'starttls'):
smtp.starttls() smtp.starttls()
smtp.sendmail(from_addr, recipients, message) smtp.sendmail(from_addr, recipients, message)
else: else:
@ -520,12 +520,18 @@ def delivery_plan(recipients):
keyhome = conf.get_item('gpg', 'keyhome') keyhome = conf.get_item('gpg', 'keyhome')
return [MimeOpenPGPEncrypt(gpg_mime_to, gpg_mime_cmd, keyhome), plan = []
InlineOpenPGPEncrypt(gpg_inline_to, gpg_inline_cmd, keyhome), if gpg_mime_to:
KeepIntact(ungpg_to)] plan.append(MimeOpenPGPEncrypt(gpg_mime_to, gpg_mime_cmd, keyhome))
if gpg_inline_to:
plan.append(InlineOpenPGPEncrypt(gpg_inline_to, gpg_inline_cmd, keyhome))
if ungpg_to:
plan.append(KeepIntact(ungpg_to))
return plan
def deliver_message(raw_message, from_address, to_addrs): def deliver_message(raw_message: email.message.Message, from_address, to_addrs):
"""Send RAW_MESSAGE to all TO_ADDRS using the best encryption method available.""" """Send RAW_MESSAGE to all TO_ADDRS using the best encryption method available."""
global from_addr global from_addr

View file

@ -15,6 +15,7 @@ There are 3 operations available:
import logging import logging
import lacre.mailgate as mailgate import lacre.mailgate as mailgate
from email.message import Message
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -27,7 +28,7 @@ class MailOperation:
"""Initialise the operation with a recipient.""" """Initialise the operation with a recipient."""
self._recipients = recipients self._recipients = recipients
def perform(self, message): def perform(self, message: Message):
"""Perform this operation on MESSAGE. """Perform this operation on MESSAGE.
Return target message. Return target message.
@ -46,10 +47,10 @@ class MailOperation:
class OpenPGPEncrypt(MailOperation): class OpenPGPEncrypt(MailOperation):
"""OpenPGP-encrypt the message.""" """OpenPGP-encrypt the message."""
def __init__(self, recipient, key, keyhome): def __init__(self, recipients, keys, keyhome):
"""Initialise encryption operation.""" """Initialise encryption operation."""
super().__init__(recipient) super().__init__(recipients)
self._key = key self._keys = keys
self._keyhome = keyhome self._keyhome = keyhome
def extend_keys(self, keys): def extend_keys(self, keys):
@ -58,13 +59,17 @@ class OpenPGPEncrypt(MailOperation):
def __repr__(self): def __repr__(self):
"""Generate a representation with just method and key.""" """Generate a representation with just method and key."""
return f"<{type(self).__name__} {self._recipients} {self._key}>" return f"<{type(self).__name__} {self._recipients} {self._keys}>"
class InlineOpenPGPEncrypt(OpenPGPEncrypt): class InlineOpenPGPEncrypt(OpenPGPEncrypt):
"""Inline encryption strategy.""" """Inline encryption strategy."""
def perform(self, msg): def __init__(self, recipients, keys, keyhome):
"""Initialise strategy object."""
super().__init__(recipients, keys, keyhome)
def perform(self, msg: Message):
"""Encrypt with PGP Inline.""" """Encrypt with PGP Inline."""
LOG.debug('Sending PGP/Inline...') LOG.debug('Sending PGP/Inline...')
return mailgate._gpg_encrypt_and_return(msg, return mailgate._gpg_encrypt_and_return(msg,
@ -77,11 +82,9 @@ class MimeOpenPGPEncrypt(OpenPGPEncrypt):
def __init__(self, recipients, keys, keyhome): def __init__(self, recipients, keys, keyhome):
"""Initialise strategy object.""" """Initialise strategy object."""
super().__init__(recipients) super().__init__(recipients, keys, keyhome)
self._keys = keys
self._keyhome = keyhome
def perform(self, msg): def perform(self, msg: Message):
"""Encrypt with PGP MIME.""" """Encrypt with PGP MIME."""
LOG.debug('Sending PGP/MIME...') LOG.debug('Sending PGP/MIME...')
return mailgate._gpg_encrypt_and_return(msg, return mailgate._gpg_encrypt_and_return(msg,
@ -98,7 +101,7 @@ class SMimeEncrypt(MailOperation):
self._email = email self._email = email
self._cert = certificate self._cert = certificate
def perform(self, message): def perform(self, message: Message):
"""Encrypt with a certificate.""" """Encrypt with a certificate."""
LOG.warning(f"Delivering clear-text to {self._recipients}") LOG.warning(f"Delivering clear-text to {self._recipients}")
return message return message
@ -118,9 +121,9 @@ class KeepIntact(MailOperation):
"""Initialise pass-through operation for a given recipient.""" """Initialise pass-through operation for a given recipient."""
super().__init__(recipients) super().__init__(recipients)
def perform(self, message): def perform(self, message: Message):
"""Return MESSAGE unmodified.""" """Return MESSAGE unmodified."""
return message return message.as_string()
def __repr__(self): def __repr__(self):
"""Return representation with just method and email.""" """Return representation with just method and email."""