SMTP スタブ

とりあえず25番に反応してくれる人がいないと先に進まなかったので。

SmtpServer
接続を待ち受ける人。

namespace SmtpStub {
	class SmtpServer {
		TcpListener _listener = new TcpListener( new IPEndPoint( IPAddress.Any, 25 ) );
		List<MailProcessor> _processores = new List<MailProcessor>();

		bool _activated;
		/// <summary>
		/// 現在の稼働状態を示します。
		/// </summary>
		public bool Activated {
			get { return _activated; }
		}

		public void Start() {
			_actived = true;
			_listener.Start();
		}

		public void Stop() {
			_actived = false;
			_listener.Stop();
			foreach ( var item in _processores ) {
				item.Shutdown -= processor_Shutdown;
				item.Dispose();
			}
			_processores.Clear();
		}

		public event EventHandler<MailProcessorEventArgs> CommunicationMessage;

		public SmtpServer() {
		}

		public void Listen() {
			try {
				while ( Actived ) {
					var client = _listener.AcceptTcpClient();
					var processor = new MailProcessor( client );
					processor.CommunicationMessage += new EventHandler<MailProcessorEventArgs>( processor_CommunicationMessage );
					processor.Shutdown += new EventHandler( processor_Shutdown );
					var th = new Thread( processor.Connect );

					_processores.Add( processor );

					th.Start();
				}
				_listener.Stop();
			} catch ( SocketException ) {
				if ( Actived ) {
					throw;
				}
			}
		}

		void processor_Shutdown( object sender, EventArgs e ) {
			if ( _actived ) {
				_processores.Remove( (MailProcessor)sender );
			}
		}

		void processor_CommunicationMessage( object sender, MailProcessorEventArgs e ) {
			OnCommunicationMessage( e );
		}

		void OnCommunicationMessage( MailProcessorEventArgs e ) {
			if ( CommunicationMessage != null ) {
				CommunicationMessage( this, e );
			}
		}
	}
}


MailProcessor
接続された相手と会話する人。

namespace SmtpStub {
	class MailProcessor : IDisposable {
		readonly TcpClient _client;
		readonly Encoding _encoding = Encoding.Default;
		static int _globalCode;

		public event EventHandler<MailProcessorEventArgs> CommunicationMessage;
		static readonly object _lockObject = new object();

		public MailProcessor( TcpClient client ) {
			_client = client;
			lock ( _lockObject ) {
				_globalCode++;
			}
		}

		public void Connect() {
			try {
				var stream = _client.GetStream();
				_client.ReceiveTimeout = 10 * 1000;
				SendRequest( "220 SMTP stub" );
				ProcessingCommand( stream );
			} catch ( IOException ex ) {
				var exception = ex.InnerException as SocketException;
				if ( exception == null ) {
					OnCommunicationMessage( ex.ToString(), ResponseType.None );
				} else {
					if ( IsTimeout( exception ) ) {
						OnCommunicationMessage( "connection timeout", ResponseType.None );
					} else if ( ( IsConnectionClose( exception ) ) ) {
						OnCommunicationMessage( "close connection", ResponseType.None );
					}else{
						OnCommunicationMessage( ex.ToString(), ResponseType.None );
					}
				}
			} catch ( Exception ex ) {
				OnCommunicationMessage( ex.ToString(), ResponseType.None );
			} finally {
				_client.Close();
				OnShutdown();
			}
		}

		private void ProcessingCommand( NetworkStream stream ) {
			while ( true ) {
				try {
					ReceiveCommand( ReadAll( stream, new byte[] { 0x0d, 0x0a } ), Helo, "250 SMTP stub Hello " + _client.Client.RemoteEndPoint.ToString() );
					ReceiveCommand( ReadAll( stream, new byte[] { 0x0d, 0x0a } ), Mail, "250 ok" );
					ReceiveCommand( ReadAll( stream, new byte[] { 0x0d, 0x0a } ), Rcpt, "250 ok" );
					ReceiveCommand( ReadAll( stream, new byte[] { 0x0d, 0x0a } ), Data, "354 go ahead" );
					SendRequest( Body( ReadAll( stream, new byte[] { 0x0d, 0x0a, (byte)'.' ,0x0d, 0x0a } ) ), "250 ok Message accepted for delivery" );
				} catch ( ApplicationException ) {
					if ( !_client.Connected ) {
						break;
					}
				}
			}
		}

		/// <summary>
		/// 接続を切ったことによる例外かどうかを判別します。
		/// </summary>
		/// <param name="exception">判別対象例外。</param>
		/// <returns>接続が切られたことによる例外であれば true。それ以外は false。</returns>
		private bool IsConnectionClose( SocketException exception ) {
			return _disposed && exception.ErrorCode == 0x2714;
		}

		private bool IsTimeout( SocketException exception ) {
			return !_disposed && exception.ErrorCode == 0x274c;
		}

		void ReceiveCommand( string  message, Func<string, bool> func, string respnce ) {
			if ( func( message ) ) {
				SendRequest( respnce );
			} else {
				SendRequest( Quit( message ), "221 stub" );
				_client.Close();
				throw new ApplicationException();
			}
		}

		private bool Quit( string message ) {
			var regex = new Regex( "^QUIT\r\n$", RegexOptions.IgnoreCase | RegexOptions.Compiled );
			return regex.IsMatch( message );
		}

