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
6 changes: 6 additions & 0 deletions smtp2mqtt/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## [1.0.5] - 2026-08-26

### Added

- New configuration option `smtp_max_line_length_kb` to raise aiosmtpd's SMTP line-length limit above the RFC 5321 4.5.3.1.6 default, to accommodate devices (e.g. the Swann NVW-MR4K / MaxRanger4K security hub) that send a base64-encoded attachment as one unbroken line with no CRLF wrapping (violates RFC 2045 6.8), which previously caused the add-on to reject the message with `500 Line too long` and never publish it.

## [1.0.4] - 2024-06-24

### Changed
Expand Down
13 changes: 13 additions & 0 deletions smtp2mqtt/DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ mqtt_pass:
mqtt_topic: smtp2mqtt

smtp_auth_required: false
smtp_max_line_length_kb:

smtp_relay_host:
smtp_relay_port: 25
Expand Down Expand Up @@ -122,6 +123,18 @@ If enabled, a simple dummy authentication is performed that always returns succe

Default value: `false`

### Option: `smtp_max_line_length_kb` (optional)

Raises the maximum SMTP line length (in KB) that the add-on will accept during the `DATA` phase, instead of the RFC 5321 4.5.3.1.6 default of ~1000 octets.

Some devices violate RFC 2045 6.8 by sending a base64-encoded attachment (e.g. a JPEG snapshot) as a single, unbroken line with no CRLF wrapping. Since aiosmtpd's default line length limit is only ~1000 bytes, it will reject any such line with `500 Line too long` and abort the connection - which means the add-on never receives (and can never publish) that message at all. This has been observed with the Swann NVW-MR4K / MaxRanger4K security hub's motion-alert emails, which always include such a snapshot.

If you're seeing `500 Line too long (see RFC5321 4.5.3.1.6)` in the log (with `debug: true`) right before the sending device gives up, set this to a value comfortably larger than the base64-encoded size of the largest attachment you expect (base64 inflates size by ~4/3), e.g. `8192` for an ~6MB snapshot.

Leave this unset to keep the standard, RFC-compliant line length limit - this only exists to accommodate non-conformant senders that can't be fixed on their end.

Default value: *(unset - standard aiosmtpd line length limit applies)*

### Option: `smtp_relay_host` (optional)

The host name or IP address of an SMTP server to relay the message through, in order to send it on its way.
Expand Down
5 changes: 2 additions & 3 deletions smtp2mqtt/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
ARG BUILD_FROM
FROM $BUILD_FROM
FROM ghcr.io/home-assistant/base:3.24

WORKDIR /app

COPY start.sh smtp2mqtt.py /app/

RUN chmod 755 /app/start.sh

RUN apk add --no-cache python3 py3-pip && pip3 install paho-mqtt aiosmtpd
RUN apk add --no-cache python3 py3-pip && pip3 install --break-system-packages paho-mqtt aiosmtpd

ENTRYPOINT ["/app/start.sh"]

Expand Down
4 changes: 2 additions & 2 deletions smtp2mqtt/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@
name: "SMTP to MQTT"
description: "A simple, tiny, absolutely no-frills Python-based SMTP server that publishes mail to an MQTT topic then (optionally) passes the mail on to the next relay"
url: "https://github.com/bcastellucci/addons"
version: "1.0.4"
version: "1.0.5"
slug: "smtp2mqtt"
arch:
- aarch64
- amd64
- armhf
- armv7
- i386
image: "bcastellucci/image-{arch}-smtp2mqtt"
startup: application
boot: auto
map:
Expand All @@ -24,6 +23,7 @@ schema:
mqtt_pass: password?
mqtt_topic: str?
smtp_auth_required: bool?
smtp_max_line_length_kb: int(1,65536)?
smtp_relay_host: str?
smtp_relay_port: port?
smtp_relay_user: str?
Expand Down
46 changes: 45 additions & 1 deletion smtp2mqtt/smtp2mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from email.policy import default
from aiosmtpd.controller import Controller
from aiosmtpd.smtp import AuthResult
from aiosmtpd.smtp import SMTP as AiosmtpdSMTP
from paho.mqtt import publish

