MailSharp / MailSharp.SMTP / Services / SpfChecker.cs
Code · 115 lines · 3132 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
115using MailSharp.DNS;
using System.Net;

namespace MailSharp.SMTP.Services;

public class SpfChecker(IConfiguration configuration, Resolver resolver)
{
	public async Task<bool> CheckSpfAsync(string clientIp, string mailFromDomain, string heloDomain, CancellationToken cancellationToken = default)
	{
		string spfRecord = await ResolveSpfRecordAsync(mailFromDomain, cancellationToken)
			?? await ResolveSpfRecordAsync(heloDomain, cancellationToken)
			?? string.Empty;

		if (string.IsNullOrEmpty(spfRecord))
			return false;

		return await EvaluateSpfRecordAsync(spfRecord, clientIp, mailFromDomain, cancellationToken);
	}

	private async Task<string?> ResolveSpfRecordAsync(string domain, CancellationToken cancellationToken)
	{
		try
		{
			var dnsServer = GetDnsServer();
			var response = await resolver.QueryAsync(dnsServer, domain, DnsQType.TXT, DnsQClass.IN, cancellationToken);
			return response.RecordsTXT
				.SelectMany(r => r.Texts)
				.FirstOrDefault(t => t.StartsWith("v=spf1", StringComparison.OrdinalIgnoreCase));
		}
		catch
		{
			return null;
		}
	}

	private async Task<bool> EvaluateSpfRecordAsync(string spfRecord, string clientIp, string mailFromDomain, CancellationToken cancellationToken)
	{
		if (!spfRecord.StartsWith("v=spf1", StringComparison.OrdinalIgnoreCase))
			return false;

		string[] mechanisms = spfRecord.Split(' ', StringSplitOptions.RemoveEmptyEntries);

		foreach (string mechanism in mechanisms.Skip(1))
		{
			if (mechanism.StartsWith("ip4:"))
			{
				if (IsIpInRange(clientIp, mechanism[4..]))
					return true;
			}
			else if (mechanism.StartsWith("include:"))
			{
				string includeDomain = mechanism[8..];
				string? includeSpf = await ResolveSpfRecordAsync(includeDomain, cancellationToken);
				if (includeSpf != null && await EvaluateSpfRecordAsync(includeSpf, clientIp, mailFromDomain, cancellationToken))
					return true;
			}
			else if (mechanism == "-all")
			{
				return false;
			}
			else if (mechanism == "+all")
			{
				return true;
			}
		}

		return false;
	}

	private static bool IsIpInRange(string clientIp, string ipRange)
	{
		try
		{
			if (ipRange.Contains('/'))
			{
				var parts = ipRange.Split('/');
				var networkAddress = IPAddress.Parse(parts[0]);
				var cidr = int.Parse(parts[1]);
				var clientAddress = IPAddress.Parse(clientIp);

				byte[] networkBytes = networkAddress.GetAddressBytes();
				byte[] clientBytes = clientAddress.GetAddressBytes();
				int maskBytes = cidr / 8;
				int maskRemainder = cidr % 8;

				for (int i = 0; i < maskBytes; i++)
				{
					if (networkBytes[i] != clientBytes[i])
						return false;
				}

				if (maskRemainder > 0)
				{
					byte mask = (byte)(0xFF << (8 - maskRemainder));
					return (networkBytes[maskBytes] & mask) == (clientBytes[maskBytes] & mask);
				}

				return true;
			}

			return clientIp == ipRange;
		}
		catch
		{
			return false;
		}
	}

	private IPEndPoint GetDnsServer()
	{
		string host = configuration["SpfSettings:DnsServer"] ?? "8.8.8.8";
		int port = configuration.GetValue<int?>("SpfSettings:DnsPort") ?? 53;
		return new IPEndPoint(IPAddress.Parse(host), port);
	}
}