/// <summary> /// Constructor. /// </summary> /// <param name="options">The SMTP server options.</param> /// <param name="tcpClient">The TCP client to operate the session on.</param> internal SmtpSession(ISmtpServerOptions options, TcpClient tcpClient) { _options = options; _tcpClient = tcpClient; _context = new SmtpSessionContext(tcpClient); _stateMachine = new SmtpStateMachine(options, _context); }
/// <summary> /// Listen for SMTP traffic on the given endpoint. /// </summary> /// <param name="endpointDefinition">The definition of the endpoint to listen on.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>A task which performs the operation.</returns> async Task ListenAsync(IEndpointDefinition endpointDefinition, CancellationToken cancellationToken) { // The listener can be stopped either by the caller cancelling the CancellationToken used when starting the server, or when calling // the shutdown method. The Shutdown method will stop the listeners and allow any active sessions to finish gracefully. var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_shutdownTokenSource.Token, cancellationToken); using var endpointListener = _endpointListenerFactory.CreateListener(endpointDefinition); while (cancellationTokenSource.Token.IsCancellationRequested == false) { var sessionContext = new SmtpSessionContext(_serviceProvider, _options, endpointDefinition); try { // wait for a client connection sessionContext.Pipe = await GetPipeAsync(endpointListener, sessionContext, cancellationTokenSource.Token).ConfigureAwait(false); } catch (OperationCanceledException) { } catch (Exception ex) { OnSessionFaulted(new SessionFaultedEventArgs(sessionContext, ex)); continue; } _sessions.Run(sessionContext, cancellationTokenSource.Token); } }
/// <summary> /// Execute the command handler against the specified session context. /// </summary> /// <param name="context">The session context to execute the command handler against.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>A task which asynchronously performs the execution.</returns> internal async Task ExecuteAsync(SmtpSessionContext context, CancellationToken cancellationToken) { var retryCount = _maxRetries; while (retryCount-- > 0 && context.IsQuitRequested == false && cancellationToken.IsCancellationRequested == false) { var text = await context.Text.ReadLineAsync(cancellationToken).ConfigureAwait(false); SmtpCommand command; SmtpResponse errorResponse; if (TryAccept(context, text, out command, out errorResponse) == false) { var response = new SmtpResponse(errorResponse.ReplyCode, $"{errorResponse.Message}, {retryCount} retry(ies) remaining."); await context.Text.ReplyAsync(response, cancellationToken); continue; } // the command was a normal command so we can reset the retry count retryCount = _maxRetries; await ExecuteAsync(command, context, cancellationToken).ConfigureAwait(false); } }
/// <summary> /// Listen for SMTP traffic on the given endpoint. /// </summary> /// <param name="endpointDefinition">The definition of the endpoint to listen on.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>A task which performs the operation.</returns> async Task ListenAsync(IEndpointDefinition endpointDefinition, CancellationToken cancellationToken) { using (var endpointListener = _options.EndpointListenerFactory.CreateListener(endpointDefinition)) { while (_shutdownTokenSource.Token.IsCancellationRequested == false && cancellationToken.IsCancellationRequested == false) { var sessionContext = new SmtpSessionContext(_options, endpointDefinition); try { await ListenAsync(sessionContext, endpointListener, cancellationToken); } catch (OperationCanceledException) when(_shutdownTokenSource.Token.IsCancellationRequested == false) { if (sessionContext.NetworkClient != null) { OnSessionCancelled(new SessionEventArgs(sessionContext)); } } catch (OperationCanceledException) { } catch (Exception ex) { OnSessionFaulted(new SessionFaultedEventArgs(sessionContext, ex)); } } } }
/// <summary> /// Read the command input. /// </summary> /// <param name="context">The session context to execute the command handler against.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The input that was received from the client.</returns> async Task <IReadOnlyList <ArraySegment <byte> > > ReadCommandInputAsync(SmtpSessionContext context, CancellationToken cancellationToken) { var timeout = new CancellationTokenSource(_context.ServerOptions.CommandWaitTimeout); var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, cancellationToken); try { return(await context.NetworkClient.ReadLineAsync(cancellationTokenSource.Token).ConfigureAwait(false)); } catch (OperationCanceledException) { if (timeout.IsCancellationRequested) { await context.NetworkClient.ReplyAsync(new SmtpResponse(SmtpReplyCode.ServiceClosingTransmissionChannel, "Timeout whilst waiting for input."), cancellationToken).ConfigureAwait(false); return(null); } await context.NetworkClient.ReplyAsync(new SmtpResponse(SmtpReplyCode.ServiceClosingTransmissionChannel, "The session has be cancelled."), CancellationToken.None).ConfigureAwait(false); return(null); } finally { timeout.Dispose(); cancellationTokenSource.Dispose(); } }
/// <summary> /// Execute the command handler against the specified session context. /// </summary> /// <param name="context">The session context to execute the command handler against.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>A task which asynchronously performs the execution.</returns> async Task ExecuteAsync(SmtpSessionContext context, CancellationToken cancellationToken) { var retries = _options.MaxRetryCount; while (retries-- > 0 && context.IsQuitRequested == false && cancellationToken.IsCancellationRequested == false) { var text = await context.Client.ReadLineAsync(cancellationToken).ReturnOnAnyThread(); if (TryAccept(context, text, out SmtpCommand command, out SmtpResponse response)) { try { await ExecuteAsync(command, context, cancellationToken).ReturnOnAnyThread(); retries = _options.MaxRetryCount; continue; } catch (SmtpResponseException responseException) { context.IsQuitRequested = responseException.IsQuitRequested; response = responseException.Response; } } await context.Client.ReplyAsync(CreateErrorResponse(response, retries), cancellationToken); } }
/// <summary> /// Listen for SMTP traffic on the given endpoint. /// </summary> /// <param name="endpointDefinition">The definition of the endpoint to listen on.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>A task which performs the operation.</returns> async Task ListenAsync(IEndpointDefinition endpointDefinition, CancellationToken cancellationToken) { // The listener can be stopped either by the caller cancelling the CancellationToken used when starting the server, or when calling // the shutdown method. The Shutdown method will stop the listeners and allow any active sessions to finish gracefully. var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_shutdownTokenSource.Token, cancellationToken); using var endpointListener = _endpointListenerFactory.CreateListener(endpointDefinition); while (cancellationTokenSource.Token.IsCancellationRequested == false) { var sessionContext = new SmtpSessionContext(_serviceProvider, _options, endpointDefinition); try { // wait for a client connection sessionContext.Pipe = await endpointListener.GetPipeAsync(sessionContext, cancellationTokenSource.Token).ConfigureAwait(false); cancellationTokenSource.Token.ThrowIfCancellationRequested(); if (sessionContext.EndpointDefinition.IsSecure && _options.ServerCertificate != null) { await sessionContext.Pipe.UpgradeAsync(_options.ServerCertificate, _options.SupportedSslProtocols, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); } _sessions.Run(sessionContext, cancellationTokenSource.Token); } catch (OperationCanceledException) { } } }
/// <summary> /// Execute the command handler against the specified session context. /// </summary> /// <param name="context">The session context to execute the command handler against.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>A task which asynchronously performs the execution.</returns> async Task ExecuteAsync(SmtpSessionContext context, CancellationToken cancellationToken) { // The PROXY protocol requires that the receiver must wait for the // proxy command to be fully received before it starts processing the // session. Since the receiver is expected to speak first in SMTP, // i.e. sending the greeting on connect, we wait for the proxy // command to be consumed and processed before speaking to the // remote client. if (!_context.ServerOptions.Proxy) { await OutputGreetingAsync(cancellationToken).ReturnOnAnyThread(); } if (_context.ServerOptions.Proxy) { await IngestProxyAsync(context, cancellationToken).ReturnOnAnyThread(); } var retries = _context.ServerOptions.MaxRetryCount; while (retries-- > 0 && context.IsQuitRequested == false && cancellationToken.IsCancellationRequested == false) { var text = await ReadCommandInputAsync(context, cancellationToken); if (text == null) { return; } if (TryMake(context, text, out var command, out var response)) { try { if (await ExecuteAsync(command, context, cancellationToken).ReturnOnAnyThread()) { _stateMachine.Transition(context); } retries = _context.ServerOptions.MaxRetryCount; continue; } catch (SmtpResponseException responseException) { context.IsQuitRequested = responseException.IsQuitRequested; response = responseException.Response; } catch (OperationCanceledException) { await context.NetworkClient.ReplyAsync(new SmtpResponse(SmtpReplyCode.ServiceClosingTransmissionChannel, "The session has be cancelled."), cancellationToken); return; } } await context.NetworkClient.ReplyAsync(CreateErrorResponse(response, retries), cancellationToken); } }
/// <summary> /// Execute the command. /// </summary> /// <param name="command">The command to execute.</param> /// <param name="context">The execution context to operate on.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>A task which asynchronously performs the execution.</returns> static async Task <bool> ExecuteAsync(SmtpCommand command, SmtpSessionContext context, CancellationToken cancellationToken) { context.RaiseCommandExecuting(command); var result = await command.ExecuteAsync(context, cancellationToken); context.RaiseCommandExecuted(command); return(result); }
/// <summary> /// Constructor. /// </summary> /// <param name="options">The SMTP server options.</param> /// <param name="tcpClient">The TCP client to operate the session on.</param> /// <param name="stateMachine">The SMTP state machine.</param> internal SmtpSession(ISmtpServerOptions options, TcpClient tcpClient, SmtpStateMachine stateMachine) { _options = options; _tcpClient = tcpClient; Context = new SmtpSessionContext(new SmtpTransaction(), stateMachine, tcpClient.Client.RemoteEndPoint) { Text = new NetworkTextStream(tcpClient.GetStream()) }; }
/// <summary> /// Constructor. /// </summary> /// <param name="options">The SMTP server options.</param> /// <param name="tcpClient">The TCP client to operate the session on.</param> /// <param name="stateMachine">The SMTP state machine.</param> internal SmtpSession(ISmtpServerOptions options, TcpClient tcpClient, SmtpStateMachine stateMachine) { _options = options; _tcpClient = tcpClient; _stateMachine = stateMachine; Context = new SmtpSessionContext(new SmtpTransaction(), stateMachine, tcpClient.Client.RemoteEndPoint) { Text = new NetworkTextStream(tcpClient) }; }
/// <summary> /// Listen for SMTP traffic on the given endpoint. /// </summary> /// <param name="endpointDefinition">The definition of the endpoint to listen on.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>A task which performs the operation.</returns> async Task ListenAsync(IEndpointDefinition endpointDefinition, CancellationToken cancellationToken) { // keep track of the running tasks for disposal var sessions = new ConcurrentDictionary <SmtpSession, SmtpSession>(); using (var endpointListener = _options.EndpointListenerFactory.CreateListener(endpointDefinition)) { while (cancellationToken.IsCancellationRequested == false) { var sessionContext = new SmtpSessionContext(_options, endpointDefinition); // wait for a client connection var stream = await endpointListener.GetStreamAsync(sessionContext, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); sessionContext.NetworkClient = new NetworkClient(stream, _options.NetworkBufferSize, _options.NetworkBufferReadTimeout); if (endpointDefinition.IsSecure && _options.ServerCertificate != null) { await sessionContext.NetworkClient.UpgradeAsync(_options.ServerCertificate, _options.SupportedSslProtocols, cancellationToken); cancellationToken.ThrowIfCancellationRequested(); } // create a new session to handle the connection var session = new SmtpSession(sessionContext); sessions.TryAdd(session, session); OnSessionCreated(new SessionEventArgs(sessionContext)); session.Run(cancellationToken); #pragma warning disable 4014 session.Task .ContinueWith(t => { if (sessions.TryRemove(session, out var s)) { sessionContext.NetworkClient.Dispose(); } OnSessionCompleted(new SessionEventArgs(sessionContext)); }, cancellationToken); #pragma warning restore 4014 } // the server has been cancelled, wait for the tasks to complete await Task.WhenAll(sessions.Keys.Select(s => s.Task)).ConfigureAwait(false); } }
/// <summary> /// Execute the command handler against the specified session context. /// </summary> /// <param name="context">The session context to execute the command handler against.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>A task which asynchronously performs the execution.</returns> async Task ExecuteAsync(SmtpSessionContext context, CancellationToken cancellationToken) { var retries = _context.ServerOptions.MaxRetryCount; while (retries-- > 0 && context.IsQuitRequested == false && cancellationToken.IsCancellationRequested == false) { var text = await ReadCommandInputAsync(context, cancellationToken).ConfigureAwait(false); if (text == null) { return; } if (TryMake(context, text, out var command, out var response) == false) { await context.NetworkClient.ReplyAsync(CreateErrorResponse(response, retries), cancellationToken).ConfigureAwait(false); continue; } try { if (await ExecuteAsync(command, context, cancellationToken).ConfigureAwait(false)) { _stateMachine.Transition(context); } retries = _context.ServerOptions.MaxRetryCount; } catch (SmtpResponseException responseException) when(responseException.IsQuitRequested) { await context.NetworkClient.ReplyAsync(responseException.Response, cancellationToken).ConfigureAwait(false); return; } catch (SmtpResponseException responseException) { response = CreateErrorResponse(responseException.Response, retries); await context.NetworkClient.ReplyAsync(response, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { await context.NetworkClient.ReplyAsync(new SmtpResponse(SmtpReplyCode.ServiceClosingTransmissionChannel, "The session has be cancelled."), cancellationToken).ConfigureAwait(false); return; } } }
internal void Run(SmtpSessionContext sessionContext, CancellationToken cancellationToken) { var handle = new SmtpSessionHandle(new SmtpSession(sessionContext), sessionContext); Add(handle); handle.CompletionTask = RunAsync(handle, cancellationToken); // ReSharper disable once MethodSupportsCancellation handle.CompletionTask.ContinueWith( task => { Remove(handle); }); }
/// <summary> /// Read the command input. /// </summary> /// <param name="context">The session context to execute the command handler against.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The input that was received from the client.</returns> async Task <IReadOnlyList <ArraySegment <byte> > > ReadCommandInputAsync(SmtpSessionContext context, CancellationToken cancellationToken) { try { return(await context.Client.ReadLineAsync(_options.CommandWaitTimeout, cancellationToken).ReturnOnAnyThread()); } catch (TimeoutException) { await context.Client.ReplyAsync(new SmtpResponse(SmtpReplyCode.ServiceClosingTransmissionChannel, "Timeout whilst waiting for input."), cancellationToken); } catch (OperationCanceledException) { await context.Client.ReplyAsync(new SmtpResponse(SmtpReplyCode.ServiceClosingTransmissionChannel, "The session has be cancelled."), cancellationToken); } return(null); }
/// <summary> /// Execute the command handler against the specified session context. /// </summary> /// <param name="context">The session context to execute the command handler against.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>A task which asynchronously performs the execution.</returns> async Task ExecuteAsync(SmtpSessionContext context, CancellationToken cancellationToken) { var retries = _context.ServerOptions.MaxRetryCount; while (retries-- > 0 && context.IsQuitRequested == false && cancellationToken.IsCancellationRequested == false) { try { var command = await ReadCommandAsync(context, cancellationToken).ConfigureAwait(false); if (command == null) { return; } if (_stateMachine.TryAccept(command, out var errorResponse) == false) { throw new SmtpResponseException(errorResponse); } if (await ExecuteAsync(command, context, cancellationToken).ConfigureAwait(false)) { _stateMachine.Transition(context); } retries = _context.ServerOptions.MaxRetryCount; } catch (SmtpResponseException responseException) when(responseException.IsQuitRequested) { await context.Pipe.Output.WriteReplyAsync(responseException.Response, cancellationToken).ConfigureAwait(false); context.IsQuitRequested = true; } catch (SmtpResponseException responseException) { var response = CreateErrorResponse(responseException.Response, retries); await context.Pipe.Output.WriteReplyAsync(response, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { await context.Pipe.Output.WriteReplyAsync(new SmtpResponse(SmtpReplyCode.ServiceClosingTransmissionChannel, "The session has be cancelled."), CancellationToken.None).ConfigureAwait(false); } } }
async Task IngestProxyAsync(SmtpSessionContext context, CancellationToken cancellationToken) { var text = await ReadCommandInputAsync(context, cancellationToken); if (TryMake(context, text, out SmtpCommand command, out SmtpResponse response)) { if (await ExecuteAsync(command, context, cancellationToken).ReturnOnAnyThread()) { await OutputGreetingAsync(cancellationToken).ReturnOnAnyThread(); await _context.NetworkClient.FlushAsync(cancellationToken).ReturnOnAnyThread(); } else { await _context.NetworkClient.FlushAsync(cancellationToken).ReturnOnAnyThread(); } } }
/// <summary> /// Execute the command handler against the specified session context. /// </summary> /// <param name="context">The session context to execute the command handler against.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>A task which asynchronously performs the execution.</returns> async Task ExecuteAsync(SmtpSessionContext context, CancellationToken cancellationToken) { var retries = _options.MaxRetryCount; while (retries-- > 0 && context.IsQuitRequested == false && cancellationToken.IsCancellationRequested == false) { var text = await ReadCommandInputAsync(context, cancellationToken); if (text == null) { return; } if (TryAccept(context, text, out SmtpCommand command, out SmtpResponse response)) { try { await ExecuteAsync(command, context, cancellationToken).ReturnOnAnyThread(); retries = _options.MaxRetryCount; continue; } catch (SmtpResponseException responseException) { context.IsQuitRequested = responseException.IsQuitRequested; response = responseException.Response; } catch (OperationCanceledException) { await context.Client.ReplyAsync(new SmtpResponse(SmtpReplyCode.ServiceClosingTransmissionChannel, "The session has be cancelled."), cancellationToken); return; } } await context.Client.ReplyAsync(CreateErrorResponse(response, retries), cancellationToken); } }
async Task ListenAsync(SmtpSessionContext sessionContext, IEndpointListener endpointListener, CancellationToken cancellationToken) { // The listener can be stopped either by the caller cancelling the CancellationToken used when starting the server, or when calling // the shutdown method. The Shutdown method will stop the listeners and allow any active sessions to finish gracefully. var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_shutdownTokenSource.Token, cancellationToken); // wait for a client connection var stream = await endpointListener.GetStreamAsync(sessionContext, cancellationTokenSource.Token).ConfigureAwait(false); cancellationTokenSource.Token.ThrowIfCancellationRequested(); sessionContext.NetworkClient = new NetworkClient(stream, _options.NetworkBufferSize); if (sessionContext.EndpointDefinition.IsSecure && _options.ServerCertificate != null) { await sessionContext.NetworkClient.Stream.UpgradeAsync(_options.ServerCertificate, _options.SupportedSslProtocols, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); } _sessions.Run(sessionContext, cancellationToken); }
public void Run(SmtpSessionContext sessionContext, CancellationToken cancellationToken) { var session = new SmtpSession(sessionContext); Add(session); _smtpServer.OnSessionCreated(new SessionEventArgs(sessionContext)); session.Run( exception => { Remove(session); sessionContext.NetworkClient.Dispose(); if (exception != null) { _smtpServer.OnSessionFaulted(new SessionFaultedEventArgs(sessionContext, exception)); } _smtpServer.OnSessionCompleted(new SessionEventArgs(sessionContext)); }, cancellationToken); }
/// <summary> /// Advances the enumerator to the next command in the stream. /// </summary> /// <param name="context">The session context to execute the command handler against.</param> /// <param name="text">The text to return the commands from.</param> /// <param name="command">The command that was found.</param> /// <param name="errorResponse">The error response that indicates why a command could not be accepted.</param> /// <returns>true if a valid command was found, false if not.</returns> bool TryAccept(SmtpSessionContext context, string text, out SmtpCommand command, out SmtpResponse errorResponse) { return(context.StateMachine.TryAccept(new TokenEnumerator(new StringTokenReader(text)), out command, out errorResponse)); }
/// <summary> /// Constructor. /// </summary> /// <param name="context">The session context.</param> internal SmtpSession(SmtpSessionContext context) { _context = context; _stateMachine = new SmtpStateMachine(_context); }
/// <summary> /// Execute the command. /// </summary> /// <param name="command">The command to execute.</param> /// <param name="context">The execution context to operate on.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>A task which asynchronously performs the execution.</returns> Task <bool> ExecuteAsync(SmtpCommand command, SmtpSessionContext context, CancellationToken cancellationToken) { context.RaiseCommandExecuting(command); return(command.ExecuteAsync(context, cancellationToken)); }
/// <summary> /// Advances the enumerator to the next command in the stream. /// </summary> /// <param name="context">The session context to use when making session based transitions.</param> /// <param name="segments">The list of array segments to read the command from.</param> /// <param name="command">The command that was found.</param> /// <param name="errorResponse">The error response that indicates why a command could not be accepted.</param> /// <returns>true if a valid command was found, false if not.</returns> bool TryMake(SmtpSessionContext context, IReadOnlyList <ArraySegment <byte> > segments, out SmtpCommand command, out SmtpResponse errorResponse) { var tokenEnumerator = new TokenEnumerator(new ByteArrayTokenReader(segments)); return(_stateMachine.TryMake(context, tokenEnumerator, out command, out errorResponse)); }
public SmtpSessionHandle(SmtpSession session, SmtpSessionContext sessionContext) { Session = session; SessionContext = sessionContext; }
async Task <ISecurableDuplexPipe> GetPipeAsync(IEndpointListener endpointListener, SmtpSessionContext sessionContext, CancellationToken cancellationToken) { var pipe = await endpointListener.GetPipeAsync(sessionContext, cancellationToken).ConfigureAwait(false); try { cancellationToken.ThrowIfCancellationRequested(); if (sessionContext.EndpointDefinition.IsSecure && _options.ServerCertificate != null) { await pipe.UpgradeAsync(_options.ServerCertificate, _options.SupportedSslProtocols, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); } } catch { pipe.Dispose(); throw; } return(pipe); }
/// <summary> /// Constructor. /// </summary> /// <param name="context">The session context.</param> internal SmtpSession(SmtpSessionContext context) { _context = context; _stateMachine = new SmtpStateMachine(_context); _commandFactory = context.ServiceProvider.GetServiceOrDefault <ISmtpCommandFactory>(new SmtpCommandFactory()); }