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") 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: class FromStrMixin:
"""Additional operations for configuration enums.""" """Additional operations for configuration enums."""

View file

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

View file

@ -27,7 +27,8 @@ documentation.
import email import email
import email.mime.multipart import email.mime.multipart
from email.message import EmailMessage from email.message import EmailMessage
from email.policy import SMTP from email.policy import SMTP, SMTPUTF8
from email.errors import HeaderParseError
import unittest import unittest
from configparser import RawConfigParser from configparser import RawConfigParser
@ -165,6 +166,26 @@ class EmailParsingTest(unittest.TestCase):
self.assertIsInstance(payload, str) self.assertIsInstance(payload, str)
self.assertTrue(message_boundary in payload) 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): class EmailTest(unittest.TestCase):
def test_boundary_generated_after_as_string_call(self): def test_boundary_generated_after_as_string_call(self):