From d12a9c8d134904e2d0ccf0bd6afc7b14d81798e6 Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 28 Oct 2025 11:29:37 +0700 Subject: [PATCH 1/6] TF-3976 Support DNS SRV resolvers without Cloudflare/Google dependency --- .../login_datasource_impl.dart | 8 +- .../dns_lookup/dns_lookup_manager.dart | 97 ++++++++ .../dns_lookup/dns_lookup_priority.dart | 27 +++ .../login/data/network/dns_service.dart | 68 ------ .../login/presentation/login_bindings.dart | 4 +- .../bindings/network/network_bindings.dart | 4 +- pubspec.lock | 49 +++- pubspec.yaml | 9 +- .../data/network/dns_lookup_manager_test.dart | 223 ++++++++++++++++++ 9 files changed, 393 insertions(+), 96 deletions(-) create mode 100644 lib/features/login/data/network/dns_lookup/dns_lookup_manager.dart create mode 100644 lib/features/login/data/network/dns_lookup/dns_lookup_priority.dart delete mode 100644 lib/features/login/data/network/dns_service.dart create mode 100644 test/features/login/data/network/dns_lookup_manager_test.dart diff --git a/lib/features/login/data/datasource_impl/login_datasource_impl.dart b/lib/features/login/data/datasource_impl/login_datasource_impl.dart index d1f9c0bdf6..ef85ee5901 100644 --- a/lib/features/login/data/datasource_impl/login_datasource_impl.dart +++ b/lib/features/login/data/datasource_impl/login_datasource_impl.dart @@ -1,23 +1,23 @@ import 'package:tmail_ui_user/features/login/data/datasource/login_datasource.dart'; -import 'package:tmail_ui_user/features/login/data/network/dns_service.dart'; +import 'package:tmail_ui_user/features/login/data/network/dns_lookup/dns_lookup_manager.dart'; import 'package:tmail_ui_user/features/login/domain/model/recent_login_url.dart'; import 'package:tmail_ui_user/features/login/domain/model/recent_login_username.dart'; import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; class LoginDataSourceImpl implements LoginDataSource { - final DNSService _dnsService; + final DnsLookupManager _dnsLookupManager; final ExceptionThrower _exceptionThrower; LoginDataSourceImpl( - this._dnsService, + this._dnsLookupManager, this._exceptionThrower ); @override Future dnsLookupToGetJmapUrl(String emailAddress) { return Future.sync(() async { - return await _dnsService.getJmapUrl(emailAddress); + return await _dnsLookupManager.lookupJmapUrl(emailAddress); }).catchError(_exceptionThrower.throwException); } diff --git a/lib/features/login/data/network/dns_lookup/dns_lookup_manager.dart b/lib/features/login/data/network/dns_lookup/dns_lookup_manager.dart new file mode 100644 index 0000000000..ee3ad1c8d8 --- /dev/null +++ b/lib/features/login/data/network/dns_lookup/dns_lookup_manager.dart @@ -0,0 +1,97 @@ +import 'dart:async'; + +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/build_utils.dart'; +import 'package:super_dns_client/super_dns_client.dart'; +import 'package:tmail_ui_user/features/login/data/network/dns_lookup/dns_lookup_priority.dart'; + +/// Handles DNS SRV lookups for JMAP service discovery. +/// +/// The manager attempts lookups in order of priority: +/// **System → Public UDP → Public DoH → Cloud (Google/Cloudflare)**. +class DnsLookupManager { + static const String _jmapServicePrefix = '_jmap._tcp'; + static const Duration _defaultTimeout = Duration(seconds: 3); + + /// Builds the JMAP SRV hostname from [emailAddress]. + /// + /// Example: + /// ``` + /// input : user@example.com + /// output: _jmap._tcp.example.com + /// ``` + String buildJmapHostName(String emailAddress) { + final parts = emailAddress.split('@'); + if (parts.length != 2 || parts[1].isEmpty) { + throw ArgumentError('Invalid email address: $emailAddress'); + } + return '$_jmapServicePrefix.${parts[1]}'; + } + + /// Creates the appropriate [DnsClient] for the given [priority]. + DnsClient createClient(DnsLookupPriority priority) { + const debug = BuildUtils.isDebugMode; + switch (priority) { + case DnsLookupPriority.system: + return SystemUdpSrvClient(debugMode: debug); + case DnsLookupPriority.publicUdp: + return PublicUdpSrvClient(debugMode: debug); + case DnsLookupPriority.publicDoh: + return DnsOverHttpsBinaryClient(debugMode: debug); + case DnsLookupPriority.cloud: + return DnsOverHttps.google(debugMode: debug); + } + } + + /// Attempts SRV resolution for the JMAP hostname derived from [emailAddress]. + /// + /// Each lookup attempt will timeout after [_defaultTimeout] seconds. + /// Returns the first successfully resolved hostname, or an empty string if all fail. + Future lookupJmapUrl(String emailAddress) async { + final jmapHostName = buildJmapHostName(emailAddress); + log('$runtimeType::lookupJmapUrl → Resolving SRV for: $jmapHostName'); + + final priorities = List.of(DnsLookupPriority.values) + ..sort((a, b) => a.priority.compareTo(b.priority)); + + for (final priority in priorities) { + final client = createClient(priority); + log('$runtimeType::lookupJmapUrl → 🔍 Trying ${priority.label} (timeout: ${_defaultTimeout.inSeconds}s)...'); + + try { + final records = client is DnsOverHttps + ? await client.lookupSrvParallel(jmapHostName).timeout( + _defaultTimeout, + onTimeout: () { + throw TimeoutException( + 'Lookup timed out after ${_defaultTimeout.inSeconds}s'); + }, + ) + : await client.lookupSrv(jmapHostName).timeout( + _defaultTimeout, + onTimeout: () { + throw TimeoutException( + 'Lookup timed out after ${_defaultTimeout.inSeconds}s'); + }, + ); + + if (records.isNotEmpty) { + final target = records.first.target; + log('$runtimeType::lookupJmapUrl → ✅ Success via ${priority.label}: $target'); + return target; + } + + log('$runtimeType::lookupJmapUrl → ⚠️ No records via ${priority.label}, continuing...'); + } on TimeoutException catch (_) { + logError( + '$runtimeType::lookupJmapUrl → ⏱️ ${priority.label} lookup timed out'); + } catch (error, stack) { + logError( + '$runtimeType::lookupJmapUrl → ❌ ${priority.label} lookup failed: $error, $stack'); + } + } + + log('$runtimeType::lookupJmapUrl → 🚨 All DNS lookups failed for $jmapHostName'); + return ''; + } +} diff --git a/lib/features/login/data/network/dns_lookup/dns_lookup_priority.dart b/lib/features/login/data/network/dns_lookup/dns_lookup_priority.dart new file mode 100644 index 0000000000..733ce567c7 --- /dev/null +++ b/lib/features/login/data/network/dns_lookup/dns_lookup_priority.dart @@ -0,0 +1,27 @@ +/// Represents the priority order and description of different DNS lookup modes. +/// +/// Priority level increases with fallback order: +/// 1 → System default +/// 2 → Public resolvers (UDP/TCP or DOH) +/// 3 → Cloud resolvers (Google/Cloudflare) +enum DnsLookupPriority { + /// Uses the device's system-configured DNS (e.g., from ISP or OS settings). + system(1, 'System Default'), + + /// Uses open DNS resolvers accessible via UDP/TCP (e.g., Quad9, OpenDNS). + publicUdp(2, 'Public DNS (UDP/TCP)'), + + /// Uses DNS-over-HTTPS (DoH) resolvers for secure name resolution. + publicDoh(2, 'Public DNS (DoH)'), + + /// Uses Google or Cloudflare DNS resolvers. + cloud(3, 'Cloud DNS (Google/Cloudflare)'); + + /// The lookup priority (lower means higher priority). + final int priority; + + /// A human-readable description for UI or logging. + final String label; + + const DnsLookupPriority(this.priority, this.label); +} diff --git a/lib/features/login/data/network/dns_service.dart b/lib/features/login/data/network/dns_service.dart deleted file mode 100644 index c7aab4b4d7..0000000000 --- a/lib/features/login/data/network/dns_service.dart +++ /dev/null @@ -1,68 +0,0 @@ - -import 'package:core/utils/app_logger.dart'; -import 'package:dns_client/dns_client.dart'; -import 'package:tmail_ui_user/features/login/domain/exceptions/login_exception.dart'; - -class DNSService { - static const String _jmapServiceName = '_jmap._tcp'; - - Future _dnsLookupToUrlFromSRVType({required String hostName}) async { - try { - final url = await _dnsLookupToUrlByGoogle(hostName: hostName); - if (url.isEmpty) { - return await _dnsLookupToUrlByCloudflare(hostName: hostName); - } - return url; - } catch (e) { - return await _dnsLookupToUrlByCloudflare(hostName: hostName); - } - } - - Future _dnsLookupToUrlByGoogle({required String hostName}) async { - final dns = DnsOverHttps.google(); - final listData = await dns.lookupDataByRRType(hostName, RRType.SRVType); - if (listData.isEmpty) { - throw NotFoundDataResourceRecordException(); - } - return _parsingUrlFromDataResourceRecord(listData.first); - } - - Future _dnsLookupToUrlByCloudflare({required String hostName}) async { - final dns = DnsOverHttps.cloudflare(); - final listData = await dns.lookupDataByRRType(hostName, RRType.SRVType); - if (listData.isEmpty) { - throw NotFoundDataResourceRecordException(); - } - return _parsingUrlFromDataResourceRecord(listData.first); - } - - String _parsingUrlFromDataResourceRecord(String data) { - if (data.isEmpty) { - throw NotFoundDataResourceRecordException(); - } - final listFieldData = data.split(' '); - if (listFieldData.isEmpty) { - throw NotFoundUrlException(); - } - final url = _removeDotAtEndOfString(listFieldData.last); - log('DNSService::_parsingUrlFromDataResourceRecord:url: $url'); - if (url.trim().isEmpty) { - throw NotFoundUrlException(); - } - return url; - } - - Future getJmapUrl(String emailAddress) async { - final domainName = emailAddress.split('@')[1]; - final jmapHostName = '$_jmapServiceName.$domainName'; - log('DNSHandler::getJmapUrl:jmapHostName: $jmapHostName'); - return await _dnsLookupToUrlFromSRVType(hostName: jmapHostName); - } - - String _removeDotAtEndOfString(String value) { - if (value.lastIndexOf('.') == value.length - 1) { - return value.substring(0, value.length - 1); - } - return value; - } -} diff --git a/lib/features/login/presentation/login_bindings.dart b/lib/features/login/presentation/login_bindings.dart index 0b01069206..4d7184d2fc 100644 --- a/lib/features/login/presentation/login_bindings.dart +++ b/lib/features/login/presentation/login_bindings.dart @@ -7,7 +7,7 @@ import 'package:tmail_ui_user/features/login/data/datasource/login_datasource.da import 'package:tmail_ui_user/features/login/data/datasource_impl/hive_login_datasource_impl.dart'; import 'package:tmail_ui_user/features/login/data/datasource_impl/login_datasource_impl.dart'; import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.dart'; -import 'package:tmail_ui_user/features/login/data/network/dns_service.dart'; +import 'package:tmail_ui_user/features/login/data/network/dns_lookup/dns_lookup_manager.dart'; import 'package:tmail_ui_user/features/login/data/repository/login_repository_impl.dart'; import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart'; import 'package:tmail_ui_user/features/login/domain/repository/authentication_oidc_repository.dart'; @@ -74,7 +74,7 @@ class LoginBindings extends BaseBindings { Get.find() )); Get.lazyPut(() => LoginDataSourceImpl( - Get.find(), + Get.find(), Get.find(), )); Get.lazyPut(() => SaasAuthenticationDataSourceImpl( diff --git a/lib/main/bindings/network/network_bindings.dart b/lib/main/bindings/network/network_bindings.dart index 618453273c..6c36a6cfee 100644 --- a/lib/main/bindings/network/network_bindings.dart +++ b/lib/main/bindings/network/network_bindings.dart @@ -18,7 +18,7 @@ import 'package:tmail_ui_user/features/login/data/local/authentication_info_cach import 'package:tmail_ui_user/features/login/data/local/oidc_configuration_cache_manager.dart'; import 'package:tmail_ui_user/features/login/data/local/token_oidc_cache_manager.dart'; import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.dart'; -import 'package:tmail_ui_user/features/login/data/network/dns_service.dart'; +import 'package:tmail_ui_user/features/login/data/network/dns_lookup/dns_lookup_manager.dart'; import 'package:tmail_ui_user/features/login/data/network/interceptors/authorization_interceptors.dart'; import 'package:tmail_ui_user/features/login/data/network/oidc_http_client.dart'; import 'package:tmail_ui_user/features/login/data/utils/library_platform/app_auth_plugin/app_auth_plugin.dart'; @@ -144,6 +144,6 @@ class NetworkBindings extends Bindings { } void _bindingServices() { - Get.put(DNSService()); + Get.put(DnsLookupManager()); } } \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index b054b9502a..889cdc7790 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -77,10 +77,10 @@ packages: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.7.0" async: dependency: "direct main" description: @@ -463,15 +463,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" - dns_client: - dependency: "direct main" - description: - path: "." - ref: twake-supported - resolved-ref: "0966c504c1a813ff40e22ea8896537c32d97b82d" - url: "https://github.com/linagora/dns_client.git" - source: git - version: "0.2.1" dotted_border: dependency: "direct main" description: @@ -1176,10 +1167,10 @@ packages: dependency: transitive description: name: freezed_annotation - sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.4" frontend_server_client: dependency: transitive description: @@ -2166,6 +2157,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + super_dns: + dependency: transitive + description: + name: super_dns + sha256: "213d06aafacdcb342ba491faceb50cfca19e90df9cbd2216b39949b5080b283d" + url: "https://pub.dev" + source: hosted + version: "0.1.0" + super_dns_client: + dependency: "direct main" + description: + name: super_dns_client + sha256: "78c2b9c38d96d95f85d495902fc3bc49354cd63db7c4ebd15faa55bff2e4d13f" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + super_ip: + dependency: transitive + description: + name: super_ip + sha256: "450f781cddd61b34793063e6f0508c190ca0664bb80a7df12a12b9ec4cd90d0e" + url: "https://pub.dev" + source: hosted + version: "0.1.0" + super_raw: + dependency: transitive + description: + name: super_raw + sha256: "7f33a7b6d11b0a829c9a8eda27767ea63f8ef61e1bcc179055861a84f1ad5744" + url: "https://pub.dev" + source: hosted + version: "0.1.0" super_tag_editor: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index da68f830c1..aa97e732b3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -90,13 +90,6 @@ dependencies: url: https://github.com/linagora/flutter-date-range-picker.git ref: main - # TODO: We will change it when the PR in upstream repository will be merged - # https://github.com/dietfriends/dns_client/pull/9 - dns_client: - git: - url: https://github.com/linagora/dns_client.git - ref: twake-supported - linagora_design_flutter: git: url: https://github.com/linagora/linagora-design-flutter.git @@ -112,6 +105,8 @@ dependencies: ### Dependencies from pub.dev ### super_tag_editor: 1.1.0 + super_dns_client: 0.3.1 + external_app_launcher: 4.0.3 cupertino_icons: 1.0.6 diff --git a/test/features/login/data/network/dns_lookup_manager_test.dart b/test/features/login/data/network/dns_lookup_manager_test.dart new file mode 100644 index 0000000000..07e9b929e2 --- /dev/null +++ b/test/features/login/data/network/dns_lookup_manager_test.dart @@ -0,0 +1,223 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:super_dns_client/super_dns_client.dart'; +import 'package:tmail_ui_user/features/login/data/network/dns_lookup/dns_lookup_manager.dart'; +import 'package:tmail_ui_user/features/login/data/network/dns_lookup/dns_lookup_priority.dart'; + +// Generate mock class for DnsClient +@GenerateMocks([DnsClient]) +import 'dns_lookup_manager_test.mocks.dart'; + +void main() { + late MockDnsClient mockSystemClient; + late MockDnsClient mockPublicClient; + late MockDnsClient mockDohClient; + late MockDnsClient mockCloudClient; + + setUp(() { + mockSystemClient = MockDnsClient(); + mockPublicClient = MockDnsClient(); + mockDohClient = MockDnsClient(); + mockCloudClient = MockDnsClient(); + }); + + group('DnsLookupManager.lookupJmapUrl', () { + test('✅ should return target when system resolver succeeds', () async { + // Arrange + when(mockSystemClient.lookupSrv(any)).thenAnswer((_) async => [ + const SrvRecord( + name: '_jmap._tcp.example.com', + port: 443, + priority: 10, + weight: 5, + ttl: 3600, + target: 'mail.example.com', + ) + ]); + + final manager = _TestableDnsLookupManager({ + DnsLookupPriority.system: mockSystemClient, + DnsLookupPriority.publicUdp: mockPublicClient, + DnsLookupPriority.publicDoh: mockDohClient, + DnsLookupPriority.cloud: mockCloudClient, + }); + + // Act + final result = await manager.lookupJmapUrl('user@example.com'); + + // Assert + expect(result, equals('mail.example.com')); + verify(mockSystemClient.lookupSrv('_jmap._tcp.example.com')).called(1); + verifyNever(mockPublicClient.lookupSrv(any)); + }); + + test('✅ should fall back when previous resolver fails', () async { + // Arrange + when(mockSystemClient.lookupSrv(any)) + .thenThrow(Exception('System failed')); + when(mockPublicClient.lookupSrv(any)).thenAnswer((_) async => [ + const SrvRecord( + name: '_jmap._tcp.example.com', + port: 443, + priority: 20, + weight: 5, + ttl: 3600, + target: 'mail-backup.example.com', + ) + ]); + + final manager = _TestableDnsLookupManager({ + DnsLookupPriority.system: mockSystemClient, + DnsLookupPriority.publicUdp: mockPublicClient, + DnsLookupPriority.publicDoh: mockDohClient, + DnsLookupPriority.cloud: mockCloudClient, + }); + + // Act + final result = await manager.lookupJmapUrl('user@example.com'); + + // Assert + expect(result, equals('mail-backup.example.com')); + verifyInOrder([ + mockSystemClient.lookupSrv(any), + mockPublicClient.lookupSrv(any), + ]); + }); + + test('✅ should skip to next resolver when previous returns empty list', + () async { + // Arrange + when(mockSystemClient.lookupSrv(any)).thenAnswer((_) async => []); + when(mockPublicClient.lookupSrv(any)).thenAnswer((_) async => [ + const SrvRecord( + name: '_jmap._tcp.example.com', + port: 443, + priority: 15, + weight: 5, + ttl: 3600, + target: 'mail-fallback.example.com', + ) + ]); + + final manager = _TestableDnsLookupManager({ + DnsLookupPriority.system: mockSystemClient, + DnsLookupPriority.publicUdp: mockPublicClient, + DnsLookupPriority.publicDoh: mockDohClient, + DnsLookupPriority.cloud: mockCloudClient, + }); + + // Act + final result = await manager.lookupJmapUrl('user@example.com'); + + // Assert + expect(result, equals('mail-fallback.example.com')); + verifyInOrder([ + mockSystemClient.lookupSrv(any), + mockPublicClient.lookupSrv(any), + ]); + }); + + test('⚠️ should continue when some resolvers throw exceptions', () async { + // Arrange + when(mockSystemClient.lookupSrv(any)) + .thenThrow(Exception('System crashed')); + when(mockPublicClient.lookupSrv(any)) + .thenThrow(Exception('Network down')); + when(mockDohClient.lookupSrv(any)).thenAnswer((_) async => [ + const SrvRecord( + name: '_jmap._tcp.example.com', + port: 443, + priority: 5, + weight: 5, + ttl: 3600, + target: 'mail-doh.example.com', + ) + ]); + + final manager = _TestableDnsLookupManager({ + DnsLookupPriority.system: mockSystemClient, + DnsLookupPriority.publicUdp: mockPublicClient, + DnsLookupPriority.publicDoh: mockDohClient, + DnsLookupPriority.cloud: mockCloudClient, + }); + + // Act + final result = await manager.lookupJmapUrl('user@example.com'); + + // Assert + expect(result, equals('mail-doh.example.com')); + verify(mockDohClient.lookupSrv(any)).called(1); + }); + + test('⏱️ should return empty string when all resolvers timeout', () async { + // Arrange: simulate all resolvers hanging + when(mockSystemClient.lookupSrv(any)).thenAnswer( + (_) => Future.delayed(const Duration(seconds: 10), () => [])); + when(mockPublicClient.lookupSrv(any)).thenAnswer( + (_) => Future.delayed(const Duration(seconds: 10), () => [])); + when(mockDohClient.lookupSrv(any)).thenAnswer( + (_) => Future.delayed(const Duration(seconds: 10), () => [])); + when(mockCloudClient.lookupSrv(any)).thenAnswer( + (_) => Future.delayed(const Duration(seconds: 10), () => [])); + + final manager = _TestableDnsLookupManager({ + DnsLookupPriority.system: mockSystemClient, + DnsLookupPriority.publicUdp: mockPublicClient, + DnsLookupPriority.publicDoh: mockDohClient, + DnsLookupPriority.cloud: mockCloudClient, + }); + + // Act + final result = await manager.lookupJmapUrl('user@example.com'); + + // Assert + expect(result, isEmpty); + }); + + test('❌ should throw ArgumentError when email is invalid', () async { + final manager = _TestableDnsLookupManager({}); + expect( + () => manager.lookupJmapUrl('invalid-email'), + throwsA(isA()), + ); + }); + }); + + group('DnsLookupManager._buildJmapHostName', () { + test('✅ should build correct host name from email', () { + final manager = DnsLookupManager(); + final result = manager.buildJmapHostNameForTest('user@domain.com'); + expect(result, equals('_jmap._tcp.domain.com')); + }); + + test('❌ should throw ArgumentError for malformed email', () { + final manager = DnsLookupManager(); + expect( + () => manager.buildJmapHostNameForTest('invalid'), + throwsA(isA()), + ); + }); + }); +} + +/// Test helper subclass that injects mock DNS clients. +class _TestableDnsLookupManager extends DnsLookupManager { + final Map clients; + + _TestableDnsLookupManager(this.clients); + + @override + DnsClient createClient(DnsLookupPriority priority) { + final client = clients[priority]; + if (client == null) { + throw StateError('No mock client provided for $priority'); + } + return client; + } +} + +/// Extends real DnsLookupManager only for testing private helpers. +extension DnsLookupManagerTestable on DnsLookupManager { + String buildJmapHostNameForTest(String email) => buildJmapHostName(email); +} From 52835a3d0f7a7fd45a92709530304cc6804c38ac Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 28 Oct 2025 13:18:10 +0700 Subject: [PATCH 2/6] TF-3976 Fix build web version is failed on CI --- pubspec.lock | 8 ++++---- pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 889cdc7790..6a3ae56c33 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -2161,18 +2161,18 @@ packages: dependency: transitive description: name: super_dns - sha256: "213d06aafacdcb342ba491faceb50cfca19e90df9cbd2216b39949b5080b283d" + sha256: "8cdf3ebabf1afe9711ab5bd0ab786634cada8a8f2af211edccb940b1ffd22f48" url: "https://pub.dev" source: hosted - version: "0.1.0" + version: "0.1.1" super_dns_client: dependency: "direct main" description: name: super_dns_client - sha256: "78c2b9c38d96d95f85d495902fc3bc49354cd63db7c4ebd15faa55bff2e4d13f" + sha256: fe51cb3da20be471ca1d72293bf0f86ddff015fbc1644b586cdb695e85433b9c url: "https://pub.dev" source: hosted - version: "0.3.1" + version: "0.3.3" super_ip: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index aa97e732b3..9bae1a1413 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -105,7 +105,7 @@ dependencies: ### Dependencies from pub.dev ### super_tag_editor: 1.1.0 - super_dns_client: 0.3.1 + super_dns_client: 0.3.3 external_app_launcher: 4.0.3 From a7a1fc1e61cceb8ce320eb842e4773d414a9000b Mon Sep 17 00:00:00 2001 From: dab246 Date: Tue, 11 Nov 2025 17:49:52 +0700 Subject: [PATCH 3/6] TF-3976: Fix URL lookup failure when DNS resolution fails --- .../dns_lookup/dns_lookup_manager.dart | 33 ++++++------------- pubspec.lock | 4 +-- pubspec.yaml | 2 +- 3 files changed, 13 insertions(+), 26 deletions(-) diff --git a/lib/features/login/data/network/dns_lookup/dns_lookup_manager.dart b/lib/features/login/data/network/dns_lookup/dns_lookup_manager.dart index ee3ad1c8d8..03407ec45a 100644 --- a/lib/features/login/data/network/dns_lookup/dns_lookup_manager.dart +++ b/lib/features/login/data/network/dns_lookup/dns_lookup_manager.dart @@ -33,13 +33,13 @@ class DnsLookupManager { const debug = BuildUtils.isDebugMode; switch (priority) { case DnsLookupPriority.system: - return SystemUdpSrvClient(debugMode: debug); + return SystemUdpSrvClient(debugMode: debug, timeout: _defaultTimeout); case DnsLookupPriority.publicUdp: - return PublicUdpSrvClient(debugMode: debug); + return PublicUdpSrvClient(debugMode: debug, timeout: _defaultTimeout); case DnsLookupPriority.publicDoh: - return DnsOverHttpsBinaryClient(debugMode: debug); + return DnsOverHttpsBinaryClient(debugMode: debug, timeout: _defaultTimeout); case DnsLookupPriority.cloud: - return DnsOverHttps.google(debugMode: debug); + return DnsOverHttps.empty(debugMode: debug, timeout: _defaultTimeout); } } @@ -56,31 +56,18 @@ class DnsLookupManager { for (final priority in priorities) { final client = createClient(priority); - log('$runtimeType::lookupJmapUrl → 🔍 Trying ${priority.label} (timeout: ${_defaultTimeout.inSeconds}s)...'); + log('$runtimeType::lookupJmapUrl → 🔍 Trying ${priority.label} (timeout: ${client.timeout.inSeconds}s)...'); try { final records = client is DnsOverHttps - ? await client.lookupSrvParallel(jmapHostName).timeout( - _defaultTimeout, - onTimeout: () { - throw TimeoutException( - 'Lookup timed out after ${_defaultTimeout.inSeconds}s'); - }, - ) - : await client.lookupSrv(jmapHostName).timeout( - _defaultTimeout, - onTimeout: () { - throw TimeoutException( - 'Lookup timed out after ${_defaultTimeout.inSeconds}s'); - }, - ); + ? await client.lookupSrvMulti(jmapHostName) + : await client.lookupSrv(jmapHostName); - if (records.isNotEmpty) { - final target = records.first.target; + final target = records.firstOrNull?.target ?? ''; + if (target.isNotEmpty) { log('$runtimeType::lookupJmapUrl → ✅ Success via ${priority.label}: $target'); return target; } - log('$runtimeType::lookupJmapUrl → ⚠️ No records via ${priority.label}, continuing...'); } on TimeoutException catch (_) { logError( @@ -92,6 +79,6 @@ class DnsLookupManager { } log('$runtimeType::lookupJmapUrl → 🚨 All DNS lookups failed for $jmapHostName'); - return ''; + throw Exception('DNS lookup failed for $jmapHostName'); } } diff --git a/pubspec.lock b/pubspec.lock index 6a3ae56c33..836562418a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -2169,10 +2169,10 @@ packages: dependency: "direct main" description: name: super_dns_client - sha256: fe51cb3da20be471ca1d72293bf0f86ddff015fbc1644b586cdb695e85433b9c + sha256: dbb9d36e91c22dcd3455b930efbc3c6f68074a91d4e7e11efb89d721387f3593 url: "https://pub.dev" source: hosted - version: "0.3.3" + version: "0.3.6" super_ip: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9bae1a1413..ca2457b32b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -105,7 +105,7 @@ dependencies: ### Dependencies from pub.dev ### super_tag_editor: 1.1.0 - super_dns_client: 0.3.3 + super_dns_client: 0.3.6 external_app_launcher: 4.0.3 From 5bdd8467e27e4eb431bcb21444ef0527dff0ab72 Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 12 Nov 2025 10:07:03 +0700 Subject: [PATCH 4/6] TF-3976: Support get system dns via native --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 836562418a..66bf7d8d72 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -2169,10 +2169,10 @@ packages: dependency: "direct main" description: name: super_dns_client - sha256: dbb9d36e91c22dcd3455b930efbc3c6f68074a91d4e7e11efb89d721387f3593 + sha256: "4ac1516480a6f345a4c6867fbc65d5db8f6e90c2a41b19aa853f7e26e52ace20" url: "https://pub.dev" source: hosted - version: "0.3.6" + version: "0.3.7" super_ip: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ca2457b32b..a4ba12000a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -105,7 +105,7 @@ dependencies: ### Dependencies from pub.dev ### super_tag_editor: 1.1.0 - super_dns_client: 0.3.6 + super_dns_client: 0.3.7 external_app_launcher: 4.0.3 From a2d4ab61be797267b7d3496ee0e599872aae1db2 Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 12 Nov 2025 11:25:20 +0700 Subject: [PATCH 5/6] TF-3976 Add verify no call for remain clients in dns_lookup_manager_test --- .../data/network/dns_lookup_manager_test.dart | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/test/features/login/data/network/dns_lookup_manager_test.dart b/test/features/login/data/network/dns_lookup_manager_test.dart index 07e9b929e2..a996b00d28 100644 --- a/test/features/login/data/network/dns_lookup_manager_test.dart +++ b/test/features/login/data/network/dns_lookup_manager_test.dart @@ -6,7 +6,7 @@ import 'package:tmail_ui_user/features/login/data/network/dns_lookup/dns_lookup_ import 'package:tmail_ui_user/features/login/data/network/dns_lookup/dns_lookup_priority.dart'; // Generate mock class for DnsClient -@GenerateMocks([DnsClient]) +@GenerateNiceMocks([MockSpec()]) import 'dns_lookup_manager_test.mocks.dart'; void main() { @@ -20,6 +20,11 @@ void main() { mockPublicClient = MockDnsClient(); mockDohClient = MockDnsClient(); mockCloudClient = MockDnsClient(); + + when(mockSystemClient.timeout).thenReturn(const Duration(seconds: 3)); + when(mockPublicClient.timeout).thenReturn(const Duration(seconds: 3)); + when(mockDohClient.timeout).thenReturn(const Duration(seconds: 3)); + when(mockCloudClient.timeout).thenReturn(const Duration(seconds: 3)); }); group('DnsLookupManager.lookupJmapUrl', () { @@ -83,6 +88,9 @@ void main() { mockSystemClient.lookupSrv(any), mockPublicClient.lookupSrv(any), ]); + + verifyNever(mockDohClient.lookupSrv(any)); + verifyNever(mockCloudClient.lookupSrv(any)); }); test('✅ should skip to next resolver when previous returns empty list', @@ -116,6 +124,9 @@ void main() { mockSystemClient.lookupSrv(any), mockPublicClient.lookupSrv(any), ]); + + verifyNever(mockDohClient.lookupSrv(any)); + verifyNever(mockCloudClient.lookupSrv(any)); }); test('⚠️ should continue when some resolvers throw exceptions', () async { @@ -148,18 +159,19 @@ void main() { // Assert expect(result, equals('mail-doh.example.com')); verify(mockDohClient.lookupSrv(any)).called(1); + verifyNever(mockCloudClient.lookupSrv(any)); }); test('⏱️ should return empty string when all resolvers timeout', () async { // Arrange: simulate all resolvers hanging when(mockSystemClient.lookupSrv(any)).thenAnswer( - (_) => Future.delayed(const Duration(seconds: 10), () => [])); + (_) => Future.delayed(const Duration(seconds: 3), () => [])); when(mockPublicClient.lookupSrv(any)).thenAnswer( - (_) => Future.delayed(const Duration(seconds: 10), () => [])); + (_) => Future.delayed(const Duration(seconds: 3), () => [])); when(mockDohClient.lookupSrv(any)).thenAnswer( - (_) => Future.delayed(const Duration(seconds: 10), () => [])); + (_) => Future.delayed(const Duration(seconds: 3), () => [])); when(mockCloudClient.lookupSrv(any)).thenAnswer( - (_) => Future.delayed(const Duration(seconds: 10), () => [])); + (_) => Future.delayed(const Duration(seconds: 3), () => [])); final manager = _TestableDnsLookupManager({ DnsLookupPriority.system: mockSystemClient, @@ -168,11 +180,11 @@ void main() { DnsLookupPriority.cloud: mockCloudClient, }); - // Act - final result = await manager.lookupJmapUrl('user@example.com'); - // Assert - expect(result, isEmpty); + expect( + () => manager.lookupJmapUrl('user@example.com'), + throwsA(isA()), + ); }); test('❌ should throw ArgumentError when email is invalid', () async { From ac5bda8facb3fb225a6dfa9959d934bb25827aa3 Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 12 Nov 2025 12:27:47 +0700 Subject: [PATCH 6/6] TF-3976 [WEB] Resolve `InternetAddress` type mismatch using `universal_io` --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 66bf7d8d72..b69baf0a9d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -2169,10 +2169,10 @@ packages: dependency: "direct main" description: name: super_dns_client - sha256: "4ac1516480a6f345a4c6867fbc65d5db8f6e90c2a41b19aa853f7e26e52ace20" + sha256: e03244e5a14232836c448e96fde6fee2dbd5123c0337872ba2826e852ccd4d56 url: "https://pub.dev" source: hosted - version: "0.3.7" + version: "0.3.8" super_ip: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a4ba12000a..03f6472032 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -105,7 +105,7 @@ dependencies: ### Dependencies from pub.dev ### super_tag_editor: 1.1.0 - super_dns_client: 0.3.7 + super_dns_client: 0.3.8 external_app_launcher: 4.0.3