Summary
We are using aiosmtpd as an SMTP receiver. In some cases, a client connection drops while the SMTP DATA command is still in progress. The message is not completed with <CRLF>.<CRLF>, so handle_DATA() is never called and envelope.content / envelope.original_content are never made available.
That behavior makes sense from an SMTP protocol perspective: the message was not accepted and the server cannot return 250 OK.
However, we need a supported way to detect and process these aborted partial payloads for quarantine / diagnostics / remediation. We do not want to treat them as successfully accepted messages.
Current behavior
Sequence:
EHLO ...
MAIL FROM:<sender@example.test>
RCPT TO:<recipient@example.test>
DATA
354 End data with <CR><LF>.<CR><LF>
<client sends some message bytes>
<client disconnects before <CRLF>.<CRLF>>
Result:
handle_DATA() is not called.
- No completed
envelope.content / envelope.original_content is available.
- The only way we found to capture bytes is to subclass
SMTP and wrap private/internal stream reader behavior.
Expected / requested behavior
Is there any supported way to access the partial DATA bytes when the client disconnects before the message terminator?
If not, would the project consider adding an opt-in hook for this case, for example:
async def handle_DATA_aborted(
self,
server,
session,
envelope,
partial_content: bytes,
error: BaseException | None,
) -> None:
...
or a similar streaming / chunk-based hook.
Important constraints:
- This should not call normal
handle_DATA().
- This should clearly mark the message as incomplete / not accepted.
- It should not attempt to return an SMTP status if the client is already gone.
- It should be opt-in.
- It should probably respect size limits or expose truncation metadata to avoid memory pressure.
- It would be useful to include
MAIL FROM, RCPT TO, peer/session data, partial byte count, and whether the SMTP terminator was seen.
Why this is needed
Some upstream clients or network paths occasionally drop during DATA. From the receiver side, we need to detect these incomplete messages and move them into a separate handling path rather than losing all visibility.
The goal is not to accept corrupted emails as valid. The goal is to have a supported server-side hook for partial/incomplete payloads so they can be logged, quarantined, or correlated with upstream retries.
Workaround tried
We tried a custom SMTP subclass that temporarily wraps self._reader during smtp_DATA() and writes received bytes to a temporary file. On CancelledError, ConnectionResetError, or EOF-like cases, it persists the partial payload and calls our own internal process_partial_email(...).
This works only as a brittle diagnostic workaround because it relies on private/internal implementation details such as self._reader and the reader buffer.
Minimal reproducer
Server:
import time
from aiosmtpd.controller import Controller
class Handler:
async def handle_DATA(self, server, session, envelope):
print("handle_DATA called")
print("bytes:", len(envelope.original_content or b""))
return "250 OK"
controller = Controller(Handler(), hostname="127.0.0.1", port=8025)
controller.start()
try:
while True:
time.sleep(1)
finally:
controller.stop()
Client that disconnects before the DATA terminator:
import socket
def recv_reply(sock):
print(sock.recv(4096).decode(errors="replace"))
sock = socket.create_connection(("127.0.0.1", 8025))
recv_reply(sock)
sock.sendall(b"EHLO example.test\r\n")
recv_reply(sock)
sock.sendall(b"MAIL FROM:<sender@example.test>\r\n")
recv_reply(sock)
sock.sendall(b"RCPT TO:<recipient@example.test>\r\n")
recv_reply(sock)
sock.sendall(b"DATA\r\n")
recv_reply(sock)
sock.sendall(
b"From: sender@example.test\r\n"
b"To: recipient@example.test\r\n"
b"Subject: partial message\r\n"
b"\r\n"
b"This is an incomplete body.\r\n"
)
# Close without sending b"\r\n.\r\n"
sock.close()
Observed result: handle_DATA() is not called.
Requested result: an optional hook or supported extension point for the aborted partial DATA case.
Related issues
This seems related to previous connection reset handling issues such as #127 and #162, but those appear to focus on exception/logging behavior after disconnects, not on exposing partial DATA to application code.
Summary
We are using
aiosmtpdas an SMTP receiver. In some cases, a client connection drops while the SMTPDATAcommand is still in progress. The message is not completed with<CRLF>.<CRLF>, sohandle_DATA()is never called andenvelope.content/envelope.original_contentare never made available.That behavior makes sense from an SMTP protocol perspective: the message was not accepted and the server cannot return
250 OK.However, we need a supported way to detect and process these aborted partial payloads for quarantine / diagnostics / remediation. We do not want to treat them as successfully accepted messages.
Current behavior
Sequence:
Result:
handle_DATA()is not called.envelope.content/envelope.original_contentis available.SMTPand wrap private/internal stream reader behavior.Expected / requested behavior
Is there any supported way to access the partial
DATAbytes when the client disconnects before the message terminator?If not, would the project consider adding an opt-in hook for this case, for example:
or a similar streaming / chunk-based hook.
Important constraints:
handle_DATA().MAIL FROM,RCPT TO, peer/session data, partial byte count, and whether the SMTP terminator was seen.Why this is needed
Some upstream clients or network paths occasionally drop during
DATA. From the receiver side, we need to detect these incomplete messages and move them into a separate handling path rather than losing all visibility.The goal is not to accept corrupted emails as valid. The goal is to have a supported server-side hook for partial/incomplete payloads so they can be logged, quarantined, or correlated with upstream retries.
Workaround tried
We tried a custom
SMTPsubclass that temporarily wrapsself._readerduringsmtp_DATA()and writes received bytes to a temporary file. OnCancelledError,ConnectionResetError, or EOF-like cases, it persists the partial payload and calls our own internalprocess_partial_email(...).This works only as a brittle diagnostic workaround because it relies on private/internal implementation details such as
self._readerand the reader buffer.Minimal reproducer
Server:
Client that disconnects before the DATA terminator:
Observed result:
handle_DATA()is not called.Requested result: an optional hook or supported extension point for the aborted partial DATA case.
Related issues
This seems related to previous connection reset handling issues such as #127 and #162, but those appear to focus on exception/logging behavior after disconnects, not on exposing partial
DATAto application code.