Code
·
160 lines
·
5338 bytes
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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160using MailSharp.Common;
using MailSharp.DNS;
using MailSharp.SMTP.Metrics;
using System.Net;
namespace MailSharp.SMTP.Services;
public class DmarcChecker(
IConfiguration configuration,
Resolver resolver,
DkimVerifier dkimVerifier,
SpfChecker spfChecker,
SmtpMetrics metrics,
ILogger<DmarcChecker> logger)
{
// Check DMARC policy for incoming email
public async Task<bool> CheckDmarcAsync(string emlContent, string clientIp, string mailFromDomain, string heloDomain, CancellationToken cancellationToken)
{
var eventIdConfig = configuration.GetSection("DmarcEventIds:DmarcCheck").Get<EventIdConfig>()
?? throw new InvalidOperationException("Missing DmarcEventIds:DmarcCheck");
logger.LogInformation(
new EventId(eventIdConfig.Id, eventIdConfig.Name),
configuration["DmarcLogMessages:DmarcCheck"],
mailFromDomain);
// Fetch DMARC policy from DNS
string? dmarcRecord = await ResolveDmarcRecordAsync(mailFromDomain, cancellationToken);
if (string.IsNullOrEmpty(dmarcRecord))
{
logger.LogWarning(
new EventId(eventIdConfig.Id, eventIdConfig.Name),
configuration["DmarcLogMessages:NoDmarcRecord"],
mailFromDomain);
return configuration.GetValue<bool>("DmarcSettings:FailOpen");
}
// Parse DMARC record
var dmarcFields = ParseDmarcRecord(dmarcRecord);
if (!dmarcFields.TryGetValue("v", out string? version) || version != "DMARC1")
{
logger.LogWarning(
new EventId(eventIdConfig.Id, eventIdConfig.Name),
configuration["DmarcLogMessages:InvalidDmarcRecord"],
mailFromDomain);
return configuration.GetValue<bool>("DmarcSettings:FailOpen");
}
if (!dmarcFields.TryGetValue("p", out string? policy))
{
logger.LogWarning(
new EventId(eventIdConfig.Id, eventIdConfig.Name),
configuration["DmarcLogMessages:MissingPolicy"],
mailFromDomain);
return configuration.GetValue<bool>("DmarcSettings:FailOpen");
}
// Check SPF and DKIM alignment
bool spfPass = await spfChecker.CheckSpfAsync(clientIp, mailFromDomain, heloDomain);
if(spfPass == false)
metrics.IncrementRejectedSpf();
bool dkimPass = await dkimVerifier.VerifyDkimAsync(emlContent, clientIp);
if(dkimPass == false)
metrics.IncrementRejectedDkim();
bool spfAligned = spfPass && IsSpfAligned(mailFromDomain, heloDomain);
bool dkimAligned = dkimPass && IsDkimAligned(emlContent, mailFromDomain);
bool dmarcPass = (spfAligned && dmarcFields.GetValueOrDefault("aspf", "r") == "r") ||
(dkimAligned && dmarcFields.GetValueOrDefault("adkim", "r") == "r");
if(dmarcPass == false)
metrics.IncrementRejectedDmarc();
// Apply DMARC policy
switch (policy.ToLower())
{
case "none":
return true; // Always pass for monitoring
case "quarantine":
case "reject":
if (!dmarcPass)
{
logger.LogWarning(
new EventId(eventIdConfig.Id, eventIdConfig.Name),
configuration["DmarcLogMessages:PolicyFailed"],
policy, mailFromDomain);
return false;
}
return true;
default:
logger.LogWarning(
new EventId(eventIdConfig.Id, eventIdConfig.Name),
configuration["DmarcLogMessages:UnknownPolicy"],
policy, mailFromDomain);
return configuration.GetValue<bool>("DmarcSettings:FailOpen");
}
}
// Resolve DMARC TXT record via DNS
private async Task<string?> ResolveDmarcRecordAsync(string domain, CancellationToken cancellationToken)
{
try
{
string host = configuration["DmarcSettings:DnsServer"] ?? "8.8.8.8";
int port = configuration.GetValue<int?>("DmarcSettings:DnsPort") ?? 53;
var dnsServer = new IPEndPoint(IPAddress.Parse(host), port);
var response = await resolver.QueryAsync(dnsServer, $"_dmarc.{domain}", DnsQType.TXT, DnsQClass.IN, cancellationToken);
return response.RecordsTXT
.SelectMany(r => r.Texts)
.FirstOrDefault(t => t.StartsWith("v=DMARC1", StringComparison.OrdinalIgnoreCase));
}
catch
{
return null;
}
}
// Parse DMARC record into key-value pairs
private static Dictionary<string, string> ParseDmarcRecord(string dmarcRecord)
{
var fields = new Dictionary<string, string>();
string[] parts = dmarcRecord.Split(';', StringSplitOptions.RemoveEmptyEntries);
foreach (string part in parts)
{
string trimmed = part.Trim();
if (string.IsNullOrEmpty(trimmed))
continue;
int equalsIndex = trimmed.IndexOf('=');
if (equalsIndex == -1)
continue;
string key = trimmed[..equalsIndex].Trim();
string value = trimmed[(equalsIndex + 1)..].Trim();
fields[key] = value;
}
return fields;
}
// Check SPF alignment (relaxed alignment)
private static bool IsSpfAligned(string mailFromDomain, string heloDomain)
{
return mailFromDomain.Equals(heloDomain, StringComparison.OrdinalIgnoreCase);
}
// Check DKIM alignment (relaxed alignment)
private static bool IsDkimAligned(string emlContent, string mailFromDomain)
{
string[] headers = emlContent.Split(new[] { "\r\n" }, StringSplitOptions.None);
string? dkimHeader = headers.FirstOrDefault(h => h.StartsWith("DKIM-Signature:", StringComparison.OrdinalIgnoreCase));
if (dkimHeader == null)
return false;
var dkimFields = ParseDmarcRecord(dkimHeader["DKIM-Signature:".Length..]);
return dkimFields.TryGetValue("d", out string? dkimDomain) &&
dkimDomain.Equals(mailFromDomain, StringComparison.OrdinalIgnoreCase);
}
}