aio-libs / aio-libs/aiosmtpd

Add a supported hook for aborted/partial SMTP DATA when the client disconnects

オープン
#614 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る
主要言語
Python
スター
373
フォーク
105
平均マージ
4分
マージ済み PR(30日)
2

説明

### 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 `.`, 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:

```text
EHLO ...
MAIL FROM:
RCPT TO:
DATA
354 End data with .

.>
```

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:

```python
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:

```python
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:

```python
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:\r\n")
recv_reply(sock)

sock.sendall(b"RCPT TO:\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.

コントリビューションガイド

コントリビューションガイドを開く

評価

この issue はまだ評価されていません。

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。