Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -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<String> dnsLookupToGetJmapUrl(String emailAddress) {
return Future.sync(() async {
return await _dnsService.getJmapUrl(emailAddress);
return await _dnsLookupManager.lookupJmapUrl(emailAddress);
}).catchError(_exceptionThrower.throwException);
}

Expand Down
84 changes: 84 additions & 0 deletions lib/features/login/data/network/dns_lookup/dns_lookup_manager.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
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, timeout: _defaultTimeout);
case DnsLookupPriority.publicUdp:
return PublicUdpSrvClient(debugMode: debug, timeout: _defaultTimeout);
case DnsLookupPriority.publicDoh:
return DnsOverHttpsBinaryClient(debugMode: debug, timeout: _defaultTimeout);
case DnsLookupPriority.cloud:
return DnsOverHttps.empty(debugMode: debug, timeout: _defaultTimeout);
}
}

/// 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<String> 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));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are sorting priority, but there are 2 DnsLookupPriority with the same priority. Should we update them to be different?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are the same type so the priority can be the same.


for (final priority in priorities) {
final client = createClient(priority);
log('$runtimeType::lookupJmapUrl → 🔍 Trying ${priority.label} (timeout: ${client.timeout.inSeconds}s)...');

try {
final records = client is DnsOverHttps
? await client.lookupSrvMulti(jmapHostName)
: await client.lookupSrv(jmapHostName);

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(
'$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');
throw Exception('DNS lookup failed for $jmapHostName');
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
68 changes: 0 additions & 68 deletions lib/features/login/data/network/dns_service.dart

This file was deleted.

4 changes: 2 additions & 2 deletions lib/features/login/presentation/login_bindings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -74,7 +74,7 @@ class LoginBindings extends BaseBindings {
Get.find<CacheExceptionThrower>()
));
Get.lazyPut(() => LoginDataSourceImpl(
Get.find<DNSService>(),
Get.find<DnsLookupManager>(),
Get.find<RemoteExceptionThrower>(),
));
Get.lazyPut(() => SaasAuthenticationDataSourceImpl(
Expand Down
4 changes: 2 additions & 2 deletions lib/main/bindings/network/network_bindings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -144,6 +144,6 @@ class NetworkBindings extends Bindings {
}

void _bindingServices() {
Get.put(DNSService());
Get.put(DnsLookupManager());
}
}
49 changes: 36 additions & 13 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -1168,10 +1159,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:
Expand Down Expand Up @@ -2158,6 +2149,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.1"
super_dns:
dependency: transitive
description:
name: super_dns
sha256: "8cdf3ebabf1afe9711ab5bd0ab786634cada8a8f2af211edccb940b1ffd22f48"
url: "https://pub.dev"
source: hosted
version: "0.1.1"
super_dns_client:
dependency: "direct main"
description:
name: super_dns_client
sha256: dbb9d36e91c22dcd3455b930efbc3c6f68074a91d4e7e11efb89d721387f3593
url: "https://pub.dev"
source: hosted
version: "0.3.6"
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:
Expand Down
9 changes: 2 additions & 7 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -112,6 +105,8 @@ dependencies:
### Dependencies from pub.dev ###
super_tag_editor: 0.4.1

super_dns_client: 0.3.6

external_app_launcher: 4.0.3

cupertino_icons: 1.0.6
Expand Down
Loading
Loading