summaryrefslogtreecommitdiff
path: root/DomainDig/EmailSecurityService.swift
blob: 8922993f88fa59c7d176b0eefca8b1da98a96521 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import Foundation

struct EmailSecurityService {
    /// Analyze email security records. SPF is parsed from existing TXT records;
    /// DMARC and DKIM require additional DoH queries.
    static func analyze(domain: String, txtRecords: [DNSRecord]) async -> EmailSecurityResult {
        // SPF: extract from existing TXT records
        let spfRecord = txtRecords.first(where: { $0.value.lowercased().hasPrefix("v=spf1") })
        let spf = EmailSecurityRecord(found: spfRecord != nil, value: spfRecord?.value)

        // DMARC and DKIM queries in parallel
        async let dmarcResult = queryTXT(subdomain: "_dmarc.\(domain)")
        async let dkimResult = queryDKIM(domain: domain)

        let dmarcValue = await dmarcResult
        let dkimValue = await dkimResult

        let dmarc = EmailSecurityRecord(
            found: dmarcValue != nil,
            value: dmarcValue
        )
        let dkim = EmailSecurityRecord(
            found: dkimValue != nil,
            value: dkimValue
        )

        return EmailSecurityResult(spf: spf, dmarc: dmarc, dkim: dkim)
    }

    /// Query a TXT record for the given subdomain via DoH.
    private static func queryTXT(subdomain: String) async -> String? {
        do {
            let records = try await DNSLookupService.lookup(domain: subdomain, recordType: .TXT)
            return records.first?.value
        } catch {
            return nil
        }
    }

    /// Try common DKIM selectors and return the first found.
    private static func queryDKIM(domain: String) async -> String? {
        let selectors = ["default", "google", "mail"]
        return await withTaskGroup(of: (Int, String?).self, returning: String?.self) { group in
            for (index, selector) in selectors.enumerated() {
                group.addTask {
                    let value = await queryTXT(subdomain: "\(selector)._domainkey.\(domain)")
                    return (index, value)
                }
            }

            var results: [(Int, String?)] = []
            for await result in group {
                results.append(result)
            }
            // Return the first (by selector order) that has a value
            return results
                .sorted { $0.0 < $1.0 }
                .first(where: { $0.1 != nil })?.1
        }
    }
}