Skip to content
Merged
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
4 changes: 3 additions & 1 deletion lib/src/smtp/smtp_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -451,10 +451,12 @@ class SmtpClient extends ClientBase {
log(responseText, isClient: false);
}
}
final response = SmtpResponse(responseTexts);
final cmd = _currentCommand;
if (cmd != null) {
try {
// parse inside the guard so that a malformed server reply fails the
// pending command instead of leaving its completer hanging forever
final response = SmtpResponse(responseTexts);
final next = cmd.next(response);
final text = next?.text;
final data = next?.data;
Expand Down
13 changes: 10 additions & 3 deletions lib/src/smtp/smtp_response.dart
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,18 @@ class SmtpResponseLine {
const SmtpResponseLine(this.code, this.message);

/// Parses the given response [text].
///
/// A reply line is `Reply-code [ SP textstring ]` or
/// `Reply-code "-" [ textstring ]` (RFC 5321, section 4.2), so both the
/// separator and the text are optional: a bare `250` is a valid line.
factory SmtpResponseLine.parse(String text) {
final code = int.tryParse(text.substring(0, 3));
final message = (code == null) ? text : text.substring(4);
final code = text.length < 3 ? null : int.tryParse(text.substring(0, 3));
if (code == null) {
return SmtpResponseLine(500, text);
}
final message = text.length > 4 ? text.substring(4) : '';

return SmtpResponseLine(code ?? 500, message);
return SmtpResponseLine(code, message);
}

/// The code of the response
Expand Down
5 changes: 5 additions & 0 deletions test/smtp/mock_smtp_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ class MockSmtpServer {
}

String? nextResponse;

/// Responses for consecutive requests, consumed before [nextResponse]
final List<String> responses = [];
final Socket _socket;

_MailSendState _sendState = _MailSendState.notStarted;
Expand All @@ -35,6 +38,8 @@ class MockSmtpServer {
return;
} else if (request == 'QUIT\r\n') {
writeln('221 2.0.0 Bye');
} else if (responses.isNotEmpty) {
writeln(responses.removeAt(0));
} else if (nextResponse == null || nextResponse.isEmpty) {
// // no supported request found, answer with the pre-defined response:
writeln('500 Invalid state - define nextResponse for MockSmtpServer');
Expand Down
25 changes: 25 additions & 0 deletions test/smtp/smtp_client_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,31 @@ void main() {
expect(client.serverInfo.supports('NOTTHERE'), isFalse);
});

test('SmtpClient EHLO with bare reply code', () async {
// RFC 5321 section 4.2: the last reply line may consist of the code only
_mockServer.nextResponse = '250-domain.com Hello\r\n250';
final response = await client.ehlo();
expect(response.type, SmtpResponseType.success);
expect(response.code, 250);
expect(response.message, isEmpty);
});

test('SmtpClient AUTH XOAUTH2 with bare 334 challenge', () async {
// smtp.yandex.ru answers `AUTH XOAUTH2` (sent without an initial
// response) with an empty challenge written as `334<CRLF>` - no space
_mockServer.responses.addAll([
'334',
'235 2.7.0 Authentication successful',
]);
final response = await client.authenticate(
'user@example.com',
'access-token',
AuthMechanism.xoauth2,
);
expect(response.type, SmtpResponseType.success);
expect(response.code, 235);
});

test('SmtpClient login', () async {
_mockServer.nextResponse = '235 2.7.0 Authentication successful';
final response = await client.authenticate(_smtpUser, _smtpPassword);
Expand Down
54 changes: 54 additions & 0 deletions test/src/smtp/smtp_response_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import 'package:enough_mail/enough_mail.dart';
import 'package:test/test.dart';

void main() {
group('SmtpResponseLine.parse', () {
test('code with space and text', () {
final line = SmtpResponseLine.parse('250 OK');
expect(line.code, 250);
expect(line.message, 'OK');
expect(line.type, SmtpResponseType.success);
});

test('code with hyphen and text (multiline reply)', () {
final line = SmtpResponseLine.parse('250-PIPELINING');
expect(line.code, 250);
expect(line.message, 'PIPELINING');
});

test('bare code without text (RFC 5321 section 4.2)', () {
final line = SmtpResponseLine.parse('250');
expect(line.code, 250);
expect(line.message, isEmpty);
expect(line.type, SmtpResponseType.success);
});

test('code followed only by a separator', () {
expect(SmtpResponseLine.parse('250 ').message, isEmpty);
expect(SmtpResponseLine.parse('250-').message, isEmpty);
});

test('non-numeric prefix is kept as message with code 500', () {
final line = SmtpResponseLine.parse('garbage line');
expect(line.code, 500);
expect(line.message, 'garbage line');
expect(line.type, SmtpResponseType.fatalError);
});

test('line shorter than a reply code does not throw', () {
final line = SmtpResponseLine.parse('25');
expect(line.code, 500);
expect(line.message, '25');
});
});

group('SmtpResponse', () {
test('multiline reply ending with a bare code', () {
final response = SmtpResponse(['250-domain.com Hello', '250']);
expect(response.responseLines.length, 2);
expect(response.code, 250);
expect(response.message, isEmpty);
expect(response.isOkStatus, isTrue);
});
});
}
Loading