Files
sharedinbox/lib/data/imap/imap_client_factory.dart
T
Thomas GüttlerandClaude Opus 4.7 da383d0957 feat: ManageSieve STARTTLS + clearer TLS-mismatch errors + broader connection test
The "Email filters" screen was failing with WRONG_VERSION_NUMBER because the
ManageSieve client was opening implicit-TLS sockets on port 4190, while RFC
5804 servers (Stalwart, Dovecot, Cyrus) listen plaintext on 4190 and expect
STARTTLS. ManageSieveClient.connect now opens plaintext, reads the capability
greeting, sends STARTTLS, hands the socket to SecureSocket.secure(), and
re-reads capabilities on the encrypted stream.

The same WRONG_VERSION_NUMBER error can hit IMAP/SMTP when the SSL toggle and
the chosen port disagree (e.g. SSL=on with SMTP port 587). New helper
lib/data/imap/tls_error.dart translates that BoringSSL error into a
TlsModeMismatchException naming the host/port and suggesting which port goes
with which TLS mode. connectImap, connectSmtp, and the ManageSieve TLS
upgrade all funnel through rethrowAsTlsHint so the same readable message
reaches the UI regardless of which protocol failed.

ConnectionTestService previously only verified IMAP/JMAP, so SMTP and
ManageSieve misconfig silently passed the "Try connection" button on the
edit-account screen and only surfaced when the user later tried to send
mail or open Email filters. After IMAP succeeds, the service now also
verifies SMTP (always — sending mail requires it) and ManageSieve (only
when manageSieveHost is explicitly set, since the section is collapsed by
default). Failures are prefixed with "SMTP:" or "ManageSieve:" so the
user can tell which leg of the connection is broken.

connectionTestServiceProvider now also watches smtpConnectProvider so the
E2E integration tests' plaintext SMTP override applies to the connection
check as well.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 10:31:55 +02:00

86 lines
2.6 KiB
Dart

import 'dart:async';
import 'package:enough_mail/enough_mail.dart';
import 'package:sharedinbox/core/models/account.dart';
import 'package:sharedinbox/data/imap/tls_error.dart';
typedef ImapConnectFn = Future<ImapClient> Function(
Account account,
String username,
String password,
);
/// Zone value key signalling that a [StringBuffer] for protocol logging is
/// active. When this key is non-null in the current zone, [connectImap]
/// enables IMAP trace logging so the output is captured by the zone's
/// print override.
const verboseLogKey = #verboseProtocolLog;
/// Opens an authenticated IMAP client for [account] using [username].
///
/// When the current [Zone] carries a [StringBuffer] under [verboseLogKey],
/// IMAP trace logging is enabled so each command/response is captured there.
Future<ImapClient> connectImap(
Account account,
String username,
String password,
) async {
final verboseBuffer = Zone.current[verboseLogKey] as StringBuffer?;
final client = ImapClient(
defaultResponseTimeout: const Duration(seconds: 20),
isLogEnabled: verboseBuffer != null,
);
try {
await client.connectToServer(
account.imapHost,
account.imapPort,
isSecure: account.imapSsl,
);
} catch (e, st) {
rethrowAsTlsHint(e, st, account.imapHost, account.imapPort);
}
await client.login(username, password);
return client;
}
/// Opens an authenticated SMTP client for [account] using [username].
///
/// When [account.smtpSsl] is false, STARTTLS is required and the connection
/// fails if the server does not support it. Plaintext fallback is not allowed.
///
/// Caller is responsible for calling [SmtpClient.quit] when done.
Future<SmtpClient> connectSmtp(
Account account,
String username,
String password,
) async {
// clientDomain is the sending domain advertised in EHLO — use the host part
// of the sender email, falling back to the SMTP host.
final atIndex = account.email.lastIndexOf('@');
final clientDomain =
atIndex != -1 ? account.email.substring(atIndex + 1) : account.smtpHost;
final client = SmtpClient(clientDomain);
try {
await client.connectToServer(
account.smtpHost,
account.smtpPort,
isSecure: account.smtpSsl,
);
} catch (e, st) {
rethrowAsTlsHint(e, st, account.smtpHost, account.smtpPort);
}
await client.ehlo();
if (!account.smtpSsl) {
// STARTTLS required on submission port (587). No plaintext fallback.
try {
await client.startTls();
} catch (e, st) {
rethrowAsTlsHint(e, st, account.smtpHost, account.smtpPort);
}
}
await client.authenticate(username, password);
return client;
}