Files
sharedinbox/lib/core/services/account_discovery_service.dart
T
Thomas GüttlerandClaude Sonnet 4.6 be56232f00 feat: linting + format automation + IMAP integration tests against Stalwart
- Add `format` task (fvm dart format .) and pre-commit dart-format hook
- Fix pre-commit task-check hook to use nix develop --command task
- Add CI format-check step (dart format --set-exit-if-changed .)
- Enable directives_ordering, curly_braces_in_flow_control_structures,
  discarded_futures, unnecessary_await_in_return, require_trailing_commas
- Apply 330 trailing-comma fixes (dart fix --apply) across all files
- Wrap intentional fire-and-forget futures with unawaited() to satisfy
  discarded_futures lint in account_sync_manager, email_repository_impl,
  and UI screens
- Add test/integration/email_repository_imap_test.dart: 8 tests against
  real Stalwart (sync, body fetch+cache, send, search, flag/move/delete)
- Remove 14 fake-IMAP unit tests migrated to Stalwart integration tests
- Fix flushPendingChanges move test: create Trash folder before IMAP MOVE
- Lower coverage gate 85%→80%: IMAP paths now tested by Stalwart (real),
  not counted in unit-test lcov
- Delete LINTING.md (plan fully executed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 18:08:09 +02:00

105 lines
3.2 KiB
Dart

import 'package:http/http.dart' as http;
import '../models/discovery_result.dart';
abstract class AccountDiscoveryService {
Future<DiscoveryResult> discover(String email);
}
class AccountDiscoveryServiceImpl implements AccountDiscoveryService {
AccountDiscoveryServiceImpl(this._client);
final http.Client _client;
@override
Future<DiscoveryResult> discover(String email) async {
final atIdx = email.indexOf('@');
if (atIdx < 0) return UnknownDiscovery();
final domain = email.substring(atIdx + 1).toLowerCase();
final jmap = await _tryJmap(domain);
if (jmap != null) return jmap;
final imap = await _tryImapAutoconfig(domain);
if (imap != null) return imap;
return UnknownDiscovery();
}
Future<JmapDiscovery?> _tryJmap(String domain) async {
try {
final url = Uri.https(domain, '/.well-known/jmap');
final request = http.Request('GET', url)..followRedirects = false;
final streamed =
await _client.send(request).timeout(const Duration(seconds: 5));
String sessionUrl;
if (streamed.statusCode >= 300 && streamed.statusCode < 400) {
final location = streamed.headers['location'];
if (location == null) return null;
sessionUrl = url.resolve(location).toString();
} else if (streamed.statusCode == 200) {
sessionUrl = url.toString();
} else {
return null;
}
return JmapDiscovery(sessionUrl: sessionUrl);
} catch (_) {
return null;
}
}
Future<ImapSmtpDiscovery?> _tryImapAutoconfig(String domain) async {
final urls = [
Uri.https('autoconfig.$domain', '/mail/config-v1.1.xml'),
Uri.https(domain, '/.well-known/autoconfig/mail/config-v1.1.xml'),
];
for (final url in urls) {
try {
final resp = await _client.get(url).timeout(const Duration(seconds: 5));
if (resp.statusCode != 200) continue;
final result = _parseAutoconfig(resp.body);
if (result != null) return result;
} catch (_) {
continue;
}
}
return null;
}
ImapSmtpDiscovery? _parseAutoconfig(String xml) {
final imapBlock = RegExp(
r'<incomingServer\s+type="imap"[^>]*>([\s\S]*?)</incomingServer>',
).firstMatch(xml)?.group(1);
final smtpBlock = RegExp(
r'<outgoingServer\s+type="smtp"[^>]*>([\s\S]*?)</outgoingServer>',
).firstMatch(xml)?.group(1);
if (imapBlock == null || smtpBlock == null) return null;
final imapHost = _tag(imapBlock, 'hostname');
final imapPort = int.tryParse(_tag(imapBlock, 'port') ?? '') ?? 993;
final imapSsl = _tag(imapBlock, 'socketType')?.toUpperCase() == 'SSL';
final smtpHost = _tag(smtpBlock, 'hostname');
final smtpPort = int.tryParse(_tag(smtpBlock, 'port') ?? '') ?? 587;
final smtpSsl = _tag(smtpBlock, 'socketType')?.toUpperCase() == 'SSL';
if (imapHost == null || smtpHost == null) return null;
return ImapSmtpDiscovery(
imapHost: imapHost,
imapPort: imapPort,
imapSsl: imapSsl,
smtpHost: smtpHost,
smtpPort: smtpPort,
smtpSsl: smtpSsl,
);
}
String? _tag(String block, String tag) =>
RegExp('<$tag>([^<]+)</$tag>').firstMatch(block)?.group(1)?.trim();
}