You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Closes#883 (send/receive email like curl). Adds a new optional mail/ module with asynchronous email clients built on the hloop event loop — replacing the synchronous demo in protocol/smtp.c (kept as-is for compatibility).
Module
mail/mime.{h,c} — MIME assembly + parsing, shared by both clients.
mail/smtp_client.{h,c} — async SMTP client (C core + SmtpClient C++ wrapper). State machine: EHLO → AUTH LOGIN → MAIL FROM → RCPT TO (multi) → DATA → QUIT. Result via callback.
mail/imap_client.{h,c} — async IMAP client (C core + ImapClient C++ wrapper). LOGIN → SELECT → SEARCH → FETCH (per id, literal {n} parsing) → LOGOUT, then MIME-parses each message and delivers via per-mail callback.
TLS: direct TLS (SMTPS 465 / IMAPS 993) via the existing hssl abstraction.
The API/structure mirrors the existing mqtt/ module (C core + C++ wrapper, _new/_run/_stop/_free, hloop state machine, hmutex for thread-safety).
Build
--with-mail / WITH_MAIL (default OFF), wired across configure / Makefile / CMake / Bazel, mirroring the mqtt module. Requires SSL for direct TLS. Linux CI now builds with --with-mail.
Add a new optional `mail/` module providing asynchronous email clients
built on the hloop event loop, addressing issue #883 (send/recv email
like curl). Replaces the synchronous demo in protocol/smtp.c (kept as-is
for compatibility).
- mail/mime.{h,c}: MIME assembly (multipart/mixed + multipart/alternative,
base64 attachments, RFC 2047 encoded-word for non-ASCII headers) and
parsing (multipart split, base64 / quoted-printable decode). Shared by
both clients. mail_t with multi-recipient To/Cc + attachments.
- mail/smtp_client.{h,c}: async SMTP client (C core + SmtpClient C++
wrapper). State machine EHLO -> AUTH LOGIN -> MAIL FROM -> RCPT TO
(multi) -> DATA -> QUIT. Direct TLS (SMTPS 465) via hssl.
- mail/imap_client.{h,c}: async IMAP client (C core + ImapClient C++
wrapper). LOGIN -> SELECT -> SEARCH -> FETCH (per id, with literal {n}
parsing) -> LOGOUT, then MIME-parses each message. Direct TLS (IMAPS 993).
- build wiring: --with-mail / WITH_MAIL (default OFF), mirroring the mqtt
module across configure / Makefile / CMake / Bazel.
- examples/mail/{sendmail,recvmail}_test.cpp + docs/cn/{SmtpClient,ImapClient}.md.
- README: mention the mail client.
Scope: direct TLS only (no STARTTLS); IMAP read-only minimal set (no
STORE/COPY/IDLE); no POP3. Integration is manually verified against real
mailboxes; CI covers compilation.
Co-authored-by: TRAE CLI <traecli@bytedance.com>
MIME (mail/mime.c):
- header injection: encode header values containing CR/LF/control chars via
RFC 2047 (was appended verbatim, allowing Bcc-style injection).
- base64 decode now tolerates MIME line breaks/whitespace (hv_base64_decode
rejects CR/LF), so wrapped attachments no longer decode to 0 bytes.
- decode RFC 2047 encoded-words when parsing headers (Subject etc.), so a
build->parse round-trip returns the original text.
- attachment decode is binary-safe now (track decoded length instead of
strlen, which truncated data at the first embedded NUL).
SMTP (mail/smtp_client.c):
- clamp vsnprintf return to the actual buffer size to avoid reading past
the stack buffer on very long host/address.
- dot-stuff the DATA payload so a body line "." cannot terminate DATA early.
- accept RCPT TO reply code 252 (accepted, cannot verify recipient).
- set TLS SNI hostname (hio_set_hostname) for SMTPS virtual hosts.
IMAP (mail/imap_client.c):
- quote LOGIN credentials and reject control characters (command injection).
- require a complete CRLF-terminated line before treating a tagged response
as final, so responses split across reads are not discarded.
- set TLS SNI hostname for IMAPS virtual hosts.
- use BODY.PEEK[] so fetching does not mark messages as \Seen.
Build:
- examples/CMakeLists.txt: add WITH_MAIL targets (sendmail_test/recvmail_test).
Co-authored-by: TRAE CLI <traecli@bytedance.com>
Thanks for the thorough review — addressed in 0cd21e9. Summary of what changed:
High
SNI hostname (SMTP/IMAP): hio_create_socket only stores the peer address, so hio_set_hostname(io, host) is now called before hio_enable_ssl — name-based virtual SMTPS/IMAPS hosts now get proper SNI.
vsnprintf truncation (SMTP): return value (would-be length) is clamped to the actual buffer size before writing, so a long host/address can't cause an over-read.
Header CR/LF injection (MIME): header values with CR/LF/control chars are now forced through RFC 2047 encoding instead of the ASCII fast path.
IMAP credential injection: LOGIN username/password are quoted (with "/\ escaping) and control characters are rejected up front.
Medium
base64 with line breaks (MIME): hv_base64_decode rejects CR/LF, so a whitespace-stripping wrapper was added — previously any wrapped attachment decoded to 0 bytes. Verified a 5000-byte attachment now round-trips.
RFC 2047 decode on parse: Subject/etc. encoded-words are decoded when parsing, so build→parse round-trips to the original text.
IMAP partial tagged response: a tagged line is only treated as final once a full CRLF-terminated line has arrived, so responses split across reads aren't discarded.
RCPT TO 252: accepted as success.
DATA dot-stuffing: body lines starting with . are dot-stuffed so a lone . line can't terminate DATA early.
CMake example targets: examples/CMakeLists.txt now builds sendmail_test/recvmail_test under WITH_MAIL.
Also made attachment decoding binary-safe (track decoded length instead of strlen, which truncated at embedded NULs), and switched FETCH to BODY.PEEK[] so reading doesn't set \Seen.
Not changed (minor / out of scope for this module): SEARCH id cap >4096, reporting a failed LOGOUT, header continuation unfolding — can follow up if needed.
ok is computed but ignored in this no-literal path. A tagged NO/BAD response to FETCH is consumed as a successful message ID, so the client advances and can eventually report done(0) despite the fetch failing. Report the error and close instead of returning success when ok is false.
This issue also appears on line 298 of the same file.
Unfold continuation lines in RFC 5322 headers
mail/mime.c:410
find_header intentionally returns only one physical line, so folded RFC 5322 headers are not unfolded. A folded Content-Type commonly puts the boundary parameter on the continuation line, causing multipart messages and attachments to be skipped; unfold continuations before parsing header values.
Parse display names and addresses separately
mail/mime.c:634
mail_addr_t.addr is documented as the mailbox address, but this stores the entire decoded From field. For From: Alice <alice@example.com>, callers receive Alice <alice@example.com> as from.addr and no from.name, producing an invalid address if the parsed mail is reused for SMTP. Split the display name and angle-address into the two fields.
Buffer partial SMTP response lines
mail/smtp_client.c:119
This parser only examines the bytes from one read callback. TCP/TLS may split a single 220 or 250 line across callbacks; the partial line is then discarded and the state machine waits forever for a response it already consumed. Keep an accumulated response buffer and parse only complete lines.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Closes #883 (send/receive email like curl). Adds a new optional
mail/module with asynchronous email clients built on the hloop event loop — replacing the synchronous demo inprotocol/smtp.c(kept as-is for compatibility).Module
mail/mime.{h,c}— MIME assembly + parsing, shared by both clients.multipart/mixed+multipart/alternative, base64 attachments, RFC 2047 encoded-word for non-ASCII headers.mail_t.mail_tsupports multi-recipient To/Cc + attachments, withmail_set_*/mail_add_*/mail_clearhelpers.mail/smtp_client.{h,c}— async SMTP client (C core +SmtpClientC++ wrapper). State machine:EHLO → AUTH LOGIN → MAIL FROM → RCPT TO (multi) → DATA → QUIT. Result via callback.mail/imap_client.{h,c}— async IMAP client (C core +ImapClientC++ wrapper).LOGIN → SELECT → SEARCH → FETCH (per id, literal {n} parsing) → LOGOUT, then MIME-parses each message and delivers via per-mail callback.hsslabstraction.The API/structure mirrors the existing
mqtt/module (C core + C++ wrapper,_new/_run/_stop/_free,hloopstate machine,hmutexfor thread-safety).Build
--with-mail/WITH_MAIL(default OFF), wired across configure / Makefile / CMake / Bazel, mirroring the mqtt module. Requires SSL for direct TLS. Linux CI now builds with--with-mail.Examples + docs
examples/mail/sendmail_test.cpp,examples/mail/recvmail_test.cppdocs/cn/SmtpClient.md,docs/cn/ImapClient.mdScope / limitations (by design)
Testing
make libhv(--with-mail --with-openssl) ✅,make sendmail_test recvmail_test✅-DWITH_MAIL=ON✅-Wall -Wextra.