diff --git a/smtpproxy.net/SMTPProxy.cs b/smtpproxy.net/SMTPProxy.cs index 6eaef41..f993a48 100644 --- a/smtpproxy.net/SMTPProxy.cs +++ b/smtpproxy.net/SMTPProxy.cs @@ -1,564 +1,678 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.ComponentModel; -using System.Configuration; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.ServiceProcess; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading; -using Newtonsoft.Json.Linq; - -namespace smtpproxy.net; - -public class SMTPProxy : ServiceBase -{ - private ConcurrentQueue EmailQueue; - - private ManualResetEvent mre; - - private bool bRunning; - - private Thread Sending; - - private Thread Listening; - - private long ThreadCount; - - private bool ConsoleMode; - - private static readonly object _syncObject = new object(); - - private IContainer components; - - public SMTPProxy() - { - InitializeComponent(); - base.ServiceName = "SMTPProxy.net"; - ConsoleMode = false; - EmailQueue = new ConcurrentQueue(); - mre = new ManualResetEvent(initialState: false); - bRunning = true; - ThreadCount = 0L; - } - - protected override void OnStart(string[] args) - { - Listening = new Thread(StartListening); - Listening.Start(); - Sending = new Thread(StartSending); - Sending.Start(); - } - - public void Start() - { - ConsoleMode = true; - Listening = new Thread(StartListening); - Listening.Start(); - Sending = new Thread(StartSending); - Sending.Start(); - } - - protected override void OnStop() - { - bRunning = false; - mre.Set(); - } - - public void Reload(string Folder) - { - foreach (string item in Directory.EnumerateFiles(Folder, "*.eml").ToList()) - { - Email email = new Email(); - email.sDATA = File.ReadAllText(item); - email.sHELO = File.ReadAllText(item.Replace(".eml", ".HELO")); - email.sMAIL = File.ReadAllText(item.Replace(".eml", ".MAIL")); - foreach (string item2 in File.ReadAllLines(item.Replace(".eml", ".RCPTS")).ToList()) - { - if (item2.Length > 0) - { - email.sRCPTs.Add(item2 + "\r\n"); - } - } - email.FromIP = "127.0.0.1"; - EmailQueue.Enqueue(email); - } - SendQueuedEmails(); - } - - public void StartSending() - { - while (!mre.WaitOne(30000) && bRunning) - { - SendQueuedEmails(); - } - } - - public void StartListening() - { - Dns.GetHostEntry(Dns.GetHostName()); - IPAddress iPAddress = IPAddress.Parse(ConfigurationManager.AppSettings["ListeningAddress"]); - IPEndPoint localEP = new IPEndPoint(iPAddress, 25); - Socket socket = new Socket(iPAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp); - Log("Started Listening on port 25 at " + iPAddress.ToString()); - socket.Bind(localEP); - socket.Listen(100); - while (true) - { - try - { - Socket socket2 = socket.Accept(); - if (socket2.Connected) - { - Log("New Connection:" + DateTime.Now.ToString("G", CultureInfo.CreateSpecificCulture("en-AU"))); - ThreadPool.QueueUserWorkItem(ProcessMailClient, socket2); - } - } - catch (SocketException ex) - { - Log($"SocketException : {ex.ToString()}"); - } - catch (Exception ex2) - { - Log($"Unexpected exception : {ex2.ToString()}"); - } - } - } - - private void ProcessMailClient(object stateinfo) - { - Socket socket = (Socket)stateinfo; - socket.ReceiveTimeout = 180000; - socket.SendTimeout = 180000; - byte[] bytes = Encoding.ASCII.GetBytes("220 Connected to SBC SMTPProxy.net\r\n"); - socket.Send(bytes); - IPEndPoint iPEndPoint = socket.RemoteEndPoint as IPEndPoint; - string text = ConfigurationManager.AppSettings["LogFolder"] + "in"; - Directory.CreateDirectory(text); - string logFileName = $"{text}\\{Guid.NewGuid()}_{iPEndPoint.Address}.log"; - Log("New Client Connected from " + iPEndPoint.Address, logFileName); - new List(); - List list = new List(); - byte[] array = new byte[2000]; - StringBuilder stringBuilder = new StringBuilder(); - string text2 = ""; - string text3 = ""; - string text4 = ""; - string text5 = ""; - string text6 = ""; - int num = 0; - bool flag = false; - do - { - try - { - num = socket.Receive(array, 1999, SocketFlags.None); - if (num > 0) - { - array[num] = 0; - string text7 = Encoding.Default.GetString(array, 0, num); - text3 = text2 + text7; - text2 = text7; - stringBuilder.Append(text7); - if (num == 1999 || !text3.Contains("\r\n")) - { - continue; - } - string text8 = stringBuilder.ToString(); - string text9 = ((text8.Length < 4) ? text8 : text8.Substring(0, 4)); - string text10 = "500 Invalid Command\r\n"; - bool flag2 = false; - if (flag) - { - text6 += text8; - if (text8 == ".\r\n" || text8.Contains("\r\n.\r\n")) - { - flag = false; - text10 = "250 OK\r\n"; - Email email = new Email(); - email.sHELO = text4; - email.sMAIL = text5; - email.sRCPTs = new List(list); - email.sDATA = text6; - email.FromIP = iPEndPoint.Address.ToString(); - EmailQueue.Enqueue(email); - text5 = (text6 = ""); - list.Clear(); - } - } - else - { - switch (text9) - { - case "HELO": - case "EHLO": - text4 = stringBuilder.ToString(); - text10 = "250 Hi\r\n"; - break; - case "MAIL": - if (text4.Length == 0) - { - text10 = "503 Do HELO first\r\n"; - break; - } - if (text5.Length > 0) - { - text10 = "503 Already have MAIL\r\n"; - break; - } - text5 = stringBuilder.ToString(); - text10 = "250 MAIL command recorded\r\n"; - break; - case "RCPT": - if (text4.Length == 0) - { - text10 = "503 Do HELO first\r\n"; - break; - } - if (text5.Length == 0) - { - text10 = "503 Do MAIL first\r\n"; - break; - } - list.Add(stringBuilder.ToString()); - text10 = "250 RCPT command recorded\r\n"; - break; - case "DATA": - if (text4.Length == 0) - { - text10 = "503 Do HELO first\r\n"; - } - else if (text5.Length == 0) - { - text10 = "503 Do MAIL first\r\n"; - } - else if (list.Count == 0) - { - text10 = "503 Do RCPT first\r\n"; - } - text10 = "354 DATA\r\n"; - flag2 = true; - break; - case "RSET": - text10 = "250 OK\r\n"; - text5 = (text6 = ""); - list.Clear(); - break; - case "QUIT": - socket.Shutdown(SocketShutdown.Both); - socket.Close(); - num = 0; - goto end_IL_00ed; - default: - Log("Unknown:" + text9, logFileName); - break; - } - } - if (!flag) - { - byte[] bytes2 = Encoding.ASCII.GetBytes(text10); - socket.Send(bytes2); - Log(text10, logFileName); - } - if (flag2) - { - flag = true; - } - stringBuilder.Clear(); - array[0] = 0; - continue; - } - if (num == 0) - { - Log("Client disconnected.", logFileName); - continue; - } - Log("Client reset the connection.", logFileName); - socket.Shutdown(SocketShutdown.Both); - socket.Close(); - num = 0; - end_IL_00ed:; - } - catch (SocketException ex) - { - Log($"SocketException : {ex.ToString()}", logFileName); - num = 0; - } - catch (Exception ex2) - { - Log($"Unexpected exception : {ex2.ToString()}", logFileName); - num = 0; - } - } - while (socket.Connected && num > 0); - if (EmailQueue.Count > 5 && Interlocked.Read(ref ThreadCount) < Convert.ToUInt32(ConfigurationManager.AppSettings["ThreadMax"])) - { - Interlocked.Increment(ref ThreadCount); - SendQueuedEmails(); - Interlocked.Decrement(ref ThreadCount); - } - } - - private void Log(string info, string LogFileName = null) - { - if (ConsoleMode) - { - Console.WriteLine(info.Replace("\r\n", "")); - } - if (!string.IsNullOrEmpty(LogFileName)) - { - using (StreamWriter streamWriter = File.AppendText(LogFileName)) - { - streamWriter.WriteLine("{0}", info); - streamWriter.Flush(); - return; - } - } - lock (_syncObject) - { - DateTime now = DateTime.Now; - using StreamWriter streamWriter2 = File.AppendText(string.Format("{0}\\{1:00}{2:00}{3:00}.log", ConfigurationManager.AppSettings["LogFolder"] + "in", now.Year, now.Month, now.Day)); - streamWriter2.WriteLine("{0}", info); - streamWriter2.Flush(); - } - } - - private void SendQueuedEmails() - { - JToken val2 = default(JToken); - while (!EmailQueue.IsEmpty) - { - if (!EmailQueue.TryDequeue(out var result)) - { - continue; - } - string sMAIL = result.sMAIL; - sMAIL = sMAIL.Replace(" ", ""); - sMAIL = sMAIL.Replace("MAILFROM:", ""); - sMAIL = sMAIL.Replace("<", ""); - sMAIL = sMAIL.Replace(">", ""); - sMAIL = sMAIL.Replace("\r", ""); - sMAIL = sMAIL.Replace("\n", ""); - string path = ConfigurationManager.AppSettings["Profiles"] + result.FromIP + "\\" + sMAIL + ".json"; - if (!File.Exists(path)) - { - int num = sMAIL.IndexOf('@'); - string text = sMAIL.Substring(num + 1); - path = ConfigurationManager.AppSettings["Profiles"] + result.FromIP + "\\default@" + text + ".json"; - if (!File.Exists(path)) - { - path = ConfigurationManager.AppSettings["Profiles"] + result.FromIP + "\\default.json"; - } - } - JObject val; - try - { - val = JObject.Parse(File.ReadAllText(path)); - } - catch (Exception ex) - { - Log(ex.Message); - continue; - } - int num2 = Convert.ToInt32(val["logging"][(object)"enabled"]); - bool bHideRecipients = Convert.ToInt32(val["HideRecipients"]) == 1; - bool num3 = num2 == 1; - string text2 = ""; - string text3 = ((object)val["logging"][(object)"path"]).ToString() + "\\log"; - Directory.CreateDirectory(text3); - Guid guid = Guid.NewGuid(); - string recipientEmail = result.GetRecipientEmail(result.sRCPTs[0]); - text2 = $"{text3}\\{recipientEmail}.{guid}.log"; - string text4 = ((object)val["logging"][(object)"path"]).ToString() + "\\out"; - Directory.CreateDirectory(text4); - if (num3) - { - File.WriteAllText($"{text4}\\{recipientEmail}.{guid}.eml", result.sDATA); - File.WriteAllText($"{text4}\\{recipientEmail}.{guid}.HELO", result.sHELO); - File.WriteAllText($"{text4}\\{recipientEmail}.{guid}.MAIL", result.sMAIL); - File.WriteAllLines($"{text4}\\{recipientEmail}.{guid}.RCPTS", result.sRCPTs); - } - IPAddress iPAddress = Dns.GetHostEntry(((object)val["smtpserver"]).ToString()).AddressList[0]; - IPEndPoint remoteEP = new IPEndPoint(iPAddress, Convert.ToInt32(((object)val["smtpserverport"]).ToString())); - int num4 = Convert.ToInt32(val["recipientreplacement"][(object)"enabled"]); - List list = val["recipientreplacement"][(object)"replacements"].ToObject>(); - int num5 = Convert.ToInt32(val["recipientfilter"][(object)"enabled"]); - List list2 = val["recipientfilter"][(object)"whitelist"].ToObject>(); - bool flag = false; - List list3 = null; - if (val.TryGetValue("bcc", ref val2)) - { - flag = Extensions.Value((IEnumerable)val2[(object)"enabled"]) == 1; - list3 = val2[(object)"recipients"].ToObject>(); - } - Socket socket = null; - bool flag2 = num4 == 1; - bool flag3 = num5 == 1; - try - { - socket = new Socket(iPAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp); - socket.Connect(remoteEP); - if (!CheckAndSend("220", result.sHELO, socket, text2) || !CheckAndSend("250", result.sMAIL, socket, text2)) - { - continue; - } - if (flag2) - { - foreach (string item in list) - { - string nextCommand = $"RCPT TO: {item}\r\n"; - CheckAndSend("250", nextCommand, socket, text2); - } - goto IL_053d; - } - int num6 = 0; - foreach (string sRCPT in result.sRCPTs) - { - if (sRCPT.Length < 8) - { - continue; - } - if (flag3) - { - string r = result.GetRecipientEmail(sRCPT); - if (!list2.Exists((string prd) => prd.Equals(r))) - { - Log("Filtering Recipient " + r, text2); - continue; - } - Log("Recipient " + r + " OK", text2); - } - if (CheckAndSend("250", sRCPT, socket, text2)) - { - num6++; - } - } - if (num6 != 0) - { - goto IL_053d; - } - Log("No Valid recipients, aborting", text2); - socket.Shutdown(SocketShutdown.Both); - socket.Close(); - goto end_IL_03a8; - IL_053d: - if (flag) - { - foreach (string item2 in list3) - { - string nextCommand2 = $"RCPT TO: {item2}\r\n"; - CheckAndSend("250", nextCommand2, socket, text2); - } - } - if (CheckAndSend("250", "DATA\r\n", socket, text2) && CheckAndSend("354", CleanseDataBlock(result.sDATA, bHideRecipients), socket, text2) && CheckAndSend("250", "RSET\r\n", socket, text2) && CheckAndSend("250", "QUIT\r\n", socket, text2)) - { - socket.Shutdown(SocketShutdown.Both); - socket.Close(); - } - end_IL_03a8:; - } - catch (ArgumentNullException ex2) - { - Log($"ArgumentNullException : {ex2.ToString()}", text2); - } - catch (SocketException ex3) - { - Log($"SocketException : {ex3.ToString()}", text2); - } - catch (Exception ex4) - { - Log($"Unexpected exception : {ex4.ToString()}", text2); - } - } - } - - private static string CleanseDataBlock(string sData, bool bHideRecipients) - { - if (bHideRecipients) - { - StringBuilder stringBuilder = new StringBuilder(); - using StringReader stringReader = new StringReader(sData); - string pattern = "^[Tt][Oo]: *$"; - string pattern2 = "^[Dd][Aa][Tt][Ee]:.*$"; - bool flag = false; - bool flag2 = false; - for (string text = stringReader.ReadLine(); text != null; text = stringReader.ReadLine()) - { - if (flag2) - { - stringBuilder.Append(text + "\r\n"); - } - else if (!flag) - { - if (Regex.Match(text, pattern).Success) - { - flag = true; - } - stringBuilder.Append(text + "\r\n"); - } - else if (Regex.Match(text, pattern2).Success) - { - flag = false; - stringBuilder.Append(text + "\r\n"); - flag2 = true; - } - } - return stringBuilder.ToString(); - } - return sData; - } - - private bool CheckAndSend(string PrevSuccessCode, string NextCommand, Socket sock, string LogFileName) - { - byte[] array = new byte[2500]; - string text = ""; - int num = sock.Receive(array); - if (num <= 0) - { - Log("recv failed", LogFileName); - sock.Shutdown(SocketShutdown.Both); - sock.Close(); - return false; - } - array[num] = 0; - text = Encoding.ASCII.GetString(array, 0, num); - Log(text, LogFileName); - if (text.Substring(0, 3) != PrevSuccessCode) - { - sock.Shutdown(SocketShutdown.Both); - sock.Close(); - return false; - } - if (NextCommand.Length > 500) - { - Log(NextCommand.Substring(0, 500), LogFileName); - } - else - { - Log(NextCommand, LogFileName); - } - byte[] bytes = Encoding.ASCII.GetBytes(NextCommand); - sock.Send(bytes); - return true; - } - - protected override void Dispose(bool disposing) - { - if (disposing && components != null) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - private void InitializeComponent() - { - components = new Container(); - base.ServiceName = "Service1"; - } -} +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.ComponentModel; +using System.Configuration; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.ServiceProcess; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using Newtonsoft.Json.Linq; + +namespace smtpproxy.net +{ + public class SMTPProxy : ServiceBase + { + private ConcurrentQueue EmailQueue; + private ManualResetEvent mre; + private bool bRunning; + private Thread Sending; + private Thread Listening; + private long ThreadCount; + private bool ConsoleMode; + private static readonly object _syncObject = new object(); + private IContainer components; + + public SMTPProxy() + { + InitializeComponent(); + base.ServiceName = "SMTPProxy.net"; + ConsoleMode = false; + EmailQueue = new ConcurrentQueue(); + mre = new ManualResetEvent(false); + bRunning = true; + ThreadCount = 0L; + } + + protected override void OnStart(string[] args) + { + Listening = new Thread(StartListening) { IsBackground = true }; + Listening.Start(); + Sending = new Thread(StartSending) { IsBackground = true }; + Sending.Start(); + } + + public void Start() + { + ConsoleMode = true; + Listening = new Thread(StartListening) { IsBackground = true }; + Listening.Start(); + Sending = new Thread(StartSending) { IsBackground = true }; + Sending.Start(); + } + + protected override void OnStop() + { + bRunning = false; + mre.Set(); + } + + public void Reload(string folder) + { + if (!Directory.Exists(folder)) return; + + foreach (string item in Directory.EnumerateFiles(folder, "*.eml")) + { + try + { + Email email = new Email + { + sDATA = File.ReadAllText(item), + sHELO = File.Exists(item.Replace(".eml", ".HELO")) ? File.ReadAllText(item.Replace(".eml", ".HELO")) : "HELO localhost\r\n", + sMAIL = File.Exists(item.Replace(".eml", ".MAIL")) ? File.ReadAllText(item.Replace(".eml", ".MAIL")) : "", + FromIP = "127.0.0.1" + }; + + string rcptFile = item.Replace(".eml", ".RCPTS"); + if (File.Exists(rcptFile)) + { + foreach (string rcpt in File.ReadAllLines(rcptFile)) + { + if (!string.IsNullOrWhiteSpace(rcpt)) + { + email.sRCPTs.Add(rcpt.Trim() + "\r\n"); + } + } + } + + EmailQueue.Enqueue(email); + } + catch (Exception ex) + { + Log($"Failed to reload email file {item}: {ex.Message}"); + } + } + SendQueuedEmails(); + } + + public void StartSending() + { + while (!mre.WaitOne(30000) && bRunning) + { + SendQueuedEmails(); + } + } + + public void StartListening() + { + try + { + string listenSetting = ConfigurationManager.AppSettings["ListeningAddress"] ?? "0.0.0.0"; + IPAddress ipAddress = IPAddress.Parse(listenSetting); + IPEndPoint localEP = new IPEndPoint(ipAddress, 25); + + Socket socket = new Socket(ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + socket.Bind(localEP); + socket.Listen(100); + + Log($"Started Listening on port 25 at {ipAddress}"); + + while (bRunning) + { + try + { + Socket clientSocket = socket.Accept(); + if (clientSocket.Connected) + { + Log($"New Connection: {DateTime.Now.ToString("G", CultureInfo.CreateSpecificCulture("en-AU"))}"); + ThreadPool.QueueUserWorkItem(ProcessMailClient, clientSocket); + } + } + catch (SocketException ex) + { + if (!bRunning) break; + Log($"SocketException accepting client: {ex.Message}"); + } + catch (Exception ex) + { + Log($"Unexpected exception accepting client: {ex.Message}"); + } + } + } + catch (Exception ex) + { + Log($"Fatal listening error: {ex}"); + } + } + + private void ProcessMailClient(object stateInfo) + { + Socket socket = (Socket)stateInfo; + socket.ReceiveTimeout = 180000; + socket.SendTimeout = 180000; + + IPEndPoint remoteEndPoint = socket.RemoteEndPoint as IPEndPoint; + string clientIp = remoteEndPoint?.Address.ToString() ?? "0.0.0.0"; + + string logDir = Path.Combine(ConfigurationManager.AppSettings["LogFolder"] ?? "C:\\Logs\\", "in"); + Directory.CreateDirectory(logDir); + string logFileName = Path.Combine(logDir, $"{Guid.NewGuid()}_{clientIp}.log"); + + Log($"New Client Connected from {clientIp}", logFileName); + + byte[] welcome = Encoding.ASCII.GetBytes("220 Connected to SBC SMTPProxy.net\r\n"); + socket.Send(welcome); + + List rcptList = new List(); + byte[] buffer = new byte[4096]; + StringBuilder commandBuffer = new StringBuilder(); + + string heloHeader = ""; + string mailFrom = ""; + string dataBody = ""; + bool receivingData = false; + + try + { + while (socket.Connected) + { + int bytesRead = socket.Receive(buffer, SocketFlags.None); + if (bytesRead == 0) + { + Log("Client disconnected gracefully.", logFileName); + break; + } + + string chunk = Encoding.Default.GetString(buffer, 0, bytesRead); + commandBuffer.Append(chunk); + + if (receivingData) + { + dataBody += chunk; + if (dataBody.EndsWith("\r\n.\r\n") || dataBody == ".\r\n") + { + receivingData = false; + + Email email = new Email + { + sHELO = heloHeader, + sMAIL = mailFrom, + sRCPTs = new List(rcptList), + sDATA = dataBody, + FromIP = clientIp + }; + + EmailQueue.Enqueue(email); + Log("Queued inbound message.", logFileName); + + mailFrom = ""; + dataBody = ""; + rcptList.Clear(); + commandBuffer.Clear(); + + byte[] okBytes = Encoding.ASCII.GetBytes("250 OK\r\n"); + socket.Send(okBytes); + Log("250 OK", logFileName); + } + continue; + } + + // Process complete commands ending in newline + string fullBuffer = commandBuffer.ToString(); + if (!fullBuffer.Contains("\r\n")) + { + continue; + } + + string[] lines = fullBuffer.Split(new[] { "\r\n" }, StringSplitOptions.None); + // Keep uncompleted line in buffer + commandBuffer.Clear(); + commandBuffer.Append(lines[lines.Length - 1]); + + for (int i = 0; i < lines.Length - 1; i++) + { + string line = lines[i].Trim(); + if (string.IsNullOrEmpty(line)) continue; + + string verb = line.Length >= 4 ? line.Substring(0, 4).ToUpperInvariant() : line.ToUpperInvariant(); + string response = "500 Invalid Command\r\n"; + + switch (verb) + { + case "HELO": + case "EHLO": + heloHeader = line + "\r\n"; + response = "250 Hi\r\n"; + break; + + case "MAIL": + if (string.IsNullOrEmpty(heloHeader)) + { + response = "503 Do HELO first\r\n"; + } + else if (!string.IsNullOrEmpty(mailFrom)) + { + response = "503 Already have MAIL\r\n"; + } + else + { + mailFrom = line + "\r\n"; + response = "250 MAIL command recorded\r\n"; + } + break; + + case "RCPT": + if (string.IsNullOrEmpty(heloHeader)) + { + response = "503 Do HELO first\r\n"; + } + else if (string.IsNullOrEmpty(mailFrom)) + { + response = "503 Do MAIL first\r\n"; + } + else + { + rcptList.Add(line + "\r\n"); + response = "250 RCPT command recorded\r\n"; + } + break; + + case "DATA": + if (string.IsNullOrEmpty(heloHeader)) + { + response = "503 Do HELO first\r\n"; + } + else if (string.IsNullOrEmpty(mailFrom)) + { + response = "503 Do MAIL first\r\n"; + } + else if (rcptList.Count == 0) + { + response = "503 Do RCPT first\r\n"; + } + else + { + response = "354 Start mail input; end with .\r\n"; + receivingData = true; + dataBody = ""; + } + break; + + case "RSET": + response = "250 OK\r\n"; + mailFrom = ""; + dataBody = ""; + rcptList.Clear(); + break; + + case "QUIT": + byte[] quitBytes = Encoding.ASCII.GetBytes("221 Bye\r\n"); + socket.Send(quitBytes); + socket.Shutdown(SocketShutdown.Both); + socket.Close(); + return; + + default: + Log($"Unknown command: {line}", logFileName); + break; + } + + byte[] respBytes = Encoding.ASCII.GetBytes(response); + socket.Send(respBytes); + Log(response.TrimEnd(), logFileName); + } + } + } + catch (Exception ex) + { + Log($"Session exception: {ex.Message}", logFileName); + } + finally + { + try + { + if (socket.Connected) + { + socket.Shutdown(SocketShutdown.Both); + socket.Close(); + } + } + catch { } + } + + // Trigger worker threads if backlog grows + int threadMax = int.TryParse(ConfigurationManager.AppSettings["ThreadMax"], out var tm) ? tm : 5; + if (EmailQueue.Count > 5 && Interlocked.Read(ref ThreadCount) < threadMax) + { + Interlocked.Increment(ref ThreadCount); + ThreadPool.QueueUserWorkItem(state => + { + try { SendQueuedEmails(); } + finally { Interlocked.Decrement(ref ThreadCount); } + }); + } + } + + private void Log(string info, string logFileName = null) + { + if (ConsoleMode) + { + Console.WriteLine(info?.Replace("\r\n", "")); + } + + if (!string.IsNullOrEmpty(logFileName)) + { + try + { + using (StreamWriter sw = File.AppendText(logFileName)) + { + sw.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} - {info}"); + } + return; + } + catch { } + } + + lock (_syncObject) + { + try + { + string logDir = Path.Combine(ConfigurationManager.AppSettings["LogFolder"] ?? "C:\\Logs\\", "in"); + Directory.CreateDirectory(logDir); + string path = Path.Combine(logDir, $"{DateTime.Now:yyyyMMdd}.log"); + using (StreamWriter sw = File.AppendText(path)) + { + sw.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} - {info}"); + } + } + catch { } + } + } + + private void SendQueuedEmails() + { + string profilesBase = ConfigurationManager.AppSettings["Profiles"] ?? "C:\\Profiles\\"; + + while (EmailQueue.TryDequeue(out Email email)) + { + string cleanSender = email.sMAIL + .Replace(" ", "") + .Replace("MAILFROM:", "") + .Replace("<", "") + .Replace(">", "") + .Replace("\r", "") + .Replace("\n", ""); + + string profilePath = Path.Combine(profilesBase, email.FromIP, $"{cleanSender}.json"); + + if (!File.Exists(profilePath)) + { + int atIndex = cleanSender.IndexOf('@'); + string domain = atIndex >= 0 ? cleanSender.Substring(atIndex + 1) : ""; + profilePath = Path.Combine(profilesBase, email.FromIP, $"default@{domain}.json"); + + if (!File.Exists(profilePath)) + { + profilePath = Path.Combine(profilesBase, email.FromIP, "default.json"); + } + } + + if (!File.Exists(profilePath)) + { + Log($"No configuration profile found for IP {email.FromIP} and sender {cleanSender}"); + continue; + } + + JObject config; + try + { + config = JObject.Parse(File.ReadAllText(profilePath)); + } + catch (Exception ex) + { + Log($"Error parsing JSON profile at {profilePath}: {ex.Message}"); + continue; + } + + bool loggingEnabled = config["logging"]?["enabled"]?.Value() == 1; + string logBasePath = config["logging"]?["path"]?.ToString() ?? "C:\\Logs\\"; + bool hideRecipients = config["HideRecipients"]?.Value() == 1; + + string logDir = Path.Combine(logBasePath, "log"); + string outDir = Path.Combine(logBasePath, "out"); + Directory.CreateDirectory(logDir); + Directory.CreateDirectory(outDir); + + Guid guid = Guid.NewGuid(); + string primaryRcpt = email.sRCPTs.Count > 0 ? email.GetRecipientEmail(email.sRCPTs[0]) : "unknown"; + string mailLogPath = Path.Combine(logDir, $"{primaryRcpt}.{guid}.log"); + + if (loggingEnabled) + { + File.WriteAllText(Path.Combine(outDir, $"{primaryRcpt}.{guid}.eml"), email.sDATA); + File.WriteAllText(Path.Combine(outDir, $"{primaryRcpt}.{guid}.HELO"), email.sHELO); + File.WriteAllText(Path.Combine(outDir, $"{primaryRcpt}.{guid}.MAIL"), email.sMAIL); + File.WriteAllLines(Path.Combine(outDir, $"{primaryRcpt}.{guid}.RCPTS"), email.sRCPTs); + } + + string smtpHost = config["smtpserver"]?.ToString(); + int smtpPort = config["smtpserverport"]?.Value() ?? 25; + + if (string.IsNullOrEmpty(smtpHost)) + { + Log("Missing smtpserver target in configuration profile.", mailLogPath); + continue; + } + + bool replaceRecipients = config["recipientreplacement"]?["enabled"]?.Value() == 1; + List replacementList = config["recipientreplacement"]?["replacements"]?.ToObject>() ?? new List(); + + bool filterRecipients = config["recipientfilter"]?["enabled"]?.Value() == 1; + List whitelist = config["recipientfilter"]?["whitelist"]?.ToObject>() ?? new List(); + + bool bccEnabled = config["bcc"]?["enabled"]?.Value() == 1; + List bccRecipients = config["bcc"]?["recipients"]?.ToObject>() ?? new List(); + + Socket relaySocket = null; + try + { + IPAddress targetIp; + if (!IPAddress.TryParse(smtpHost, out targetIp)) + { + var hostEntry = Dns.GetHostEntry(smtpHost); + targetIp = hostEntry.AddressList.FirstOrDefault(a => a.AddressFamily == AddressFamily.InterNetwork) ?? hostEntry.AddressList[0]; + } + + relaySocket = new Socket(targetIp.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + relaySocket.ReceiveTimeout = 60000; + relaySocket.SendTimeout = 60000; + relaySocket.Connect(new IPEndPoint(targetIp, smtpPort)); + + if (!CheckAndSend("220", email.sHELO, relaySocket, mailLogPath) || + !CheckAndSend("250", email.sMAIL, relaySocket, mailLogPath)) + { + continue; + } + + bool hasValidRecipients = false; + + if (replaceRecipients) + { + foreach (string rep in replacementList) + { + if (CheckAndSend("250", $"RCPT TO: <{rep}>\r\n", relaySocket, mailLogPath)) + { + hasValidRecipients = true; + } + } + } + else + { + foreach (string rcptLine in email.sRCPTs) + { + if (rcptLine.Trim().Length < 8) continue; + + if (filterRecipients) + { + string extractedEmail = email.GetRecipientEmail(rcptLine); + if (!whitelist.Any(w => w.Equals(extractedEmail, StringComparison.OrdinalIgnoreCase))) + { + Log($"Filtering recipient: {extractedEmail}", mailLogPath); + continue; + } + Log($"Recipient approved by whitelist: {extractedEmail}", mailLogPath); + } + + if (CheckAndSend("250", rcptLine, relaySocket, mailLogPath)) + { + hasValidRecipients = true; + } + } + } + + if (!hasValidRecipients) + { + Log("No valid recipients accepted by upstream relay. Aborting.", mailLogPath); + continue; + } + + if (bccEnabled) + { + foreach (string bcc in bccRecipients) + { + CheckAndSend("250", $"RCPT TO: <{bcc}>\r\n", relaySocket, mailLogPath); + } + } + + if (CheckAndSend("250", "DATA\r\n", relaySocket, mailLogPath)) + { + string cleansedData = CleanseDataBlock(email.sDATA, hideRecipients); + if (!cleansedData.EndsWith("\r\n")) cleansedData += "\r\n"; + if (!cleansedData.EndsWith("\r\n.\r\n")) cleansedData += ".\r\n"; + + if (CheckAndSend("354", cleansedData, relaySocket, mailLogPath)) + { + CheckAndSend("250", "RSET\r\n", relaySocket, mailLogPath); + CheckAndSend("250", "QUIT\r\n", relaySocket, mailLogPath); + } + } + } + catch (Exception ex) + { + Log($"Upstream relay transmission failed: {ex.Message}", mailLogPath); + } + finally + { + if (relaySocket != null) + { + try + { + if (relaySocket.Connected) relaySocket.Shutdown(SocketShutdown.Both); + relaySocket.Close(); + } + catch { } + } + } + } + } + + private static string CleanseDataBlock(string sData, bool bHideRecipients) + { + if (!bHideRecipients || string.IsNullOrEmpty(sData)) return sData; + + StringBuilder sb = new StringBuilder(); + using (StringReader reader = new StringReader(sData)) + { + string line; + bool inHeaderSection = true; + bool skippingToHeader = false; + + while ((line = reader.ReadLine()) != null) + { + // Empty line marks end of email headers and start of MIME/Body + if (inHeaderSection && line.Length == 0) + { + inHeaderSection = false; + skippingToHeader = false; + } + + if (inHeaderSection) + { + if (Regex.IsMatch(line, @"^[Tt][Oo]:", RegexOptions.IgnoreCase)) + { + skippingToHeader = true; + continue; // Strip To: line + } + + // Check for header folding (continuation line starting with whitespace) + if (skippingToHeader && (line.StartsWith(" ") || line.StartsWith("\t"))) + { + continue; // Strip folded recipient lines + } + else + { + skippingToHeader = false; + } + } + + sb.Append(line).Append("\r\n"); + } + } + return sb.ToString(); + } + + private bool CheckAndSend(string prevSuccessCode, string nextCommand, Socket sock, string logFileName) + { + byte[] buffer = new byte[4096]; + try + { + int bytesRead = sock.Receive(buffer); + if (bytesRead <= 0) + { + Log("Upstream closed connection unexpectedly.", logFileName); + return false; + } + + string response = Encoding.ASCII.GetString(buffer, 0, bytesRead); + Log($"Upstream << {response.TrimEnd()}", logFileName); + + if (!response.StartsWith(prevSuccessCode, StringComparison.Ordinal)) + { + Log($"Expected response code {prevSuccessCode}, received: {response}", logFileName); + return false; + } + + Log($"Upstream >> {(nextCommand.Length > 200 ? nextCommand.Substring(0, 200) + "..." : nextCommand.TrimEnd())}", logFileName); + byte[] sendBytes = Encoding.ASCII.GetBytes(nextCommand); + sock.Send(sendBytes); + return true; + } + catch (Exception ex) + { + Log($"CheckAndSend failed: {ex.Message}", logFileName); + return false; + } + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + components?.Dispose(); + mre?.Dispose(); + } + base.Dispose(disposing); + } + + private void InitializeComponent() + { + components = new Container(); + base.ServiceName = "SMTPProxy.net"; + } + } +} \ No newline at end of file