Skip to content

Commit a931868

Browse files
committed
Optimize DmarcRecordParser
1 parent 97f6e58 commit a931868

File tree

7 files changed

+266
-63
lines changed

7 files changed

+266
-63
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1-
namespace Nager.EmailAuthentication.UnitTest.DmarcRecordParserTests
1+
using Nager.EmailAuthentication.Models;
2+
3+
namespace Nager.EmailAuthentication.UnitTest.DmarcRecordParserTests
24
{
35
[TestClass]
46
public sealed class ComplexTest
57
{
68
[TestMethod]
79
public void TryParse_ValidDmarcString1_ReturnsTrueAndPopulatesDmarcRecord()
810
{
9-
var isSuccessful = DmarcRecordDataFragmentParser.TryParse("v=DMARC1; p=reject; rua=mailto:[email protected], mailto:[email protected]; pct=100; adkim=s; aspf=s", out var dmarcDataFragment, out var parsingResults);
11+
var dmarcRecord = "v=DMARC1; p=reject; rua=mailto:[email protected], mailto:[email protected]; pct=100; adkim=s; aspf=s";
12+
13+
var isSuccessful = DmarcRecordDataFragmentParser.TryParse(dmarcRecord, out var dmarcDataFragment, out var parsingResults);
1014

1115
Assert.IsTrue(isSuccessful);
1216
Assert.IsNotNull(dmarcDataFragment);
@@ -21,21 +25,77 @@ public void TryParse_ValidDmarcString1_ReturnsTrueAndPopulatesDmarcRecord()
2125
[TestMethod]
2226
public void TryParse_ValidDmarcString2_ReturnsTrueAndPopulatesDmarcRecord()
2327
{
24-
var isSuccessful = DmarcRecordDataFragmentParser.TryParse("v=DMARC1; p=reject; rua=mailto:[email protected], mailto:[email protected]; pct=100; adkim=s; aspf=s", out var dmarcDataFragment, out var parsingResults);
25-
Assert.IsNotNull(dmarcDataFragment);
28+
var dmarcRecordRaw = "v=DMARC1; p=reject; rua=mailto:[email protected]!10m, mailto:[email protected]; pct=100; adkim=s; aspf=s";
2629

27-
var isSuccessful2 = DmarcRecordParser.TryParse(dmarcDataFragment, out var dmarcRecord);
28-
Assert.IsTrue(isSuccessful2);
29-
Assert.IsNotNull(dmarcRecord);
30+
var isDataFragmentParserSuccessful = DmarcRecordDataFragmentParser.TryParse(dmarcRecordRaw, out var dmarcDataFragment, out var parsingResults);
31+
Assert.IsNotNull(dmarcDataFragment);
3032

31-
Assert.IsTrue(isSuccessful);
33+
Assert.IsTrue(isDataFragmentParserSuccessful);
3234
Assert.IsNotNull(dmarcDataFragment);
3335
Assert.AreEqual("reject", dmarcDataFragment.DomainPolicy);
34-
Assert.AreEqual("mailto:[email protected], mailto:[email protected]", dmarcDataFragment.AggregateReportUri);
36+
Assert.AreEqual("mailto:[email protected]!10m, mailto:[email protected]", dmarcDataFragment.AggregateReportUri);
3537
Assert.AreEqual("100", dmarcDataFragment.PolicyPercentage);
3638
Assert.AreEqual("s", dmarcDataFragment.DkimAlignmentMode);
3739
Assert.AreEqual("s", dmarcDataFragment.SpfAlignmentMode);
3840
Assert.IsNull(parsingResults, "ParsingResults is not null");
41+
42+
var isParserSuccessful = DmarcRecordParser.TryParse(dmarcDataFragment, out var dmarcRecord);
43+
Assert.IsTrue(isParserSuccessful);
44+
Assert.IsNotNull(dmarcRecord);
45+
Assert.AreEqual(DmarcPolicy.Reject, dmarcRecord.DomainPolicy);
46+
Assert.AreEqual(DmarcPolicy.Reject, dmarcRecord.SubdomainPolicy);
47+
Assert.AreEqual(AlignmentMode.Strict, dmarcRecord.DkimAlignmentMode);
48+
Assert.AreEqual(AlignmentMode.Strict, dmarcRecord.SpfAlignmentMode);
49+
Assert.AreEqual(100, dmarcRecord.PolicyPercentage);
50+
}
51+
52+
[TestMethod]
53+
public void TryParse_ValidDmarcString3_ReturnsTrueAndPopulatesDmarcRecord()
54+
{
55+
var dmarcRecordRaw = "v=DMARC1; p=reject; rua=mailto:[email protected], mailto:[email protected]; pct=50; adkim=r; aspf=r";
56+
57+
var isDataFragmentParserSuccessful = DmarcRecordDataFragmentParser.TryParse(dmarcRecordRaw, out var dmarcDataFragment, out var parsingResults);
58+
Assert.IsNotNull(dmarcDataFragment);
59+
60+
Assert.IsTrue(isDataFragmentParserSuccessful);
61+
Assert.IsNotNull(dmarcDataFragment);
62+
Assert.AreEqual("reject", dmarcDataFragment.DomainPolicy);
63+
Assert.AreEqual("mailto:[email protected], mailto:[email protected]", dmarcDataFragment.AggregateReportUri);
64+
Assert.AreEqual("50", dmarcDataFragment.PolicyPercentage);
65+
Assert.AreEqual("r", dmarcDataFragment.DkimAlignmentMode);
66+
Assert.AreEqual("r", dmarcDataFragment.SpfAlignmentMode);
67+
Assert.IsNull(parsingResults, "ParsingResults is not null");
68+
69+
var isParserSuccessful = DmarcRecordParser.TryParse(dmarcDataFragment, out var dmarcRecord);
70+
Assert.IsTrue(isParserSuccessful);
71+
Assert.IsNotNull(dmarcRecord);
72+
Assert.AreEqual(DmarcPolicy.Reject, dmarcRecord.DomainPolicy);
73+
Assert.AreEqual(DmarcPolicy.Reject, dmarcRecord.SubdomainPolicy);
74+
Assert.AreEqual(AlignmentMode.Relaxed, dmarcRecord.DkimAlignmentMode);
75+
Assert.AreEqual(AlignmentMode.Relaxed, dmarcRecord.SpfAlignmentMode);
76+
Assert.AreEqual(50, dmarcRecord.PolicyPercentage);
77+
}
78+
79+
[TestMethod]
80+
public void TryParse_InvalidDmarcString1_ReturnsTrueAndPopulatesDmarcRecord()
81+
{
82+
var dmarcRecordRaw = "v=DMARC1; p=reject; rua=mailto:[email protected], mailto:[email protected]; pct=50; adkim=t; aspf=t";
83+
84+
var isDataFragmentParserSuccessful = DmarcRecordDataFragmentParser.TryParse(dmarcRecordRaw, out var dmarcDataFragment, out var parsingResults);
85+
Assert.IsNotNull(dmarcDataFragment);
86+
87+
Assert.IsTrue(isDataFragmentParserSuccessful);
88+
Assert.IsNotNull(dmarcDataFragment);
89+
Assert.AreEqual("reject", dmarcDataFragment.DomainPolicy);
90+
Assert.AreEqual("mailto:[email protected], mailto:[email protected]", dmarcDataFragment.AggregateReportUri);
91+
Assert.AreEqual("50", dmarcDataFragment.PolicyPercentage);
92+
Assert.AreEqual("t", dmarcDataFragment.DkimAlignmentMode);
93+
Assert.AreEqual("t", dmarcDataFragment.SpfAlignmentMode);
94+
Assert.IsNotNull(parsingResults, "ParsingResults is null");
95+
96+
var isParserSuccessful = DmarcRecordParser.TryParse(dmarcDataFragment, out var dmarcRecord);
97+
Assert.IsFalse(isParserSuccessful);
98+
Assert.IsNull(dmarcRecord);
3999
}
40100
}
41101
}

src/Nager.EmailAuthentication/DmarcRecordParser.cs

+137-6
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,147 @@ public static bool TryParse(
8383
{
8484
return false;
8585
}
86+
tempDmarcRecord.ReportingInterval = reportingInterval.Value;
8687

87-
tempDmarcRecord.ReportingInterval = reportingInterval;
88+
if (!TryGetPolicyPercentage(dmarcDataFragment.PolicyPercentage, out var policyPercentage))
89+
{
90+
return false;
91+
}
92+
tempDmarcRecord.PolicyPercentage = policyPercentage.Value;
93+
94+
if (dmarcDataFragment.ReportFormat == null)
95+
{
96+
tempDmarcRecord.ReportFormat = "afrf";
97+
}
98+
else
99+
{
100+
if (!dmarcDataFragment.ReportFormat.Equals("afrf", StringComparison.OrdinalIgnoreCase))
101+
{
102+
return false;
103+
}
104+
105+
tempDmarcRecord.ReportFormat = dmarcDataFragment.ReportFormat;
106+
}
107+
108+
if (dmarcDataFragment.DkimAlignmentMode != null)
109+
{
110+
if (!TryGetAlignmentMode(dmarcDataFragment.DkimAlignmentMode, out var dkimAlignmentMode))
111+
{
112+
return false;
113+
}
114+
tempDmarcRecord.DkimAlignmentMode = dkimAlignmentMode.Value;
115+
}
116+
117+
if (dmarcDataFragment.SpfAlignmentMode != null)
118+
{
119+
if (!TryGetAlignmentMode(dmarcDataFragment.SpfAlignmentMode, out var spfAlignmentMode))
120+
{
121+
return false;
122+
}
123+
tempDmarcRecord.SpfAlignmentMode = spfAlignmentMode.Value;
124+
}
125+
126+
if (!string.IsNullOrEmpty(dmarcDataFragment.AggregateReportUri))
127+
{
128+
var parts = dmarcDataFragment.AggregateReportUri.Split(',', StringSplitOptions.TrimEntries);
129+
130+
var emailDetails = new List<DmarcEmailDetail>();
131+
foreach (var part in parts)
132+
{
133+
if (!DmarcEmailDetail.TryParse(part, out var dmarcEmailDetail))
134+
{
135+
return false;
136+
}
137+
138+
emailDetails.Add(dmarcEmailDetail);
139+
}
140+
141+
tempDmarcRecord.AggregateReportUri = [.. emailDetails];
142+
}
143+
144+
if (!string.IsNullOrEmpty(dmarcDataFragment.ForensicReportUri))
145+
{
146+
var parts = dmarcDataFragment.ForensicReportUri.Split(',', StringSplitOptions.TrimEntries);
147+
148+
var emailDetails = new List<DmarcEmailDetail>();
149+
foreach (var part in parts)
150+
{
151+
if (!DmarcEmailDetail.TryParse(part, out var dmarcEmailDetail))
152+
{
153+
return false;
154+
}
155+
156+
emailDetails.Add(dmarcEmailDetail);
157+
}
158+
159+
tempDmarcRecord.ForensicReportUri = [.. emailDetails];
160+
}
88161

89162
dmarcRecord = tempDmarcRecord;
90163
return true;
91164
}
92165

166+
private static bool TryGetAlignmentMode(
167+
string? input,
168+
[NotNullWhen(true)] out AlignmentMode? alignmentMode)
169+
{
170+
if (string.IsNullOrEmpty(input))
171+
{
172+
alignmentMode = null;
173+
return false;
174+
}
175+
176+
if (input.Equals("s", StringComparison.OrdinalIgnoreCase))
177+
{
178+
alignmentMode = AlignmentMode.Strict;
179+
return true;
180+
}
181+
182+
if (input.Equals("r", StringComparison.OrdinalIgnoreCase))
183+
{
184+
alignmentMode = AlignmentMode.Relaxed;
185+
return true;
186+
}
187+
188+
alignmentMode = null;
189+
return false;
190+
}
191+
192+
private static bool TryGetPolicyPercentage(
193+
string? input,
194+
[NotNullWhen(true)] out int? policyPercentage)
195+
{
196+
if (string.IsNullOrEmpty(input))
197+
{
198+
policyPercentage = 100;
199+
return true;
200+
}
201+
202+
if (!int.TryParse(input, out var tempPolicyPercentage))
203+
{
204+
policyPercentage = null;
205+
return false;
206+
}
207+
208+
if (int.IsNegative(tempPolicyPercentage))
209+
{
210+
policyPercentage = null;
211+
return false;
212+
}
213+
214+
if (tempPolicyPercentage > 100)
215+
{
216+
policyPercentage = null;
217+
return false;
218+
}
219+
220+
policyPercentage = tempPolicyPercentage;
221+
return true;
222+
}
223+
93224
private static bool TryGetReportingInterval(
94225
string? input,
95-
out TimeSpan reportingInterval)
226+
[NotNullWhen(true)] out TimeSpan? reportingInterval)
96227
{
97228
if (string.IsNullOrEmpty(input))
98229
{
@@ -102,13 +233,13 @@ private static bool TryGetReportingInterval(
102233

103234
if (!int.TryParse(input, out var intervalInSeconds))
104235
{
105-
reportingInterval = TimeSpan.Zero;
236+
reportingInterval = null;
106237
return false;
107238
}
108239

109240
if (int.IsNegative(intervalInSeconds))
110241
{
111-
reportingInterval = TimeSpan.Zero;
242+
reportingInterval = null;
112243
return false;
113244
}
114245

@@ -117,10 +248,10 @@ private static bool TryGetReportingInterval(
117248
}
118249

119250
private static bool TryGetDmarcPolicy(
120-
string policy,
251+
string input,
121252
[NotNullWhen(true)] out DmarcPolicy? dmarcPolicy)
122253
{
123-
if (Enum.TryParse(policy, true, out DmarcPolicy parsedPolicy))
254+
if (Enum.TryParse(input, true, out DmarcPolicy parsedPolicy))
124255
{
125256
dmarcPolicy = parsedPolicy;
126257
return true;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
namespace Nager.EmailAuthentication.Models
2+
{
3+
/// <summary>
4+
/// Defines the alignment mode for DMARC (Domain-based Message Authentication, Reporting, and Conformance).
5+
/// The alignment mode determines how strictly the domain in the "From" header must match the domains in SPF and DKIM.
6+
/// </summary>
7+
public enum AlignmentMode
8+
{
9+
/// <summary>
10+
/// The relaxed mode allows subdomains to pass alignment checks.
11+
/// </summary>
12+
Relaxed,
13+
14+
/// <summary>
15+
/// The strict mode requires an exact match between the domain in the "From" header and the authenticated domain.
16+
/// </summary>
17+
Strict
18+
}
19+
}

src/Nager.EmailAuthentication/Models/DmarcEmailDetail.cs

+8-15
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
namespace Nager.EmailAuthentication.Models
1+
using System.Diagnostics.CodeAnalysis;
2+
using System.Net.Mail;
3+
4+
namespace Nager.EmailAuthentication.Models
25
{
36
/// <summary>
47
/// Represents the details of a DMARC email address, including validation status and maximum size.
@@ -27,7 +30,9 @@ public class DmarcEmailDetail
2730
/// <param name="uriEmail"></param>
2831
/// <param name="dmarcEmailDetail"></param>
2932
/// <returns></returns>
30-
public static bool TryParse(string uriEmail, out DmarcEmailDetail? dmarcEmailDetail)
33+
public static bool TryParse(
34+
string uriEmail,
35+
[NotNullWhen(true)] out DmarcEmailDetail? dmarcEmailDetail)
3136
{
3237
if (string.IsNullOrEmpty(uriEmail))
3338
{
@@ -96,19 +101,7 @@ private static bool CheckIsValidEmailAddress(string emailAddress)
96101
return false;
97102
}
98103

99-
var indexOfAt = emailAddress.IndexOf('@');
100-
if (indexOfAt == -1)
101-
{
102-
return false;
103-
}
104-
105-
var indexOfDot = emailAddress.IndexOf('.', indexOfAt);
106-
if (indexOfDot == -1)
107-
{
108-
return false;
109-
}
110-
111-
return true;
104+
return MailAddress.TryCreate(emailAddress, out _);
112105
}
113106
}
114107
}

0 commit comments

Comments
 (0)