Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ Contributors

* Ryan Young <ryan@youngryan.com>
* Emily Young <em.therese.young@gmail.com>
* Matthew Foran <matthewjforan@gmail.com>
3 changes: 3 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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``.

Expand Down
5 changes: 5 additions & 0 deletions src/mailrise/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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]
Expand Down Expand Up @@ -116,6 +118,8 @@ def load_config(logger: Logger, file: io.TextIOWrapper) -> MailriseConfig:

yml_listen = yml.get('listen', {})

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.
yml_tls_mode = (yml_tls.get('mode', False) or "off").upper()
Expand Down Expand Up @@ -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,
Expand Down
37 changes: 8 additions & 29 deletions src/mailrise/smtp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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(''))

Expand Down