		private bool Body( string message ) {
			var regex = new Regex( "^.*\r\n\\.\r\n$", RegexOptions.Singleline | RegexOptions.RightToLeft | RegexOptions.Compiled );
			return regex.IsMatch( message );
		}

		private bool Data( string message ) {
			var regex = new Regex( "^DATA\r\n$", RegexOptions.IgnoreCase | RegexOptions.Compiled );
			return regex.IsMatch( message );
		}

		private bool Rcpt( string message ) {
			var regex = new Regex( "^RCPT TO:.*\r\n$", RegexOptions.IgnoreCase | RegexOptions.Compiled );
			return regex.IsMatch( message );
		}

		private bool Mail( string message ) {
			var regex = new Regex( "^MAIL FROM:.*\r\n$", RegexOptions.IgnoreCase | RegexOptions.Compiled );
			return regex.IsMatch( message );
		}

		private bool Helo( string message ) {
			var regex = new Regex( "^[EHLO|HELO] ?.*\r\n$", RegexOptions.IgnoreCase | RegexOptions.Compiled );
			return regex.IsMatch( message );
		}

		private void SendRequest( bool result, string resultMessage ) {
			if ( result ) {
				SendRequest( resultMessage );
			} else {
				SendRequest( "500 Command not found." );
				throw new FormatException();
			}
		}

		void SendRequest( string message ) {
			var buff = StringToByteArray( message + "\r\n" );
			_client.GetStream().Write( buff, 0, buff.Length );
			OnCommunicationMessage( message, ResponseType.Send );
		}

		string ByteArrayToString( byte[] source ) {
			return Encoding.ASCII.GetString( source );
		}

		byte[] StringToByteArray( string source ) {
			return _encoding.GetBytes( source );
		}

		bool IsEnd( List<byte> list, byte[] endChars ) {
			int i = list.Count - 1;
			int j = endChars.Length - 1;
			while ( j >= 0 ) {
				if ( list[i--] != endChars[j--] ) {
					return false;
				}
			}
			return true;
		}

		string ReadAll( Stream stream, byte[] endChars ) {
			var list = new List<byte>();
			int buff = 0;

			while ( buff != -1 ) {
				buff = stream.ReadByte();
				list.Add( (byte)buff );
				if ( list.Count >= endChars.Length ) {
					if ( IsEnd( list, endChars ) ) {
						var message = ByteArrayToString( list.ToArray() );
						OnCommunicationMessage( message, ResponseType.Receive );
						return message;
					}
				}

			}
			throw new ApplicationException();
		}

		protected void OnCommunicationMessage( string message, ResponseType type ) {
			if ( CommunicationMessage != null ) {
				CommunicationMessage( this, new MailProcessorEventArgs( message, type ) );
			}
		}

		public event EventHandler Shutdown;

		protected void OnShutdown() {
			if ( Shutdown != null ) {
				Shutdown( this, EventArgs.Empty );
			}
		}

		#region IDisposable メンバ
		bool _disposed;

		public void Dispose() {
			if ( _disposed ) {
				return;
			}
			_disposed = true;
			_client.Close();
		}

		#endregion

		~MailProcessor() {
			Dispose();
			CommunicationMessage = null;
			Shutdown = null;
		}
	}
}


MainForm
使う人。

namespace SmtpStub {
	public partial class MainForm : Form {
		public MainForm() {
			InitializeComponent();
			_smtpServer.CommunicationMessage += new EventHandler<MailProcessorEventArgs>( _smtpServer_CommunicationMessage );
		}

		void _smtpServer_CommunicationMessage( object sender, MailProcessorEventArgs e ) {
			if ( base.InvokeRequired ) {
				Invoke( new Action<object, MailProcessorEventArgs>( _smtpServer_CommunicationMessage ), sender, e );
				return;
			}
			if ( e.ResponseType == ResponseType.Receive ) {
				textBox1.Text += "< ";
			} else if ( e.ResponseType == ResponseType.Send ) {
				textBox1.Text += "> ";
			}
			textBox1.Text += e.Message.TrimEnd( '\r', '\n' ) + "\r\n";
		}

		SmtpServer _smtpServer = new SmtpServer();

		private void button1_Click( object sender, EventArgs e ) {
			if ( !_smtpServer.Actived ) {
				_smtpServer.Start();
				var th = new Thread( _smtpServer.Listen );
				th.Start();
				textBox1.Text += "Start smtp stub listen 25 port\r\n";
				button1.Text="終了";
			} else {
				_smtpServer.Stop();
				textBox1.Text += "Stop smtp stub\r\n";
				button1.Text = "開始";
			}
		}

		protected override void OnClosed( EventArgs e ) {
			base.OnClosed( e );
			_smtpServer.Stop();
		}

	}
}

その他

class MailProcessorEventArgs : EventArgs {
	public string Message { get; set; }
	public ResponseType ResponseType { get; set; }

	public MailProcessorEventArgs( string message, ResponseType type ) {
		Message = message;
		ResponseType = type;
	}
}

enum ResponseType {
	None,
	Send,
	Receive,
}

それにしても例外のデザインではなぜ標準例外の使用を推奨するのか。それはそれで使いにくい。