From 1e95e45458f32f1ec792763e4d6290d6bffb4c94 Mon Sep 17 00:00:00 2001 From: Matthew Foran Date: Sun, 15 Dec 2024 11:50:07 -0500 Subject: [PATCH 1/3] Remove manual multipart parsing, add `prefer_plain` parameter --- README.rst | 3 +++ src/mailrise/config.py | 5 +++++ src/mailrise/smtp.py | 37 ++++++++----------------------------- 3 files changed, 16 insertions(+), 29 deletions(-) diff --git a/README.rst b/README.rst index 848cb3a..956ba5d 100644 --- a/README.rst +++ b/README.rst @@ -210,6 +210,9 @@ listen.host string Specifies the network address listen.port number Specifies the network port to listen on. Defaults to 8025. +prefer_plain bool Prefer the plain text email type over html (when available). + + Defaults to True. tls.mode string Selects the operating mode for TLS encryption. Must be ``off``, ``onconnect``, ``starttls``, or ``starttlsrequire``. diff --git a/src/mailrise/config.py b/src/mailrise/config.py index d34e270..34da7ee 100644 --- a/src/mailrise/config.py +++ b/src/mailrise/config.py @@ -66,6 +66,7 @@ class MailriseConfig(NamedTuple): logger: The logger, which is used to record interesting events. listen_host: The network address to listen on. listen_port: The network port to listen on. + prefer_plain: prefer text/plain over text/html email. tls_mode: The TLS encryption mode. tls_certfile: The path to the TLS certificate chain file. tls_keyfile: The path to the TLS key file. @@ -77,6 +78,7 @@ class MailriseConfig(NamedTuple): logger: Logger listen_host: str listen_port: int + prefer_plain: bool tls_mode: TLSMode tls_certfile: typ.Optional[str] tls_keyfile: typ.Optional[str] @@ -116,6 +118,8 @@ def load_config(logger: Logger, file: io.TextIOWrapper) -> MailriseConfig: yml_listen = yml.get('listen', {}) + prefer_plain = yml.get('prefer_plain', False) + yml_tls = yml.get('tls', {}) # "off" is a boolean value in YAML, so it will get parsed as False. yml_tls_mode = (yml_tls.get('mode', False) or "off").upper() @@ -153,6 +157,7 @@ def load_config(logger: Logger, file: io.TextIOWrapper) -> MailriseConfig: logger=logger, listen_host=yml_listen.get('host', ''), listen_port=yml_listen.get('port', 8025), + prefer_plain=prefer_plain, tls_mode=tls_mode, tls_certfile=tls_certfile, tls_keyfile=tls_keyfile, diff --git a/src/mailrise/smtp.py b/src/mailrise/smtp.py index 6e5b268..7115ccd 100644 --- a/src/mailrise/smtp.py +++ b/src/mailrise/smtp.py @@ -76,7 +76,7 @@ async def handle_DATA(self, server: SMTP, session: Session, envelope: Envelope) message = parser.parsebytes(envelope.content) assert isinstance(message, StdlibEmailMessage) try: - notification = _parsemessage(message, envelope) + notification = _parsemessage(message, envelope, self.config.prefer_plain) except UnreadableMultipart as mpe: subparts = \ ' '.join(part.get_content_type() for part in mpe.message.iter_parts()) @@ -104,7 +104,7 @@ async def handle_DATA(self, server: SMTP, session: Session, envelope: Envelope) return '250 OK' -def _parsemessage(msg: StdlibEmailMessage, envelope: Envelope) -> r.EmailMessage: +def _parsemessage(msg: StdlibEmailMessage, envelope: Envelope, prefer_plain: bool) -> r.EmailMessage: """Parses an email message into an `EmailNotification`. Args: @@ -113,18 +113,14 @@ def _parsemessage(msg: StdlibEmailMessage, envelope: Envelope) -> r.EmailMessage Returns: The `EmailNotification` instance. """ - py_body_part = msg.get_body() + if prefer_plain: + py_body_part = msg.get_body(preferencelist=('plain', 'html')) + else: + py_body_part = msg.get_body(preferencelist=('html', 'plain')) body: typ.Optional[tuple[str, apprise.NotifyFormat]] if isinstance(py_body_part, StdlibEmailMessage): - body_part: StdlibEmailMessage - try: - py_body_part.get_content() - except KeyError: # stdlib failed to read the content, which means multipart - body_part = _getmultiparttext(py_body_part) - else: - body_part = py_body_part - body_content = contentmanager.raw_data_manager.get_content(body_part) - is_html = body_part.get_content_subtype() == 'html' + body_content = contentmanager.raw_data_manager.get_content(py_body_part) + is_html = py_body_part.get_content_subtype() == 'html' body = (body_content.strip(), apprise.NotifyFormat.HTML if is_html else apprise.NotifyFormat.TEXT) else: @@ -143,23 +139,6 @@ def _parsemessage(msg: StdlibEmailMessage, envelope: Envelope) -> r.EmailMessage ) -def _getmultiparttext(msg: StdlibEmailMessage) -> StdlibEmailMessage: - """Search for the textual body part of a multipart email.""" - content_type = msg.get_content_type() - if content_type in ('multipart/related', 'multipart/alternative'): - parts = list(msg.iter_parts()) - # Look for these types of parts in descending order. - for parttype in ('multipart/alternative', 'multipart/related', - 'text/html', 'text/plain'): - found = \ - next((p for p in parts if isinstance(p, StdlibEmailMessage) - and p.get_content_type() == parttype), None) - if found is not None: - return _getmultiparttext(found) - raise UnreadableMultipart(msg) - return msg - - def _parseattachment(part: StdlibEmailMessage) -> r.EmailAttachment: return r.EmailAttachment(data=part.get_content(), filename=part.get_filename('')) From aaeebc65622785d0e791167c45f6e85d14200a7d Mon Sep 17 00:00:00 2001 From: Matthew Foran Date: Sun, 15 Dec 2024 12:04:06 -0500 Subject: [PATCH 2/3] update authors --- AUTHORS.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index db541a9..396d3a7 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -4,3 +4,4 @@ Contributors * Ryan Young * Emily Young +* Matthew Foran \ No newline at end of file From 7240089cb73f2e2f4aa083e3ef667d6b4bdf1478 Mon Sep 17 00:00:00 2001 From: Matthew Foran Date: Sun, 15 Dec 2024 12:06:03 -0500 Subject: [PATCH 3/3] fix default value --- src/mailrise/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mailrise/config.py b/src/mailrise/config.py index 34da7ee..5fedbc9 100644 --- a/src/mailrise/config.py +++ b/src/mailrise/config.py @@ -118,7 +118,7 @@ def load_config(logger: Logger, file: io.TextIOWrapper) -> MailriseConfig: yml_listen = yml.get('listen', {}) - prefer_plain = yml.get('prefer_plain', False) + prefer_plain = yml.get('prefer_plain', True) yml_tls = yml.get('tls', {}) # "off" is a boolean value in YAML, so it will get parsed as False.