Fix unencrypted delivery in case of message generation failure

When we fail to produce byte representation of the email message being
processed, we may end up bouncing a message.  An example of such case would be
a message with a Message-Id header that Python's email parser library cannot
process.

In such cases, just take whatever original content we have received and pass
it to the destination without touching it to minimise any chances of breaking
the overall flow.
This commit is contained in:
Piotr F. Mieszkowski 2024-03-01 20:13:16 +01:00
parent d75ded751e
commit 04ca103494
Signed by: pfm
GPG key ID: BDE5BC1FA5DC53D5
3 changed files with 34 additions and 10 deletions

View file

@ -143,6 +143,11 @@ def strict_mode():
return ("default" in cfg and cfg["default"]["enc_keymap_only"] == "yes")
def should_log_headers() -> bool:
"""Check if Lacre should log message headers."""
return flag_enabled('daemon', 'log_headers')
class FromStrMixin:
"""Additional operations for configuration enums."""

View file

@ -42,9 +42,6 @@ class MailEncryptionProxy:
if message.defects:
LOG.warning("Issues found: %d; %s", len(message.defects), repr(message.defects))
if conf.flag_enabled('daemon', 'log_headers'):
LOG.info('Message headers: %s', self._extract_headers(message))
send = xport.SendFrom(envelope.mail_from)
for operation in gate.delivery_plan(envelope.rcpt_tos, message, keys):
LOG.debug(f"Sending mail via {operation!r}")
@ -55,21 +52,22 @@ class MailEncryptionProxy:
# If the message can't be encrypted, deliver cleartext.
LOG.error('Unable to encrypt message, delivering in cleartext: %s', e)
if not isinstance(operation, KeepIntact):
self._send_unencrypted(operation, message, envelope, send)
self._send_unencrypted(operation, envelope, send)
else:
LOG.exception('Cannot perform: %s', operation)
raise
except:
LOG.exception('Unexpected exception caught, bouncing message')
return xport.RESULT_ERROR
if conf.should_log_headers():
LOG.info('Message headers: %s', self._extract_headers(message))
return xport.RESULT_ERRORR
return xport.RESULT_OK
def _send_unencrypted(self, operation, message, envelope, send: xport.SendFrom):
keep = KeepIntact(operation.recipients())
new_message = keep.perform(message)
send(new_message, operation.recipients())
def _send_unencrypted(self, operation, envelope, send: xport.SendFrom):
# Do not parse and re-generate the message, just send it as it is.
send(envelope.original_content, operation.recipients())
def _beginning(self, e: Envelope) -> bytes:
double_eol_pos = e.original_content.find(DOUBLE_EOL_BYTES)

View file

@ -27,7 +27,8 @@ documentation.
import email
import email.mime.multipart
from email.message import EmailMessage
from email.policy import SMTP
from email.policy import SMTP, SMTPUTF8
from email.errors import HeaderParseError
import unittest
from configparser import RawConfigParser
@ -165,6 +166,26 @@ class EmailParsingTest(unittest.TestCase):
self.assertIsInstance(payload, str)
self.assertTrue(message_boundary in payload)
def test_fail_if_message_id_parsing_is_fixed(self):
# Unfortunately, Microsoft sends messages with Message-Id header values
# that email parser can't process.
#
# Bug: https://github.com/python/cpython/issues/105802
# Fix: https://github.com/python/cpython/pull/108133
rawmsg = b"From: alice@lacre.io\r\n" \
+ b"To: bob@lacre.io\r\n" \
+ b"Subject: Test message\r\n" \
+ b"Content-Type: text/plain\r\n" \
+ b"Content-Transfer-Encoding: base64\r\n" \
+ b"Message-Id: <[yada-yada-yada@microsoft.com]>\r\n" \
+ b"\r\n" \
+ b"SGVsbG8sIFdvcmxkIQo=\r\n"
msg = email.message_from_bytes(rawmsg, policy=SMTPUTF8)
self.assertEqual(len(msg.defects), 0)
self.assertRaises(IndexError, lambda: msg['Message-Id'])
class EmailTest(unittest.TestCase):
def test_boundary_generated_after_as_string_call(self):