fix(smtp): tolerate bare reply code without text (RFC 5321 section 4.2) - #285
Conversation
`SmtpResponseLine.parse` called `text.substring(4)` unconditionally, so a reply line consisting of the 3-digit code only (`250<CRLF>`) threw a `RangeError`. Because `SmtpResponse` was constructed outside the try/catch in `SmtpClient.onServerResponse`, the error escaped from the socket callback and the pending command's completer never completed. * parse `Reply-code [ SP textstring ]` per RFC 5321: empty message for a bare code, no throw for lines shorter than a code * construct `SmtpResponse` inside the guard so any parse error fails the pending command instead of hanging it
|
The The workflow installs the latest stable Dart SDK, which as of this run is 3.13.4 (released 2026-09-15). Its import 'package:test/test.dart';
// cSpell:disable <-- 3.13 inserts a blank line hereReproduction on pristine With this PR: The other steps are fine: |
|
Thanks a lot, much appreciated! |
Problem
SmtpResponseLine.parseatsmtp_response.dart:77callstext.substring(4)unconditionally once the 3-digit code parses, so a reply line consisting of the code only (334<CRLF>) throws:This happens on every XOAUTH2 login against smtp.yandex.ru:
SmtpAuthXOauth2CommandsendsAUTH XOAUTH2without an initial response, and Yandex writes the empty SASL challenge as a bare334— no trailing space (RFC 4954 §4 asks for334, but the RFC 5321 §4.2 reply grammarReply-code [ SP textstring ] CRLFallows the code-only form, and a client must not crash on it either way). Reproducible withopenssl s_clientand no credentials, see the issue.The consequence is worse than a crash:
SmtpClient.onServerResponsebuildsSmtpResponse(responseTexts)outside thetryblock introduced in #81, so the error escapes the socket callback as an unhandled exception and the pending command's completer never completes. The caller hangs until its own timeout, and every retry fails the same way.Fixes #284.
Fix
lib/src/smtp/smtp_response.dartReply-code [ SP textstring ]per RFC 5321: a bare code yields an empty message; lines shorter than a reply code no longer throw fromsubstring(0, 3)and fall into the existing "not a code → 500 + raw text" branch.lib/src/smtp/smtp_client.dartSmtpResponseinside the existingtryso that any exception raised while handling a server reply fails the pending command viacompleter.completeError(the behaviour SMTP Client: Future never completes/fails when an internal exception is thrown #80/smtp: properly handle exceptions which occour while sending smtp commands. #81 intended) instead of leaving it hanging.No behaviour change for well-formed replies.
Tests
New
test/src/smtp/smtp_response_test.dart:250 OK,250-text, bare250,250/250-, non-numeric line, line shorter than a code, multilineSmtpResponseending with a bare code.New client-level tests in
test/smtp/smtp_client_test.dart:SmtpClient AUTH XOAUTH2 with bare 334 challenge— mirrors the Yandex exchange (334→ base64 →235);MockSmtpServergained a smallresponsesqueue for the two-step reply.SmtpClient EHLO with bare reply code— multiline reply ending in a bare250.Without the fix both throw the
RangeErrorabove and the returned future never completes (the XOAUTH2 test only ends by the test timeout).dart format --set-exit-if-changed .,dart analyzeand the fulldart testsuite (425 tests) pass locally.Downstream context
Hit in a Flutter mail client sending through a Yandex account with OAuth:
authenticate()never returned, the send stalled for the full per-phase timeout on every retry, with anUnhandled Exceptionin the socket zone rather than an error on the returned future.