Skip to content

Commit

Permalink
Bring DNS lookup from basic_utils into this project
Browse files Browse the repository at this point in the history
Release v1.0.2
  • Loading branch information
robert-virkus committed Aug 27, 2022
1 parent 5047400 commit 0d2dafa
Show file tree
Hide file tree
Showing 16 changed files with 432 additions and 17 deletions.
8 changes: 7 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
{
"cSpell.words": [
"Imap"
"autoconfig",
"cupertino",
"dnssec",
"EMAILDOMAIN",
"Imap",
"rrecord",
"starttls"
]
}
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## 1.0.1
## 1.0.2
* Fork DSN look-up from [basic_utils](https://github.com/Ephenodrom/Dart-Basic-Utils) into enough_mail_discovery to ensure full web-compatibility
* Improve documentation


## 1.0.1
* Improve documentation
* Add build dependency

Expand Down
17 changes: 13 additions & 4 deletions example/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,14 @@ class _MyHomePageState extends State<MyHomePage> {
),
if (config != null)
DiscoveredConfigViewer(config: config)
else if (_hasDiscovered)
else if (_hasDiscovered) ...[
const SizedBox(height: 16),
const Text('Unable to resolve settings...'),
const SizedBox(height: 8),
if (kIsWeb)
const Text('Note that enough_mail_discovery can find more '
'settings on non-web platforms')
],
],
),
),
Expand All @@ -110,10 +116,12 @@ class _MyHomePageState extends State<MyHomePage> {

bool _checkEmailAddressValidity() {
final emailAddress = _editingController.text;
final atIndex = emailAddress.indexOf('@');
final lastDotIndex = emailAddress.lastIndexOf('.');
return emailAddress.length > 5 &&
emailAddress.indexOf('@') > 1 &&
emailAddress.contains('.') &&
emailAddress.lastIndexOf('.') < emailAddress.length - 2;
atIndex > 1 &&
lastDotIndex > atIndex + 1 &&
lastDotIndex < emailAddress.length - 2;
}

Future<void> _discover() async {
Expand All @@ -125,6 +133,7 @@ class _MyHomePageState extends State<MyHomePage> {
final emailAddress = _editingController.text;
final result = await Discover.discover(
emailAddress,
isLogEnabled: true,
forceSslConnection: _forceSslConnection,
isWeb: kIsWeb,
);
Expand Down
2 changes: 1 addition & 1 deletion lib/enough_mail_discovery.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
library enough_mail_discovery;

export 'src/discover.dart';
export 'src/client_config.dart';
export 'src/discover.dart';
3 changes: 1 addition & 2 deletions lib/src/discover.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import 'discover_helper.dart';

import 'client_config.dart';
import 'discover_helper.dart';

/// Helps discovering email connection settings based on an email address.
///
Expand Down
8 changes: 4 additions & 4 deletions lib/src/discover_helper.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import 'dart:async';

import 'package:basic_utils/basic_utils.dart' as basic;
import 'package:collection/collection.dart' show IterableExtension;
import 'package:http/http.dart' as http;
import 'package:universal_io/io.dart';
import 'package:xml/xml.dart' as xml;
import 'package:http/http.dart' as http;

import 'client_config.dart';
import 'dns/dns_utils.dart';
import 'dns/rrecord_type.dart';

/// Low-level helper methods for mail scenarios
class DiscoverHelper {
Expand Down Expand Up @@ -70,8 +71,7 @@ class DiscoverHelper {

/// Looks up domain referenced by the domain's DNS MX record
static Future<String?> discoverMxDomain(String domain) async {
final mxRecords =
await basic.DnsUtils.lookupRecord(domain, basic.RRecordType.MX);
final mxRecords = await DnsUtils.lookupRecord(domain, RRecordType.MX);
if (mxRecords == null || mxRecords.isEmpty) {
//print('unable to read MX records for [$domain].');
return null;
Expand Down
174 changes: 174 additions & 0 deletions lib/src/dns/dns_utils.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import 'dart:async';
import 'dart:convert';

import 'package:http/http.dart' as http;

import 'resolve_response.dart';
import 'rrecord.dart';
import 'rrecord_type.dart';

/// Supported DNS API providers
enum DnsApiProvider {
/// https://dns.google.com/resolve
google,

///https://cloudflare-dns.com/dns-query
cloudflare,
}

/// Helper class for dns record lookups
///
/// Migrated from https://github.com/Ephenodrom/Dart-Basic-Utils to
/// ensure compatibility with Flutter web.
class DnsUtils {
DnsUtils._();

/// Base url for each dns resolver
///
static const _dnsApiProviderUrl = {
DnsApiProvider.google: 'https://dns.google.com/resolve',
DnsApiProvider.cloudflare: 'https://cloudflare-dns.com/dns-query',
};

/// Lookup for records of the given [type] and [name].
/// It also supports [dnssec]
///
static Future<List<RRecord>?> lookupRecord(
String name,
RRecordType type, {
bool dnssec = false,
DnsApiProvider provider = DnsApiProvider.google,
}) async {
final url = _dnsApiProviderUrl[provider]!;
final uri = Uri.parse(
'$url?name=$name&type=${_getTypeFromType(type)}&dnssec=$dnssec');

final headers = {'Accept': 'application/dns-json'};

final httpResponse = await http.get(uri, headers: headers);
final response = ResolveResponse.fromJson(jsonDecode(httpResponse.body));
return response.answer;
}

static String _getTypeFromType(RRecordType type) =>
rRecordTypeToInt(type).toString();

///
/// Converts the given number [type] to a [RRecordType] enum.
///
static RRecordType intToRRecordType(int type) =>
_intToRRecordType[type] ?? RRecordType.A;

///
/// Converts the given type to a decimal number
///
static int rRecordTypeToInt(RRecordType type) => _rRecordTypeToInt[type] ?? 1;

///
/// Map from [RRecordType] enum to number
///
static const _rRecordTypeToInt = {
RRecordType.A: 1,
RRecordType.AAAA: 28,
RRecordType.ANY: 255,
RRecordType.CAA: 257,
RRecordType.CDS: 59,
RRecordType.CERT: 37,
RRecordType.CNAME: 5,
RRecordType.DNAME: 39,
RRecordType.DNSKEY: 48,
RRecordType.DS: 43,
RRecordType.HINFO: 13,
RRecordType.IPSECKEY: 45,
RRecordType.MX: 15,
RRecordType.NAPTR: 35,
RRecordType.NS: 2,
RRecordType.NSEC: 47,
RRecordType.NSEC3PARAM: 51,
RRecordType.PTR: 12,
RRecordType.RP: 17,
RRecordType.RRSIG: 46,
RRecordType.SOA: 6,
RRecordType.SPF: 99,
RRecordType.SRV: 33,
RRecordType.SSHFP: 44,
RRecordType.TLSA: 52,
RRecordType.TXT: 16,
RRecordType.WKS: 11,
};

///
/// Map from number to [RRecordType] enum
///
static final _intToRRecordType =
_rRecordTypeToInt.map((k, v) => MapEntry(v, k));

/// Reverse lookup for the given [ip] to determine a hostname.
///
/// This reverses the given [ip], adds ".in-addr.arpa" / ".ip6.arpa" and
/// tries to fetch a PTR record for the generated name.
///
/// Will return null, if no IP address is given or no PTR is found.
///
static Future<List<RRecord>?> reverseDns(
String ip, {
DnsApiProvider provider = DnsApiProvider.google,
}) async {
final reverse = getReverseAddr(ip);
if (reverse == null) {
return null;
}

final url = _dnsApiProviderUrl[provider];
final uri = Uri.parse(
'$url?name=$reverse&type=${_getTypeFromType(RRecordType.PTR)}');
final headers = {'Accept': 'application/dns-json'};

final httpResponse = await http.get(uri, headers: headers);
final response = ResolveResponse.fromJson(jsonDecode(httpResponse.body));
return response.answer;
}

///
/// Reverses the given [ip] address. Will return null if the given [ip] is not
/// an IP address.
///
/// Example :
/// 172.217.22.14 => 14.22.217.172.in-addr.arpa
/// 2a00:1450:4001:81a::200e => e.0.0.2.a.1.8.1.0.0.4.0.5.4.1.0.0.a.2.ip6.arpa
///
static String? getReverseAddr(String ip) {
if (ip.contains('.')) {
return '${ip.split('.').reversed.join('.')}.in-addr.arpa';
} else if (ip.contains(':')) {
return '${ip.split(':').join().split('').reversed.join('.')}.ip6.arpa';
} else {
return null;
}
}

///
/// Converts the record to the BIND representation.
///
static String toBind(RRecord record) {
final sb = StringBuffer()..write(record.name);
if (sb.length < 8) {
sb.write('\t');
}
if (sb.length < 16) {
sb.write('\t');
}
sb
..write('\t')
..write(record.ttl)
..write('\tIN\t')
..write(intToRRecordType(record.rType)
.toString()
.substring('RRecordType.'.length))
..write('\t')
..write('\"')
..write(record.data)
..write('\"');
return sb.toString();
}
}
25 changes: 25 additions & 0 deletions lib/src/dns/question.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// ignore_for_file: public_member_api_docs

import 'package:json_annotation/json_annotation.dart';

part 'question.g.dart';

@JsonSerializable(includeIfNull: false)
class Question {
const Question({required this.name, required this.type});

/*
* Json to Question object
*/
factory Question.fromJson(Map<String, dynamic> json) =>
_$QuestionFromJson(json);

final String name;

final int type;

/*
* Question object to json
*/
Map<String, dynamic> toJson() => _$QuestionToJson(this);
}
17 changes: 17 additions & 0 deletions lib/src/dns/question.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 51 additions & 0 deletions lib/src/dns/resolve_response.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// ignore_for_file: public_member_api_docs

import 'package:json_annotation/json_annotation.dart';

import 'question.dart';
import 'rrecord.dart';

part 'resolve_response.g.dart';

@JsonSerializable(includeIfNull: false)
class ResolveResponse {
const ResolveResponse({
this.status,
this.tc,
this.rd,
this.ra,
this.ad,
this.cd,
this.question,
this.answer,
this.comment,
});

/// Create [ResolveResponse] from [json]
factory ResolveResponse.fromJson(Map<String, dynamic> json) =>
_$ResolveResponseFromJson(json);

@JsonKey(name: 'Status')
final int? status;
@JsonKey(name: 'TC')
final bool? tc;
@JsonKey(name: 'RD')
final bool? rd;
@JsonKey(name: 'RA')
final bool? ra;
@JsonKey(name: 'AD')
final bool? ad;
@JsonKey(name: 'CD')
final bool? cd;
@JsonKey(name: 'Question')
final List<Question>? question;
@JsonKey(name: 'Answer')
final List<RRecord>? answer;
@JsonKey(name: 'Comment')
final String? comment;

/*
* ResolveResponse object to json
*/
Map<String, dynamic> toJson() => _$ResolveResponseToJson(this);
}
Loading

0 comments on commit 0d2dafa

Please sign in to comment.