Skip to content

Commit a8d285a

Browse files
committed
Add spf data fragment parser
1 parent e1a8eb1 commit a8d285a

File tree

20 files changed

+545
-0
lines changed

20 files changed

+545
-0
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using Nager.EmailAuthentication.FragmentParsers;
2+
3+
namespace Nager.EmailAuthentication.UnitTest.SpfRecordParserTest.FragmentParser
4+
{
5+
[TestClass]
6+
public sealed class BasicTest
7+
{
8+
[TestMethod]
9+
public void Should_Parse_Spf_With_Include_And_All()
10+
{
11+
var spf = "v=spf1 include:spf.protection.outlook.com -all";
12+
var isSuccessful = SpfRecordDataFragmentParserV1.TryParse(spf, out var spfDataFragment);
13+
14+
Assert.IsTrue(isSuccessful);
15+
Assert.IsNotNull(spfDataFragment);
16+
Assert.IsNotNull(spfDataFragment.SpfTerms);
17+
Assert.AreEqual(2, spfDataFragment.SpfTerms.Length);
18+
}
19+
20+
[TestMethod]
21+
public void Should_Parse_Spf_With_Redirect()
22+
{
23+
var spf = "v=spf1 redirect=spf.provider.com";
24+
var isSuccessful = SpfRecordDataFragmentParserV1.TryParse(spf, out var spfDataFragment);
25+
26+
Assert.IsTrue(isSuccessful);
27+
Assert.IsNotNull(spfDataFragment);
28+
Assert.IsNotNull(spfDataFragment.SpfTerms);
29+
Assert.AreEqual(1, spfDataFragment.SpfTerms.Length);
30+
}
31+
32+
[TestMethod]
33+
public void Should_Parse_Spf_With_Multiple_Ip4_And_Include()
34+
{
35+
var spf = "v=spf1 ip4:155.56.66.96/30 ip4:155.56.66.102/31 ip4:155.56.66.104/32 ip4:155.56.66.106/32 ip4:155.56.68.128/26 include:spf.protection.outlook.com -all";
36+
var isSuccessful = SpfRecordDataFragmentParserV1.TryParse(spf, out var spfDataFragment);
37+
38+
Assert.IsTrue(isSuccessful);
39+
Assert.IsNotNull(spfDataFragment);
40+
Assert.IsNotNull(spfDataFragment.SpfTerms);
41+
Assert.AreEqual(7, spfDataFragment.SpfTerms.Length);
42+
}
43+
}
44+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
using Nager.EmailAuthentication.Models.Spf;
2+
using Nager.EmailAuthentication.Models.Spf.Mechanisms;
3+
using Nager.EmailAuthentication.Models.Spf.Modifiers;
4+
using System.Diagnostics.CodeAnalysis;
5+
6+
namespace Nager.EmailAuthentication.FragmentParsers
7+
{
8+
/// <summary>
9+
/// Spf Record Data Fragment Parser
10+
/// </summary>
11+
public static class SpfRecordDataFragmentParserV1
12+
{
13+
/// <summary>
14+
/// Try Parse
15+
/// </summary>
16+
/// <param name="spf"></param>
17+
/// <param name="spfDataFragment"></param>
18+
/// <returns></returns>
19+
public static bool TryParse(
20+
string spf,
21+
[NotNullWhen(true)] out SpfRecordDataFragmentV1? spfDataFragment)
22+
{
23+
var spfPrefix = "v=spf1 ";
24+
25+
if (!spf.StartsWith(spfPrefix, StringComparison.OrdinalIgnoreCase))
26+
{
27+
spfDataFragment = null;
28+
return false;
29+
}
30+
31+
var spfTerms = new List<SpfTerm>();
32+
var mechanismTypes = new Dictionary<string, Type>
33+
{
34+
{ AllMechanism.MechanismKey, typeof(AllMechanism) },
35+
{ Ip4Mechanism.MechanismKey, typeof(Ip4Mechanism) },
36+
{ Ip6Mechanism.MechanismKey, typeof(Ip6Mechanism) },
37+
{ IncludeMechanism.MechanismKey, typeof(IncludeMechanism) },
38+
{ ExistsMechanism.MechanismKey, typeof(ExistsMechanism) },
39+
{ AMechanism.MechanismKey, typeof(AMechanism) },
40+
{ MxMechanism.MechanismKey, typeof(MxMechanism) },
41+
{ PtrMechanism.MechanismKey, typeof(PtrMechanism) },
42+
};
43+
44+
var modifierTypes = new Dictionary<string, Type>
45+
{
46+
{ RedirectModifier.ModifierKey, typeof(RedirectModifier) },
47+
{ ExpModifier.ModifierKey, typeof(ExpModifier) }
48+
};
49+
50+
var spfTermDelimiter = ' ';
51+
var allowedQualifiers = new char[] { '+', '?', '~', '-' };
52+
var inputSpan = spf.AsSpan()[spfPrefix.Length..];
53+
var nextIndexOfDelimiter = 0;
54+
var termIndex = 0;
55+
56+
while (nextIndexOfDelimiter != -1)
57+
{
58+
termIndex++;
59+
60+
nextIndexOfDelimiter = inputSpan.IndexOf(spfTermDelimiter);
61+
if (nextIndexOfDelimiter == 0)
62+
{
63+
inputSpan = inputSpan[1..];
64+
continue;
65+
}
66+
67+
ReadOnlySpan<char> value;
68+
if (nextIndexOfDelimiter == -1)
69+
{
70+
value = inputSpan.Trim();
71+
}
72+
else
73+
{
74+
value = inputSpan[..nextIndexOfDelimiter].Trim();
75+
}
76+
77+
var qualifier = '+';
78+
var indexOfQualifier = Array.IndexOf(allowedQualifiers, value[0]);
79+
if (indexOfQualifier != -1)
80+
{
81+
qualifier = allowedQualifiers[indexOfQualifier];
82+
value = value[1..];
83+
}
84+
85+
foreach (var mechanismType in mechanismTypes)
86+
{
87+
if (!value.StartsWith(mechanismType.Key))
88+
{
89+
continue;
90+
}
91+
92+
var term = Activator.CreateInstance(mechanismType.Value) as SpfTerm;
93+
if (term is null)
94+
{
95+
continue;
96+
}
97+
98+
term.Index = termIndex;
99+
100+
if (term is SpfMechanismBase spfMechanism)
101+
{
102+
spfMechanism.SetQualifier(qualifier);
103+
104+
value = value[mechanismType.Key.Length..];
105+
if (value.Length > 0)
106+
{
107+
spfMechanism.GetDataPart(value);
108+
}
109+
110+
spfTerms.Add(spfMechanism);
111+
break;
112+
}
113+
}
114+
115+
foreach (var modifierType in modifierTypes)
116+
{
117+
if (!value.StartsWith(modifierType.Key))
118+
{
119+
continue;
120+
}
121+
122+
var term = Activator.CreateInstance(modifierType.Value) as SpfTerm;
123+
if (term is null)
124+
{
125+
continue;
126+
}
127+
128+
term.Index = termIndex;
129+
130+
if (term is SpfModifierBase spfModifier)
131+
{
132+
value = value[modifierType.Key.Length..];
133+
if (value.Length > 0)
134+
{
135+
spfModifier.GetDataPart(value);
136+
}
137+
138+
spfTerms.Add(spfModifier);
139+
break;
140+
}
141+
}
142+
143+
inputSpan = inputSpan[(nextIndexOfDelimiter + 1)..];
144+
}
145+
146+
spfDataFragment = new SpfRecordDataFragmentV1
147+
{
148+
Version = "1",
149+
SpfTerms = [.. spfTerms]
150+
};
151+
152+
return true;
153+
}
154+
}
155+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
namespace Nager.EmailAuthentication.Models.Spf
2+
{
3+
/// <summary>
4+
/// Represents the different types of mechanisms used in SPF (Sender Policy Framework) records.
5+
/// These mechanisms define the rules for which servers are authorized to send emails on behalf of a domain.
6+
/// </summary>
7+
public enum MechanismType
8+
{
9+
/// <summary>
10+
/// Represents an IPv4 address mechanism (`ip4`).
11+
/// </summary>
12+
Ip4,
13+
14+
/// <summary>
15+
/// Represents an IPv6 address mechanism (`ip6`).
16+
/// </summary>
17+
Ip6,
18+
19+
/// <summary>
20+
/// Represents an "A" record mechanism (`a`), used to authorize servers based on the domain's A or AAAA records.
21+
/// </summary>
22+
A,
23+
24+
/// <summary>
25+
/// Represents a Mail Exchange (MX) record mechanism (`mx`), used to authorize servers based on the domain's MX records.
26+
/// </summary>
27+
Mx,
28+
29+
/// <summary>
30+
/// Represents a PTR record mechanism (`ptr`), used to authorize servers based on reverse DNS lookups.
31+
/// </summary>
32+
Ptr,
33+
34+
/// <summary>
35+
/// Represents an `include` mechanism, used to include the SPF record of another domain.
36+
/// </summary>
37+
Include,
38+
39+
/// <summary>
40+
/// Represents the `exists` mechanism, used to authorize servers based on the existence of a DNS record for a domain.
41+
/// </summary>
42+
Exists,
43+
44+
/// <summary>
45+
/// Represents the `all` mechanism, typically used at the end of the SPF record to define a catch-all rule for all other IPs.
46+
/// </summary>
47+
All
48+
}
49+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace Nager.EmailAuthentication.Models.Spf.Mechanisms
2+
{
3+
public class AMechanism : SpfMechanismBase
4+
{
5+
public const string MechanismKey = "a";
6+
7+
public AMechanism() : base(MechanismType.A)
8+
{
9+
10+
}
11+
}
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace Nager.EmailAuthentication.Models.Spf.Mechanisms
2+
{
3+
public class AllMechanism : SpfMechanismBase
4+
{
5+
public const string MechanismKey = "all";
6+
7+
public AllMechanism() : base(MechanismType.All)
8+
{
9+
10+
}
11+
}
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace Nager.EmailAuthentication.Models.Spf.Mechanisms
2+
{
3+
public class ExistsMechanism : SpfMechanismBase
4+
{
5+
public const string MechanismKey = "exists";
6+
7+
public ExistsMechanism() : base(MechanismType.Exists)
8+
{
9+
10+
}
11+
}
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace Nager.EmailAuthentication.Models.Spf.Mechanisms
2+
{
3+
public class IncludeMechanism : SpfMechanismBase
4+
{
5+
public const string MechanismKey = "include";
6+
7+
public IncludeMechanism() : base(MechanismType.Include)
8+
{
9+
10+
}
11+
}
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace Nager.EmailAuthentication.Models.Spf.Mechanisms
2+
{
3+
public class Ip4Mechanism : SpfMechanismBase
4+
{
5+
public const string MechanismKey = "ip4";
6+
7+
public Ip4Mechanism() : base(MechanismType.Ip4)
8+
{
9+
10+
}
11+
}
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace Nager.EmailAuthentication.Models.Spf.Mechanisms
2+
{
3+
public class Ip6Mechanism : SpfMechanismBase
4+
{
5+
public const string MechanismKey = "ip6";
6+
7+
public Ip6Mechanism() : base(MechanismType.Ip6)
8+
{
9+
10+
}
11+
}
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace Nager.EmailAuthentication.Models.Spf.Mechanisms
2+
{
3+
public class MxMechanism : SpfMechanismBase
4+
{
5+
public const string MechanismKey = "mx";
6+
7+
public MxMechanism() : base(MechanismType.Mx)
8+
{
9+
10+
}
11+
}
12+
}

0 commit comments

Comments
 (0)