PageRenderTime 66ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/Arendee.Lovon.Smtp/SmtpClientProcessor.cs

https://bitbucket.org/kzoabi/lovon
C# | 356 lines | 310 code | 43 blank | 3 comment | 46 complexity | 178f0691ee52d3d9c367106efdf5242f MD5 | raw file
  1. //Lovon is Licensed under the MS-PL, Copyright © 2012 The Arendee Lovon Team
  2. using System;
  3. using System.Linq;
  4. using System.Collections.Generic;
  5. using System.Net.Sockets;
  6. using System.Text;
  7. using System.Text.RegularExpressions;
  8. using System.Threading.Tasks;
  9. using Arendee.Lovon.Smtp.Exceptions;
  10. using NLog;
  11. using System.Net.Mail;
  12. using System.Net;
  13. namespace Arendee.Lovon.Smtp
  14. {
  15. public class SmtpClientProcessor
  16. {
  17. private const string END_OF_LINE = "\r\n";
  18. private static Logger logger = LogManager.GetCurrentClassLogger();
  19. private static readonly Regex EMAIL_ADDRESS_REGEX = new Regex("<.+@.+>", RegexOptions.IgnoreCase);
  20. private Socket client;
  21. private SmtpSessionState sessionState;
  22. private SmtpMessage currentMessage;
  23. private SmtpServer server;
  24. private Queue<string> readQueue = new Queue<string>();
  25. public SmtpClientProcessor(Socket client, SmtpServer server)
  26. {
  27. this.client = client;
  28. this.server = server;
  29. InitializeSession();
  30. }
  31. private void InitializeSession()
  32. {
  33. sessionState = SmtpSessionState.Connected;
  34. currentMessage = new SmtpMessage();
  35. }
  36. public async void ProcessAsync()
  37. {
  38. WriteLine(SmtpResponseMessage.WELCOME, server.Domain);
  39. await Task.Run(() =>
  40. {
  41. var command = ReadLine();
  42. while (command != null)
  43. {
  44. if (command != String.Empty)
  45. ProcessCommand(command);
  46. command = ReadLine();
  47. }
  48. logger.Info("Client disconnected");
  49. client.Close();
  50. });
  51. }
  52. private void ProcessCommand(string command)
  53. {
  54. logger.Trace("Command received from client '{0}': {1}", client.RemoteEndPoint, command);
  55. string[] parts = command.ToLower().Split(' ');
  56. switch (parts[0].Replace(END_OF_LINE, ""))
  57. {
  58. case "helo":
  59. case "ehlo":
  60. HandleHeloCommand(parts);
  61. break;
  62. case "mail":
  63. HandleMailCommand(parts);
  64. break;
  65. case "rcpt":
  66. HandleRcptCommand(parts);
  67. break;
  68. case "data":
  69. HandleDataCommand();
  70. break;
  71. case "rset":
  72. HandleRsetCommand();
  73. break;
  74. case "noop":
  75. HandleNoopCommand();
  76. break;
  77. case "quit":
  78. HandleQuitCommand();
  79. break;
  80. default:
  81. WriteLine(SmtpResponseMessage.COMMAND_SYNTAX_ERROR);
  82. break;
  83. }
  84. }
  85. private void HandleHeloCommand(string[] parts)
  86. {
  87. if (sessionState != SmtpSessionState.Connected)
  88. {
  89. WriteLine(SmtpResponseMessage.INVALID_COMMAND_ORDER);
  90. }
  91. else if (parts.Length != 2)
  92. {
  93. WriteLine(SmtpResponseMessage.COMMAND_PARAMETERS_SYNTAX_ERROR);
  94. }
  95. else
  96. {
  97. currentMessage.ServerAddress = ((IPEndPoint)client.RemoteEndPoint).Address;
  98. currentMessage.ServerDomain = parts[1];
  99. ///TODO: Should perform a DNS lookup of the specified domain
  100. sessionState = SmtpSessionState.HeloCommandReceived;
  101. WriteLine(SmtpResponseMessage.HELO, server.Domain);
  102. }
  103. }
  104. private void HandleMailCommand(string[] parts)
  105. {
  106. string senderAddress = null;
  107. if (sessionState != SmtpSessionState.HeloCommandReceived && sessionState != SmtpSessionState.DataCommandReceived)
  108. {
  109. WriteLine(SmtpResponseMessage.INVALID_COMMAND_ORDER);
  110. }
  111. else if (parts.Length < 2 || !TryExtractEmailAddress(String.Join("",parts) , out senderAddress))
  112. {
  113. WriteLine(SmtpResponseMessage.COMMAND_PARAMETERS_SYNTAX_ERROR);
  114. }
  115. else if (!server.SenderFilter.SenderAccepted(senderAddress))
  116. {
  117. WriteLine(SmtpResponseMessage.SENDER_ADDRESS_REJECTED);
  118. }
  119. else
  120. {
  121. currentMessage.SenderAddress = new MailAddress(senderAddress);
  122. sessionState = SmtpSessionState.MailCommandReceived;
  123. WriteLine(SmtpResponseMessage.OK);
  124. }
  125. }
  126. private void HandleRcptCommand(string[] parts)
  127. {
  128. string recipientAddress = null;
  129. if (sessionState != SmtpSessionState.MailCommandReceived && sessionState != SmtpSessionState.RcptCommandReceived)
  130. {
  131. WriteLine(SmtpResponseMessage.INVALID_COMMAND_ORDER);
  132. }
  133. else if (parts.Length < 2 || !TryExtractEmailAddress(String.Join("",parts), out recipientAddress))
  134. {
  135. WriteLine(SmtpResponseMessage.COMMAND_PARAMETERS_SYNTAX_ERROR);
  136. }
  137. else if (!server.RecipientFilter.RecipientAccepted(recipientAddress))
  138. {
  139. WriteLine(SmtpResponseMessage.RECIPIENT_ADDRESS_REJECTED);
  140. }
  141. else
  142. {
  143. currentMessage.RecipientAddresses.Add(new MailAddress(recipientAddress));
  144. sessionState = SmtpSessionState.RcptCommandReceived;
  145. WriteLine(SmtpResponseMessage.OK);
  146. }
  147. }
  148. private void HandleDataCommand()
  149. {
  150. if (sessionState != SmtpSessionState.RcptCommandReceived)
  151. {
  152. WriteLine(SmtpResponseMessage.INVALID_COMMAND_ORDER);
  153. }
  154. else
  155. {
  156. WriteLine(SmtpResponseMessage.START_MAIL_INPUT);
  157. sessionState = SmtpSessionState.DataCommandReceived;
  158. var dataBuilder = new StringBuilder();
  159. var line = ReadLine();
  160. while (line != ".")
  161. {
  162. dataBuilder.AppendFormat("{0}\r\n", line);
  163. line = ReadLine();
  164. }
  165. var rawBody = dataBuilder.ToString();
  166. currentMessage.Headers = ExtractHeadersFromRawBody(rawBody);
  167. if (currentMessage.Headers == null)
  168. {
  169. WriteLine(SmtpResponseMessage.SERVER_ERROR, "Failed to extract the headers");
  170. }
  171. else
  172. {
  173. currentMessage.Subject = currentMessage.Headers.First(h => h.Key == "subject").Value;
  174. currentMessage.Body = ExtractBodyFromRawBody(rawBody);
  175. logger.Trace("Data received from client '{0}': {1}", client.RemoteEndPoint, rawBody);
  176. logger.Debug("Mail received from client '{0}' with subject: {1}", client.RemoteEndPoint, currentMessage.Subject);
  177. }
  178. try
  179. {
  180. server.SpoolService.Spool(currentMessage);
  181. logger.Trace("Message from client '{0}' spooled", client.RemoteEndPoint);
  182. WriteLine(SmtpResponseMessage.OK);
  183. }
  184. catch (SpoolException ex)
  185. {
  186. logger.Error("{0}: {1}", ex.Message, (ex.InnerException != null ? ex.InnerException.Message : ""));
  187. WriteLine(SmtpResponseMessage.SERVER_ERROR, ex.Message);
  188. }
  189. catch (Exception ex)
  190. {
  191. logger.Error("Unexpected exception while trying to spool the message: {0}", ex.Message);
  192. WriteLine(SmtpResponseMessage.SERVER_ERROR);
  193. }
  194. finally
  195. {
  196. sessionState = SmtpSessionState.HeloCommandReceived;
  197. currentMessage = new SmtpMessage()
  198. {
  199. ServerAddress = currentMessage.ServerAddress,
  200. ServerDomain = currentMessage.ServerDomain
  201. };
  202. }
  203. }
  204. }
  205. private void HandleQuitCommand()
  206. {
  207. WriteLine(SmtpResponseMessage.GOODBYE);
  208. client.Close();
  209. }
  210. private void HandleRsetCommand()
  211. {
  212. if (sessionState == SmtpSessionState.Connected)
  213. {
  214. WriteLine(SmtpResponseMessage.INVALID_COMMAND_ORDER);
  215. }
  216. else
  217. {
  218. InitializeSession();
  219. WriteLine(SmtpResponseMessage.OK);
  220. }
  221. }
  222. private void HandleNoopCommand()
  223. {
  224. WriteLine(SmtpResponseMessage.OK);
  225. }
  226. private string ReadLine()
  227. {
  228. if (client == null || !client.Connected)
  229. return null;
  230. if (readQueue.Count > 0)
  231. return readQueue.Dequeue();
  232. byte[] buffer = new byte[client.ReceiveBufferSize];
  233. var read = client.Receive(buffer);
  234. var lineBuilder = new StringBuilder();
  235. if (read > 0)
  236. {
  237. var input = ASCIIEncoding.ASCII.GetString(buffer, 0, read)
  238. .Split(new string[] { END_OF_LINE }, StringSplitOptions.None);
  239. input.Skip(1).ToList().ForEach(i => readQueue.Enqueue(i));
  240. return input[0];
  241. }
  242. else
  243. {
  244. return null;
  245. }
  246. }
  247. private void WriteLine(string line, params object[] args)
  248. {
  249. line = String.Concat(String.Format(line, args), END_OF_LINE);
  250. var data = ASCIIEncoding.ASCII.GetBytes(line);
  251. client.Send(data);
  252. logger.Trace("Response sent to client '{0}': {1}", client.RemoteEndPoint, line);
  253. }
  254. private bool TryExtractEmailAddress(string input, out string emailAddress)
  255. {
  256. var match = EMAIL_ADDRESS_REGEX.Match(input);
  257. if (!match.Success)
  258. {
  259. emailAddress = null;
  260. return false;
  261. }
  262. else
  263. {
  264. emailAddress = match.Value.Substring(1, match.Value.Length - 2);
  265. return true;
  266. }
  267. }
  268. //TODO: THIS NEEDS TO BE FIXED, DOES NOT GET THE HEADERS PROPERLY
  269. private List<KeyValuePair<string, string>> ExtractHeadersFromRawBody(string rawBody)
  270. {
  271. try
  272. {
  273. var headers = new List<KeyValuePair<string, string>>();
  274. if (rawBody.StartsWith("\r\n\r\n"))
  275. rawBody = rawBody.Substring(4);
  276. var bodyOffset = rawBody.IndexOf("\r\n\r\n");
  277. logger.Trace("Body offset: {0}", bodyOffset);
  278. rawBody.Substring(0, bodyOffset)
  279. .Split(new string[] { END_OF_LINE }, StringSplitOptions.RemoveEmptyEntries)
  280. .Where(s => !String.IsNullOrWhiteSpace(s))
  281. .ToList()
  282. .ForEach(s =>
  283. {
  284. if (!s.Contains(':'))
  285. {
  286. var lastHeader = headers.Last();
  287. headers[headers.Count - 1] = new KeyValuePair<string, string>(lastHeader.Key, lastHeader.Value + s);
  288. }
  289. else
  290. {
  291. logger.Trace("Header Line: {0}", s);
  292. var parts = s.Split(':');
  293. logger.Trace("Parts Length: {0}", parts.Length);
  294. headers.Add(new KeyValuePair<string,string>(parts[0].ToLower(), parts[1]));
  295. }
  296. });
  297. return headers;
  298. }
  299. catch (Exception ex)
  300. {
  301. logger.TraceException("Failed to extract the headers", ex);
  302. logger.Trace(ex.ToString());
  303. logger.Trace(rawBody);
  304. return null;
  305. }
  306. }
  307. public string ExtractBodyFromRawBody(string rawBody)
  308. {
  309. var bodyOffset = rawBody.IndexOf("\r\n\r\n");
  310. return rawBody.Substring(bodyOffset);
  311. }
  312. }
  313. }