krz/domain-dig

an ios app for DNS & SSL analysis

clone: git clone https://gitbay.org/krz/domain-dig.git

v1.4.0: DomainDig/EmailSecurityService.swift · raw

 1import Foundation
 2
 3struct EmailSecurityService {
 4    /// Analyze email security records. SPF is parsed from existing TXT records;
 5    /// DMARC and DKIM require additional DoH queries.
 6    static func analyze(domain: String, txtRecords: [DNSRecord]) async -> EmailSecurityResult {
 7        // SPF: extract from existing TXT records
 8        let spfRecord = txtRecords.first(where: { $0.value.lowercased().hasPrefix("v=spf1") })
 9        let spf = EmailSecurityRecord(found: spfRecord != nil, value: spfRecord?.value)
10
11        // DMARC and DKIM queries in parallel
12        async let dmarcResult = queryTXT(subdomain: "_dmarc.\(domain)")
13        async let dkimResult = queryDKIM(domain: domain)
14
15        let dmarcValue = await dmarcResult
16        let dkimValue = await dkimResult
17
18        let dmarc = EmailSecurityRecord(
19            found: dmarcValue != nil,
20            value: dmarcValue
21        )
22        let dkim = EmailSecurityRecord(
23            found: dkimValue != nil,
24            value: dkimValue
25        )
26
27        return EmailSecurityResult(spf: spf, dmarc: dmarc, dkim: dkim)
28    }
29
30    /// Query a TXT record for the given subdomain via DoH.
31    private static func queryTXT(subdomain: String) async -> String? {
32        do {
33            let records = try await DNSLookupService.lookup(domain: subdomain, recordType: .TXT)
34            return records.first?.value
35        } catch {
36            return nil
37        }
38    }
39
40    /// Try common DKIM selectors and return the first found.
41    private static func queryDKIM(domain: String) async -> String? {
42        let selectors = ["default", "google", "mail"]
43        return await withTaskGroup(of: (Int, String?).self, returning: String?.self) { group in
44            for (index, selector) in selectors.enumerated() {
45                group.addTask {
46                    let value = await queryTXT(subdomain: "\(selector)._domainkey.\(domain)")
47                    return (index, value)
48                }
49            }
50
51            var results: [(Int, String?)] = []
52            for await result in group {
53                results.append(result)
54            }
55            // Return the first (by selector order) that has a value
56            return results
57                .sorted { $0.0 < $1.0 }
58                .first(where: { $0.1 != nil })?.1
59        }
60    }
61}