SMTPProcessor.cs
上传用户:wdhx888
上传日期:2017-06-08
资源大小:112k
文件大小:16k
源码类别:

WEB邮件程序

开发平台:

C#

  1. namespace EricDaugherty.CSES.SmtpServer
  2. {
  3. using System;
  4. using System.Net;
  5. using System.Net.Sockets;
  6. using System.Text;
  7. using System.Text.RegularExpressions;
  8. using log4net;
  9. using EricDaugherty.CSES.Common;
  10. /// <summary>
  11. /// SMTPProcessor handles a single SMTP client connection.  This
  12. /// class provides an implementation of the RFC821 specification.
  13. /// </summary>
  14. /// <remarks>
  15. ///  Created by: Eric Daugherty
  16. /// </remarks>
  17. public class SMTPProcessor
  18. {
  19. #region Constants
  20. // Command codes
  21. /// <summary>HELO Command</summary>
  22. public const int COMMAND_HELO = 0;
  23. /// <summary>RSET Command</summary>
  24.     public const int COMMAND_RSET = 1;
  25. /// <summary>NOOP Command</summary>
  26.     public const int COMMAND_NOOP = 2;
  27. /// <summary>QUIT Command</summary>
  28.     public const int COMMAND_QUIT = 3;
  29. /// <summary>MAIL FROM Command</summary>
  30.     public const int COMMAND_MAIL = 4;
  31. /// <summary>RCPT TO Command</summary>
  32.     public const int COMMAND_RCPT = 5;
  33. /// <summary>DATA Comand</summary>
  34.     public const int COMMAND_DATA = 6;
  35. // Messages
  36. private const string MESSAGE_DEFAULT_WELCOME = "220 {0} Welcome to Eric Daugherty's C# SMTP Server.";
  37. private const string MESSAGE_DEFAULT_HELO_RESPONSE = "250 {0}";
  38. private const string MESSAGE_OK = "250 OK";
  39. private const string MESSAGE_START_DATA = "354 Start mail input; end with <CRLF>.<CRLF>";
  40. private const string MESSAGE_GOODBYE = "221 Goodbye.";
  41. private const string MESSAGE_UNKNOWN_COMMAND = "500 Command Unrecognized.";
  42. private const string MESSAGE_INVALID_COMMAND_ORDER = "503 Command not allowed here.";
  43. private const string MESSAGE_INVALID_ARGUMENT_COUNT = "501 Incorrect number of arguments.";
  44. private const string MESSAGE_INVALID_ADDRESS = "451 Address is invalid.";
  45. private const string MESSAGE_UNKNOWN_USER = "550 User does not exist.";
  46. private const string MESSAGE_SYSTEM_ERROR = "554 Transaction failed.";
  47. // Regular Expressions
  48. private static readonly Regex ADDRESS_REGEX = new Regex( "<.+@.+>", RegexOptions.IgnoreCase );
  49. #endregion
  50. #region Variables
  51. /// <summary>
  52. /// Every connection will be assigned a unique id to 
  53. /// provide consistent log output and tracking.
  54. /// </summary>
  55. private long connectionId;
  56. /// <summary>Determines which recipients to accept for delivery.</summary>
  57. private IRecipientFilter recipientFilter;
  58. /// <summary>Incoming Message spool</summary>
  59. private IMessageSpool messageSpool;
  60. /// <summary>Domain name for this server.</summary>
  61. private string domain;
  62. /// <summary>The message to display to the client when they first connect.</summary>
  63. private string welcomeMessage;
  64. /// <summary>The response to the HELO command.</summary>
  65. private string heloResponse;
  66. /// <summary>Default Logger</summary>
  67. private static ILog log = LogManager.GetLogger( typeof( SMTPProcessor ) );
  68. #endregion
  69. #region Constructors
  70. /// <summary>
  71. /// Initializes the SMTPProcessor with the appropriate 
  72. /// interface implementations.  This allows the relay and
  73. /// delivery behaviour of the SMTPProcessor to be defined
  74. /// by the specific server.
  75. /// </summary>
  76. /// <param name="domain">
  77. /// The domain name this server handles mail for.  This does not have to
  78. /// be a valid domain name, but it will be included in the Welcome Message
  79. /// and HELO response.
  80. /// </param>
  81. public SMTPProcessor( string domain )
  82. {
  83. Initialize( domain );
  84. // Initialize default Interface implementations.
  85. recipientFilter = new LocalRecipientFilter( domain );
  86. messageSpool = new MemoryMessageSpool();
  87. }
  88. /// <summary>
  89. /// Initializes the SMTPProcessor with the appropriate 
  90. /// interface implementations.  This allows the relay and
  91. /// delivery behaviour of the SMTPProcessor to be defined
  92. /// by the specific server.
  93. /// </summary>
  94. /// <param name="domain">
  95. /// The domain name this server handles mail for.  This does not have to
  96. /// be a valid domain name, but it will be included in the Welcome Message
  97. /// and HELO response.
  98. /// </param>
  99. /// <param name="recipientFilter">
  100. /// The IRecipientFilter implementation is responsible for 
  101. /// filtering the recipient addresses to determine which ones
  102. /// to accept for delivery.
  103. /// </param>
  104. public SMTPProcessor( string domain, IRecipientFilter recipientFilter )
  105. {
  106. Initialize( domain );
  107. this.recipientFilter = recipientFilter;
  108. messageSpool = new MemoryMessageSpool();
  109. }
  110. /// <summary>
  111. /// Initializes the SMTPProcessor with the appropriate 
  112. /// interface implementations.  This allows the relay and
  113. /// delivery behaviour of the SMTPProcessor to be defined
  114. /// by the specific server.
  115. /// </summary>
  116. /// <param name="domain">
  117. /// The domain name this server handles mail for.  This does not have to
  118. /// be a valid domain name, but it will be included in the Welcome Message
  119. /// and HELO response.
  120. /// </param>
  121. /// <param name="messageSpool">
  122. /// The IRecipientFilter implementation is responsible for 
  123. /// filtering the recipient addresses to determine which ones
  124. /// to accept for delivery.
  125. /// </param>
  126. public SMTPProcessor( string domain, IMessageSpool messageSpool )
  127. {
  128. Initialize( domain );
  129. recipientFilter = new LocalRecipientFilter( domain );
  130. this.messageSpool = messageSpool;
  131. }
  132. /// <summary>
  133. /// Initializes the SMTPProcessor with the appropriate 
  134. /// interface implementations.  This allows the relay and
  135. /// delivery behaviour of the SMTPProcessor to be defined
  136. /// by the specific server.
  137. /// </summary>
  138. /// <param name="domain">
  139. /// The domain name this server handles mail for.  This does not have to
  140. /// be a valid domain name, but it will be included in the Welcome Message
  141. /// and HELO response.
  142. /// </param>
  143. /// <param name="recipientFilter">
  144. /// The IRecipientFilter implementation is responsible for 
  145. /// filtering the recipient addresses to determine which ones
  146. /// to accept for delivery.
  147. /// </param>
  148. /// <param name="messageSpool">
  149. /// The IMessageSpool implementation is responsible for 
  150. /// spooling the inbound message once it has been recieved from the sender.
  151. /// </param>
  152. public SMTPProcessor( string domain, IRecipientFilter recipientFilter, IMessageSpool messageSpool )
  153. {
  154. Initialize( domain );
  155. this.recipientFilter = recipientFilter;
  156. this.messageSpool = messageSpool;
  157. }
  158. /// <summary>
  159. /// Provides common initialization logic for the constructors.
  160. /// </summary>
  161. private void Initialize( string domain )
  162. {
  163. // Initialize the connectionId counter
  164. connectionId = 1;
  165. this.domain = domain;
  166. // Initialize default messages
  167. welcomeMessage = String.Format( MESSAGE_DEFAULT_WELCOME, domain );
  168. heloResponse = String.Format( MESSAGE_DEFAULT_HELO_RESPONSE, domain );
  169. }
  170. #endregion
  171. #region Properties
  172. #endregion
  173. #region User Messages (Overridable)
  174. /// <summary>
  175. /// Returns the welcome message to display to new client connections.
  176. /// This method can be overridden to allow for user defined welcome messages.
  177. /// Please refer to RFC 821 for the rules on acceptable welcome messages.
  178. /// </summary>
  179. public virtual string WelcomeMessage
  180. {
  181. get
  182. {
  183. return welcomeMessage;
  184. }
  185. set
  186. {
  187. welcomeMessage = String.Format( value, domain );
  188. }
  189. }
  190. /// <summary>
  191. /// The response to the HELO command.  This response should
  192. /// include the local server's domain name.  Please refer to RFC 821
  193. /// for more details.
  194. /// </summary>
  195. public virtual string HeloResponse
  196. {
  197. get
  198. {
  199. return heloResponse;
  200. }
  201. set
  202. {
  203. heloResponse = String.Format( value, domain );
  204. }
  205. }
  206. #endregion
  207. #region Public Methods
  208. /// <summary>
  209. /// ProcessConnection handles a connected TCP Client
  210. /// and performs all necessary interaction with this
  211. /// client to comply with RFC821.  This method is thread 
  212. /// safe.
  213. /// </summary>
  214. public void ProcessConnection( Socket socket )
  215. {
  216. long currentConnectionId = 0;
  217. // Really only need to lock on the long, but that is not
  218. // allowed.  Is there a better way to do this?
  219. lock( this )
  220. {
  221. currentConnectionId = connectionId++;
  222. }
  223. SMTPContext context = new SMTPContext( currentConnectionId, socket );
  224. try 
  225. {
  226. SendWelcomeMessage( context );
  227. ProcessCommands( context );
  228. }
  229. catch( Exception exception )
  230. {
  231. log.Error( String.Format( "Connection {0}: Error: {1}", context.ConnectionId, exception ), exception );
  232. }
  233. }
  234. #endregion
  235. #region Private Handler Methods
  236. /// <summary>
  237. /// Sends the welcome greeting to the client.
  238. /// </summary>
  239. private void SendWelcomeMessage( SMTPContext context )
  240. {
  241. context.WriteLine( WelcomeMessage );
  242. }
  243. /// <summary>
  244. /// Handles the command input from the client.  This
  245. /// message returns when the client issues the quit command.
  246. /// </summary>
  247. private void ProcessCommands( SMTPContext context )
  248. {
  249. bool isRunning = true;
  250. String inputLine;
  251. // Loop until the client quits.
  252. while( isRunning )
  253. {
  254. try
  255. {
  256. inputLine = context.ReadLine();
  257. if( inputLine == null )
  258. {
  259. isRunning = false;
  260. context.Close();
  261. continue;
  262. }
  263. log.Debug( "ProcessCommands Read: " + inputLine );
  264. String[] inputs = inputLine.Split( " ".ToCharArray() );
  265. switch( inputs[0].ToLower() )
  266. {
  267. case "helo":
  268. Helo( context, inputs );
  269. break;
  270. case "rset":
  271. Rset( context );
  272. break;
  273. case "noop":
  274. context.WriteLine( MESSAGE_OK );
  275. break;
  276. case "quit":
  277. isRunning = false;
  278. context.WriteLine( MESSAGE_GOODBYE );
  279. context.Close();
  280. break;
  281. case "mail":
  282. if( inputs[1].ToLower().StartsWith( "from" ) )
  283. {
  284. Mail( context, inputLine.Substring( inputLine.IndexOf( " " ) ) );
  285. break;
  286. }
  287. context.WriteLine( MESSAGE_UNKNOWN_COMMAND );
  288. break;
  289. case "rcpt":
  290. if( inputs[1].ToLower().StartsWith( "to" ) ) 
  291. {
  292. Rcpt( context, inputLine.Substring( inputLine.IndexOf( " " ) ) );
  293. break;
  294. }
  295. context.WriteLine( MESSAGE_UNKNOWN_COMMAND );
  296. break;
  297. case "data":
  298. Data( context );
  299. break;
  300. default:
  301. context.WriteLine( MESSAGE_UNKNOWN_COMMAND );
  302. break;
  303. }
  304. }
  305. catch( Exception exception )
  306. {
  307. log.Error( String.Format( "Connection {0}: Exception occured while processing commands: {1}", context.ConnectionId, exception ), exception );
  308. context.WriteLine( MESSAGE_SYSTEM_ERROR );
  309. }
  310. }
  311. }
  312. /// <summary>
  313. /// Handles the HELO command.
  314. /// </summary>
  315. private void Helo( SMTPContext context, String[] inputs )
  316. {
  317. if( context.LastCommand == -1 )
  318. {
  319. if( inputs.Length == 2 )
  320. {
  321. context.ClientDomain = inputs[1];
  322. context.LastCommand = COMMAND_HELO;
  323. context.WriteLine( HeloResponse );
  324. }
  325. else
  326. {
  327. context.WriteLine( MESSAGE_INVALID_ARGUMENT_COUNT );
  328. }
  329. }
  330. else
  331. {
  332. context.WriteLine( MESSAGE_INVALID_COMMAND_ORDER );
  333. }
  334. }
  335. /// <summary>
  336. /// Reset the connection state.
  337. /// </summary>
  338. private void Rset( SMTPContext context )
  339. {
  340. if( context.LastCommand != -1 )
  341. {
  342. // Dump the message and reset the context.
  343. context.Reset();
  344. context.WriteLine( MESSAGE_OK );
  345. }
  346. else
  347. {
  348. context.WriteLine( MESSAGE_INVALID_COMMAND_ORDER );
  349. }
  350. }
  351. /// <summary>
  352. /// Handle the MAIL FROM:&lt;address&gt; command.
  353. /// </summary>
  354. private void Mail( SMTPContext context, string argument )
  355. {
  356. bool addressValid = false;
  357. if( context.LastCommand == COMMAND_HELO )
  358. {
  359. string address = ParseAddress( argument );
  360. if( address != null )
  361. {
  362. try
  363. {
  364. EmailAddress emailAddress = new EmailAddress( address );
  365. context.Message.FromAddress = emailAddress;
  366. context.LastCommand = COMMAND_MAIL;
  367. addressValid = true;
  368. context.WriteLine( MESSAGE_OK );
  369. if( log.IsDebugEnabled ) log.Debug( String.Format( "Connection {0}: MailFrom address: {1} accepted.", context.ConnectionId, address ) );
  370. }
  371. catch( InvalidEmailAddressException )
  372. {
  373. // This is fine, just fall through.
  374. }
  375. }
  376. // If the address is invalid, inform the client.
  377. if( !addressValid )
  378. {
  379. if( log.IsDebugEnabled ) log.Debug( String.Format( "Connection {0}: MailFrom argument: {1} rejected.  Should be from:<username@domain.com>", context.ConnectionId, argument ) );
  380. context.WriteLine( MESSAGE_INVALID_ADDRESS );
  381. }
  382. }
  383. else
  384. {
  385. context.WriteLine( MESSAGE_INVALID_COMMAND_ORDER );
  386. }
  387. }
  388. /// <summary>
  389. /// Handle the RCPT TO:&lt;address&gt; command.
  390. /// </summary>
  391. private void Rcpt( SMTPContext context, string argument )
  392. {
  393. if( context.LastCommand == COMMAND_MAIL || context.LastCommand == COMMAND_RCPT )
  394. {
  395. string address = ParseAddress( argument );
  396. if( address != null )
  397. {
  398. try
  399. {
  400. EmailAddress emailAddress = new EmailAddress( address );
  401. // Check to make sure we want to accept this message.
  402. if( recipientFilter.AcceptRecipient( context, emailAddress ) )
  403. {
  404. context.Message.AddToAddress( emailAddress );
  405. context.LastCommand = COMMAND_RCPT;
  406. context.WriteLine( MESSAGE_OK );
  407. if( log.IsDebugEnabled ) log.Debug( String.Format( "Connection {0}: RcptTo address: {1} accepted.", context.ConnectionId, address ) );
  408. }
  409. else
  410. {
  411. context.WriteLine( MESSAGE_UNKNOWN_USER );
  412. if( log.IsDebugEnabled ) log.Debug( String.Format( "Connection {0}: RcptTo address: {1} rejected.  Did not pass Address Filter.", context.ConnectionId, address ) );
  413. }
  414. }
  415. catch( InvalidEmailAddressException )
  416. {
  417. if( log.IsDebugEnabled ) log.Debug( String.Format( "Connection {0}: RcptTo argument: {1} rejected.  Should be from:<username@domain.com>", context.ConnectionId, argument ) );
  418. context.WriteLine( MESSAGE_INVALID_ADDRESS );
  419. }
  420. }
  421. else
  422. {
  423. if( log.IsDebugEnabled ) log.Debug( String.Format( "Connection {0}: RcptTo argument: {1} rejected.  Should be from:<username@domain.com>", context.ConnectionId, argument ) );
  424. context.WriteLine( MESSAGE_INVALID_ADDRESS );
  425. }
  426. }
  427. else
  428. {
  429. context.WriteLine( MESSAGE_INVALID_COMMAND_ORDER );
  430. }
  431. }
  432. private void Data( SMTPContext context )
  433. {
  434. context.WriteLine( MESSAGE_START_DATA );
  435. SMTPMessage message = context.Message;
  436. IPEndPoint clientEndPoint = (IPEndPoint) context.Socket.RemoteEndPoint;
  437. StringBuilder header = new StringBuilder();
  438. header.Append( String.Format( "Received: from {0} ({0} [{1}])", context.ClientDomain, clientEndPoint.Address ) );
  439. header.Append( "rn" );
  440. header.Append( String.Format( "     by {0} (Eric Daugherty's C# Email Server)", domain ) );
  441. header.Append( "rn" );
  442. header.Append( "     " + System.DateTime.Now );
  443. header.Append( "rn" );
  444. message.AddData( header.ToString() );
  445. String line = context.ReadLine();
  446. while( !line.Equals( "." ) )
  447. {
  448. message.AddData( line );
  449. message.AddData( "rn" );
  450. line = context.ReadLine();
  451. }
  452. // Spool the message
  453. messageSpool.SpoolMessage( message );
  454. context.WriteLine( MESSAGE_OK );
  455. // Reset the connection.
  456. context.Reset();
  457. }
  458. #endregion
  459. #region Private Helper Methods
  460. /// <summary>
  461. /// Parses a valid email address out of the input string and return it.
  462. /// Null is returned if no address is found.
  463. /// </summary>
  464. private string ParseAddress( string input )
  465. {
  466. Match match = ADDRESS_REGEX.Match( input );
  467. string matchText;
  468. if( match.Success )
  469. {
  470. matchText = match.Value;
  471. // Trim off the :< chars
  472. matchText = matchText.Remove( 0, 1 );
  473. // trim off the . char.
  474. matchText = matchText.Remove( matchText.Length - 1, 1 );
  475. return matchText;
  476. }
  477. return null;
  478. }
  479. #endregion
  480. }
  481. }