MailSharp / MailSharp.SMTP / Services / RelayService.cs
Code · 128 lines · 3624 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
128using MailSharp.SMTP.Metrics;
using System.Net;
using System.Net.Mail;
using System.Net.Sockets;

namespace MailSharp.SMTP.Services;

public class RelayService(IConfiguration configuration, SmtpMetrics metrics, ILogger<RelayService> logger) : BackgroundService
{
	private readonly Queue<string> queue = new();

	public void Enqueue(string emlPath)
	{
		queue.Enqueue(emlPath);
	}

	protected override async Task ExecuteAsync(CancellationToken stoppingToken)
	{
		string relayQueuePath = configuration["SmtpSettings:RelayQueuePath"] ?? throw new InvalidOperationException("RelayQueuePath not configured");

		if (Directory.Exists(relayQueuePath))
		{
			foreach (string file in Directory.GetFiles(relayQueuePath, "*.eml"))
			{
				queue.Enqueue(file);
			}
		}

		while (!stoppingToken.IsCancellationRequested)
		{
			if (queue.TryDequeue(out string? emlPath) && File.Exists(emlPath))
			{
				await ProcessEmailAsync(emlPath, stoppingToken);
			}
			try
			{
				await Task.Delay(1000, stoppingToken);
			}
			catch (OperationCanceledException)
			{
				break;
			}
		}
	}

	private async Task ProcessEmailAsync(string emlPath, CancellationToken ct)
	{
		try
		{
			// Use CancellationToken.None so an in-progress relay completes cleanly on shutdown
			string emlContent = await File.ReadAllTextAsync(emlPath, CancellationToken.None);
			string recipient = ExtractRecipient(emlContent);
			string domain = recipient[(recipient.IndexOf('@') + 1)..];

			// MX record lookup using System.Net.Dns
			IPHostEntry? dnsEntry = null;
			try
			{
				dnsEntry = await Dns.GetHostEntryAsync(domain, ct);
			}
			catch (SocketException)
			{
				logger.LogWarning("No MX records found for domain {Domain}", domain);
				return;
			}

			// Extract MX records from aliases (simplified, not ideal)
			string[] mxRecords = [.. dnsEntry.Aliases
				.Where(a => a.Contains("mail exchanger", StringComparison.OrdinalIgnoreCase))
				.Select(a => a.Split(' ').Last())];

			if (mxRecords.Length == 0)
			{
				logger.LogWarning("No valid MX records found for domain {Domain}", domain);
				return;
			}

			foreach (string mxServer in mxRecords)
			{
				try
				{
					using var client = new SmtpClient(mxServer)
					{
						EnableSsl = configuration.GetValue<bool>("SmtpSettings:RelayUseTls"),
						Timeout = configuration.GetValue<int>("SmtpSettings:RelayTimeoutSeconds") * 1000
					};

					if (configuration.GetValue<bool>("SmtpSettings:RelayRequiresAuth"))
					{
						client.Credentials = new NetworkCredential(
							configuration["SmtpSettings:RelayUsername"],
							configuration["SmtpSettings:RelayPassword"]);
					}

					await client.SendMailAsync(new MailMessage
					{
						Body = emlContent,
						To = { recipient }
					}, CancellationToken.None);

					metrics.IncrementRelayed();

					File.Delete(emlPath);
					logger.LogInformation("Relayed email to {Domain} via {MxServer}", domain, mxServer);
					return;
				}
				catch (SmtpException ex)
				{
					logger.LogWarning("Failed to relay to {MxServer}: {Error}", mxServer, ex.Message);
					continue;
				}
			}

			logger.LogError("Failed to relay email to {Domain}: No reachable MX servers", domain);
		}
		catch (Exception ex)
		{
			logger.LogError("Error processing email {Path}: {Error}", emlPath, ex.Message);
		}
	}

	private static string ExtractRecipient(string emlContent)
	{
		string[] lines = emlContent.Split("\r\n");
		string? toLine = lines.FirstOrDefault(l => l.StartsWith("To:", StringComparison.OrdinalIgnoreCase));
		return toLine?[(toLine.IndexOf(':') + 1)..].Trim() ?? throw new InvalidOperationException("No recipient found");
	}
}