##
Expand All @@ -34,6 +35,7 @@
"MQTT_TOPIC": "smtp2mqtt",
"PUBLISH_ATTACHMENTS": "False",
"SAVE_ATTACHMENTS_DIR": None,
"SMTP_MAX_LINE_LENGTH_KB": None,
"DEBUG": "False",
}
config = {
Expand Down Expand Up @@ -193,13 +195,54 @@ def dummy_auth_function(server, session, envelope, mechanism, auth_data):
return AuthResult(success=True)


def make_relaxed_line_length_controller(line_length_bytes):
"""
Some devices (e.g. the Swann NVW-MR4K / MaxRanger4K security hub) attach a
base64-encoded JPEG snapshot as a single, unbroken line with no CRLF
wrapping, which violates RFC 2045 6.8. aiosmtpd enforces the RFC 5321
4.5.3.1.6 ~1000-octet SMTP line limit via SMTP.line_length_limit (which
also sizes the underlying asyncio.StreamReader buffer) and aborts the
DATA phase with "500 Line too long" as soon as it's hit, so such devices
can never successfully deliver a message with an attachment.

SMTP_MAX_LINE_LENGTH_KB lets an operator raise that limit for such
non-conformant senders. Returns a Controller subclass whose factory()
builds an SMTP protocol instance with the raised limit instead of
aiosmtpd's default of 1001 bytes.
"""
class RelaxedLineLengthSMTP(AiosmtpdSMTP):
pass
RelaxedLineLengthSMTP.line_length_limit = line_length_bytes

class RelaxedLineLengthController(Controller):
def factory(self):
return RelaxedLineLengthSMTP(self.handler, **self.SMTP_kwargs)

return RelaxedLineLengthController


if __name__ == "__main__":
if (log.isEnabledFor(logging.DEBUG)):
log.debug(", ".join([f"{k}={v}" for k, v in config.items()]), extra={'uuid': 'main thread'})

loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
c = Controller(

controller_kwargs = {}
if config["SMTP_MAX_LINE_LENGTH_KB"]:
line_length_bytes = int(config["SMTP_MAX_LINE_LENGTH_KB"]) * 1024
controller_class = make_relaxed_line_length_controller(line_length_bytes)
# a single oversized line can be nearly the whole message, so make
# sure the overall DATA size cap isn't smaller than the line itself
controller_kwargs["data_size_limit"] = max(2 ** 25, line_length_bytes * 2)
log.info(
"Relaxing SMTP line length limit to %d bytes (SMTP_MAX_LINE_LENGTH_KB=%s)",
line_length_bytes, config["SMTP_MAX_LINE_LENGTH_KB"], extra={'uuid': 'main thread'},
)
else:
controller_class = Controller

c = controller_class(
handler=SMTP2MQTTHandler(loop),
loop=loop,
hostname="0.0.0.0",
Expand All @@ -208,6 +251,7 @@ def dummy_auth_function(server, session, envelope, mechanism, auth_data):
auth_required=config["SMTP_AUTH_REQUIRED"],
#quite down the warnings when auth-required is false by letting this be true (we won't use it anyway, doesn't hurt to be true)...
auth_require_tls=(not config["SMTP_AUTH_REQUIRED"]),
**controller_kwargs,
)
c.start()
log.info("Running", extra={'uuid': 'main thread'})
Expand Down
2 changes: 1 addition & 1 deletion smtp2mqtt/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export MQTT_HOST=$MQTT_HOST

#grab the rest in a loop
for var in \
smtp_auth_required \
smtp_auth_required smtp_max_line_length_kb \
smtp_relay_host smtp_relay_port smtp_relay_user smtp_relay_pass smtp_relay_starttls smtp_relay_timeout_secs \
mqtt_port mqtt_user mqtt_pass mqtt_topic \
publish_attachments \
Expand Down