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:
parent
d75ded751e
commit
04ca103494
3 changed files with 34 additions and 10 deletions
|
@ -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."""
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in a new issue