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");
}
}