MailSharp / MailSharp.SMTP / Session / DataEnd.cs
Code · 121 lines · 4425 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
121using MailSharp.SMTP.Extensions;
using System.Net;
using System.Text;

namespace MailSharp.SMTP.Session;

public partial class SmtpSession
{
	// In HandleDataEndAsync, add DKIM verification
	private async Task HandleDataEndAsync(string[] parts, string line, CancellationToken ct)
	{
		if (state != SmtpState.DataStarted)
		{
			await writer.WriteLineAsync(configuration["SmtpResponses:BadSequence"], ct);
			return;
		}

		state = SmtpState.HeloReceived;

		// Gather connection info
		var remoteEndPoint = client.Client.RemoteEndPoint as IPEndPoint;
		string clientIp = remoteEndPoint?.Address.ToString() ?? "Unknown";
		int clientPort = remoteEndPoint?.Port ?? 0;
		string ptrRecord = "Unknown";
		try
		{
			var ptrResult = await Dns.GetHostEntryAsync(remoteEndPoint!.Address);
			ptrRecord = ptrResult.HostName;
		}
		catch
		{
			// PTR lookup failed, keep default
		}

		// Build .eml content
		StringBuilder emlContent = new();
		emlContent.AppendLine($"Received: from {clientIp} ({ptrRecord}) by {configuration["SmtpSettings:Host"]} with SMTP; {DateTime.UtcNow:ddd, dd MMM yyyy HH:mm:ss} +0000");
		emlContent.AppendLine($"From: {mailFrom}");
		emlContent.AppendLine($"To: {string.Join(", ", rcptTo)}");
		emlContent.AppendLine($"Date: {DateTime.UtcNow:ddd, dd MMM yyyy HH:mm:ss} +0000");
		emlContent.AppendLine($"Message-ID: <{Guid.NewGuid()}@{configuration["SmtpSettings:Host"]}>");
		emlContent.Append(data);

		// Verify DKIM signature
		//bool dkimValid = await dkimVerifier.VerifyDkimAsync(emlContent.ToString(), clientIp);
		//if (!dkimValid && configuration.GetValue<bool>("SmtpSettings:RequireDkim"))
		//{
		//	await writer.WriteLineAsync(configuration["SmtpResponses:DkimFailed"], ct);
		//	return;
		//}

		// Verify DMARC
		string mailFromDomain = ExtractDomain(mailFrom!);

		bool dmarcValid = await dmarcChecker.CheckDmarcAsync(emlContent.ToString(), clientIp, mailFromDomain, mailFromDomain, ct);
		if (!dmarcValid && configuration.GetValue<bool>("DmarcSettings:RequireDmarc"))
		{
			await writer.WriteLineAsync(configuration["SmtpResponses:DmarcFailed"], ct);
			return;
		}

		// Sign with DKIM
		//string selector = configuration[$"SmtpSettings:Dkim:{mailFromDomain}:Selector"] ?? "default";
		//string signedEml = dkimSigner.SignEmail(emlContent.ToString(), selector, mailFromDomain);

		string signedEml = emlContent.ToString();

		// Deliver .eml to each recipient's INBOX under {MailboxSettings:StoragePath}/{domain}/{user}/INBOX/
		string storagePath = configuration["MailboxSettings:StoragePath"] ?? throw new InvalidOperationException("MailboxSettings:StoragePath not configured");
		string messageId = Guid.NewGuid().ToString();
		foreach (string recipient in rcptTo)
		{
			string domain = ExtractDomain(recipient);
			string user = ExtractUser(recipient);
			string inboxPath = Path.Combine(storagePath, domain, user, "INBOX");
			Directory.CreateDirectory(inboxPath);
			string fileName = Path.Combine(inboxPath, $"{messageId}.eml");
			await File.WriteAllTextAsync(fileName, signedEml, ct);
			logger.LogInformation("Received email from {MailFrom} to {Recipient} saved as {FileName}", mailFrom, recipient, fileName);
		}

		await writer.WriteLineAsync(configuration["SmtpResponses:MessageAccepted"], ct);

		var rcptDomains = rcptTo.Select(r => ExtractDomain(r)).Where(d => d.Length > 0);
		metrics.MessageReceived(mailFromDomain, rcptDomains, signedEml.Length);

		mailFrom = null;
		rcptTo.Clear();
		data.Clear();
	}

	// Extracts the domain from any address format:
	//   user@domain.com            →  domain.com
	//   <user@domain.com>          →  domain.com
	//   Display Name <user@domain.com>  →  domain.com
	private static string ExtractDomain(string address)
	{
		int at = address.IndexOf('@');
		if (at < 0)
			return string.Empty;
		string after = address[(at + 1)..];
		int len = 0;
		while (len < after.Length && (char.IsLetterOrDigit(after[len]) || after[len] == '.' || after[len] == '-'))
			len++;
		return after[..len];
	}

	// Extracts the local part (user) from any address format:
	//   user@domain.com            →  user
	//   <user@domain.com>          →  user
	//   Display Name <user@domain.com>  →  user
	private static string ExtractUser(string address)
	{
		int at = address.IndexOf('@');
		if (at < 0)
			return string.Empty;
		int start = address.LastIndexOf('<', at);
		return start >= 0 ? address[(start + 1)..at] : address[..at];
	}

}