Example #1
0
        /// <summary>
        /// Create a byte array containing a signed S/MIME message.
        /// </summary>
        /// <param name="buffer">A byte array to streamline bit shuffling.</param>
        /// <param name="contentBytes">The contents of the envelope to be encrypted.</param>
        /// <param name="message">An OpaqueMail.MailMessage that contains the message to send.</param>
        /// <param name="alreadyEncrypted">Whether a portion of the message has previously been signed, as when triple wrapping.</param>
        private byte[] SmimeSign(byte[] buffer, byte[] contentBytes, MailMessage message, bool alreadyEncrypted)
        {
            if (message.SmimeSigningCertificate == null)
            {
                // If the implementation requires S/MIME signing (the default), throw an error if there's no certificate.
                if ((message.SmimeSettingsMode & SmimeSettingsMode.RequireExactSettings) > 0)
                {
                    throw new SmtpException("Trying to send a signed message, but no signing certificate has been assigned.");
                }
                else
                {
                    return(contentBytes);
                }
            }

            // First, create a buffer for tracking the unsigned portion of this message.
            StringBuilder unsignedMessageBuilder = new StringBuilder(Constants.SMALLSBSIZE);

            // If triple wrapping, the previous layer was an encrypted envelope and needs to be Base64 encoded.
            if (alreadyEncrypted)
            {
                unsignedMessageBuilder.Append("Content-Type: application/pkcs7-mime; smime-type=enveloped-data;\r\n\tname=\"smime.p7m\"\r\n");
                unsignedMessageBuilder.Append("Content-Transfer-Encoding: base64\r\n");
                unsignedMessageBuilder.Append("Content-Description: \"S/MIME Cryptographic envelopedCms\"\r\n");
                unsignedMessageBuilder.Append("Content-Disposition: attachment; filename=\"smime.p7m\"\r\n\r\n");

                unsignedMessageBuilder.Append(Functions.ToBase64String(contentBytes));
            }
            else
            {
                unsignedMessageBuilder.Append(Encoding.UTF8.GetString(contentBytes));
            }

            // Prepare the signing parameters.
            ContentInfo contentInfo = new ContentInfo(Encoding.UTF8.GetBytes(unsignedMessageBuilder.ToString()));
            SignedCms   signedCms   = new SignedCms(contentInfo, true);

            CmsSigner signer = new CmsSigner(message.SubjectIdentifierType, message.SmimeSigningCertificate);

            signer.IncludeOption = X509IncludeOption.WholeChain;

            // Sign the current time.
            if ((message.SmimeSigningOptionFlags & SmimeSigningOptionFlags.SignTime) > 0)
            {
                Pkcs9SigningTime signingTime = new Pkcs9SigningTime();
                signer.SignedAttributes.Add(signingTime);
            }

            // Encode the signed message.
            signedCms.ComputeSignature(signer);
            byte[] signedBytes = signedCms.Encode();

            // Embed the signed and original version of the message using MIME.
            StringBuilder messageBuilder = new StringBuilder(Constants.SMALLSBSIZE);

            // Build the MIME message by embedding the unsigned and signed portions.
            messageBuilder.Append("This is a multi-part S/MIME signed message.\r\n\r\n");
            messageBuilder.Append("--" + (alreadyEncrypted ? SmimeTripleSignedCmsBoundaryName : SmimeSignedCmsBoundaryName) + "\r\n");
            messageBuilder.Append(unsignedMessageBuilder.ToString());
            messageBuilder.Append("\r\n--" + (alreadyEncrypted ? SmimeTripleSignedCmsBoundaryName : SmimeSignedCmsBoundaryName) + "\r\n");
            messageBuilder.Append("Content-Type: application/x-pkcs7-signature; smime-type=signed-data; name=\"smime.p7s\"\r\n");
            messageBuilder.Append("Content-Transfer-Encoding: base64\r\n");
            messageBuilder.Append("Content-Description: \"S/MIME Cryptographic signedCms\"\r\n");
            messageBuilder.Append("Content-Disposition: attachment; filename=\"smime.p7s\"\r\n\r\n");
            messageBuilder.Append(Functions.ToBase64String(signedBytes, 0, signedBytes.Length));
            messageBuilder.Append("\r\n--" + (alreadyEncrypted ? SmimeTripleSignedCmsBoundaryName : SmimeSignedCmsBoundaryName) + "--\r\n");

            return(Encoding.UTF8.GetBytes(messageBuilder.ToString()));
        }
Example #2
0
        /// <summary>
        /// Helper function for sending the specified message to an SMTP server for delivery with S/MIME encoding.
        /// </summary>
        /// <param name="message">An OpaqueMail.MailMessage that contains the message to send.</param>
        private async Task SmimeSendAsync(MailMessage message)
        {
            // Require one or more recipients.
            if (message.To.Count + message.CC.Count + message.Bcc.Count < 1)
            {
                throw new SmtpException("One or more recipients must be specified via the '.To', '.CC', or '.Bcc' collections.");
            }

            // Require a signing certificate to be specified.
            if ((message.SmimeSigned || message.SmimeTripleWrapped) && message.SmimeSigningCertificate == null)
            {
                throw new SmtpException("A signing certificate must be passed prior to signing.");
            }

            // Ensure the rendering engine expects MIME encoding.
            message.Headers["MIME-Version"] = "1.0";

            // OpaqueMail optional setting for protecting the subject.
            // Note: This is not part of the current RFC specifcation and should only be used when sending to other OpaqueMail agents.
            if ((message.SmimeEncryptedEnvelope || message.SmimeTripleWrapped) && (message.SmimeEncryptionOptionFlags & (SmimeEncryptionOptionFlags.EncryptSubject)) > 0)
            {
                message.Headers["X-Subject-Encryption"] = "true";
                message.Body    = "Subject: " + message.Subject + "\r\n" + message.Body;
                message.Subject = Guid.NewGuid().ToString();
            }

            // Generate a multipart/mixed message containing the e-mail's body, alternate views, and attachments.
            byte[] MIMEMessageBytes = await message.MIMEEncode(SmimeBoundaryName, SmimeAlternativeViewBoundaryName);

            message.Headers["Content-Type"] = "multipart/mixed; boundary=\"" + SmimeBoundaryName + "\"";
            message.Headers["Content-Transfer-Encoding"] = "7bit";

            // Skip the MIME header.
            message.Body = Encoding.UTF8.GetString(MIMEMessageBytes);
            message.Body = message.Body.Substring(message.Body.IndexOf("\r\n\r\n") + 4);

            // Handle S/MIME signing.
            bool successfullySigned = false;

            if (message.SmimeSigned || message.SmimeTripleWrapped)
            {
                int unsignedSize = MIMEMessageBytes.Length;
                MIMEMessageBytes   = SmimeSign(buffer, MIMEMessageBytes, message, false);
                successfullySigned = MIMEMessageBytes.Length != unsignedSize;

                if (successfullySigned)
                {
                    // Remove any prior content dispositions.
                    if (message.Headers["Content-Disposition"] != null)
                    {
                        message.Headers.Remove("Content-Disposition");
                    }

                    message.Headers["Content-Type"] = "multipart/signed; protocol=\"application/x-pkcs7-signature\"; micalg=sha1;\r\n\tboundary=\"" + SmimeSignedCmsBoundaryName + "\"";
                    message.Headers["Content-Transfer-Encoding"] = "7bit";
                    message.Body = Encoding.UTF8.GetString(MIMEMessageBytes);
                }
            }

            // Handle S/MIME envelope encryption.
            bool successfullyEncrypted = false;

            if (message.SmimeEncryptedEnvelope || message.SmimeTripleWrapped)
            {
                int unencryptedSize = MIMEMessageBytes.Length;
                MIMEMessageBytes      = SmimeEncryptEnvelope(MIMEMessageBytes, message, successfullySigned);
                successfullyEncrypted = MIMEMessageBytes.Length != unencryptedSize;

                // If the message won't be triple-wrapped, wrap the encrypted message with MIME.
                if (successfullyEncrypted && (!successfullySigned || !message.SmimeTripleWrapped))
                {
                    message.Headers["Content-Type"] = "application/pkcs7-mime; name=smime.p7m;\r\n\tsmime-type=enveloped-data";
                    message.Headers["Content-Transfer-Encoding"] = "base64";

                    message.Body = Functions.ToBase64String(MIMEMessageBytes) + "\r\n";
                }
            }

            // Handle S/MIME triple wrapping (i.e. signing, envelope encryption, then signing again).
            if (successfullyEncrypted)
            {
                if (message.SmimeTripleWrapped)
                {
                    message.Headers["Content-Type"] = "multipart/signed; protocol=\"application/x-pkcs7-signature\"; micalg=sha1;\r\n\tboundary=\"" + SmimeTripleSignedCmsBoundaryName + "\"";
                    message.Headers["Content-Transfer-Encoding"] = "7bit";

                    message.Body = Encoding.UTF8.GetString(SmimeSign(buffer, MIMEMessageBytes, message, true));
                }
                else
                {
                    message.Headers["Content-Disposition"] = "attachment; filename=smime.p7m";
                }
            }

            await SmimeSendRawAsync(message);
        }
Example #3
0
        /// <summary>
        /// Create a byte array containing an encrypted S/MIME envelope.
        /// </summary>
        /// <param name="contentBytes">The contents of the envelope to be encrypted.</param>
        /// <param name="message">An OpaqueMail.MailMessage that contains the message to send.</param>
        private byte[] SmimeEncryptEnvelope(byte[] contentBytes, MailMessage message, bool alreadySigned)
        {
            // Resolve recipient public keys.
            Dictionary <string, MailAddress> addressesNeedingPublicKeys;
            HashSet <string> addressesWithPublicKeys;

            ResolvePublicKeys(message, out addressesWithPublicKeys, out addressesNeedingPublicKeys);

            // Throw an error if we're unable to encrypt the message for one or more recipients and encryption is explicitly required.
            if (addressesNeedingPublicKeys.Count > 0)
            {
                // If the implementation requires S/MIME encryption (the default), throw an error if there's no certificate.
                if ((message.SmimeSettingsMode & SmimeSettingsMode.RequireExactSettings) > 0)
                {
                    StringBuilder exceptionMessage = new StringBuilder(Constants.TINYSBSIZE);
                    exceptionMessage.Append("Trying to send encrypted message to one or more recipients without a trusted public key.\r\nRecipients without public keys: ");
                    foreach (string addressNeedingPublicKey in addressesNeedingPublicKeys.Keys)
                    {
                        exceptionMessage.Append(addressNeedingPublicKey + ", ");
                    }
                    exceptionMessage.Remove(exceptionMessage.Length - 2, 2);

                    throw new SmtpException(exceptionMessage.ToString());
                }
                else
                {
                    return(contentBytes);
                }
            }

            if (alreadySigned)
            {
                // If already signed, prepend S/MIME headers.
                StringBuilder contentBuilder = new StringBuilder(Constants.TINYSBSIZE);
                contentBuilder.Append("Content-Type: multipart/signed; protocol=\"application/x-pkcs7-signature\"; micalg=sha1;\r\n\tboundary=\"" + SmimeSignedCmsBoundaryName + "\"\r\n");
                contentBuilder.Append("Content-Transfer-Encoding: 7bit\r\n\r\n");

                contentBytes = Encoding.UTF8.GetBytes(contentBuilder.ToString() + Encoding.UTF8.GetString(contentBytes));
            }

            // Prepare the encryption envelope.
            ContentInfo  contentInfo = new ContentInfo(contentBytes);
            EnvelopedCms envelope;

            // If a specific algorithm is specified, choose that.  Otherwise, negotiate which algorithm to use.
            if (SmimeAlgorithmIdentifier != null)
            {
                envelope = new EnvelopedCms(contentInfo, SmimeAlgorithmIdentifier);
            }
            else
            {
                envelope = new EnvelopedCms(contentInfo);
            }

            // Encrypt the symmetric session key using each recipient's public key.
            foreach (string addressWithPublicKey in addressesWithPublicKeys)
            {
                CmsRecipient recipient = new CmsRecipient(SmimeCertificateCache[addressWithPublicKey]);
                envelope.Encrypt(recipient);
            }

            return(envelope.Encode());
        }
Example #4
0
        /// <summary>
        /// Sends the specified message to an SMTP server for delivery without making modifications to the body.
        /// Necessary because the standard SmtpClient.Send() may slightly alter messages, invalidating signatures.
        /// </summary>
        /// <param name="message">An OpaqueMail.MailMessage that contains the message to send.</param>
        private async Task SmimeSendRawAsync(MailMessage message)
        {
            // Connect to the SMTP server.
            TcpClient SmtpTcpClient = new TcpClient();

            SmtpTcpClient.Connect(Host, Port);
            Stream SmtpStream = SmtpTcpClient.GetStream();

            // Use stream readers and writers to simplify I/O.
            StreamReader reader = new StreamReader(SmtpStream);
            StreamWriter writer = new StreamWriter(SmtpStream);

            writer.AutoFlush = true;

            // Read the welcome message.
            string response = await reader.ReadLineAsync();

            // Send EHLO and find out server capabilities.
            await writer.WriteLineAsync("EHLO " + Host);

            char[] charBuffer = new char[Constants.SMALLBUFFERSIZE];
            int    bytesRead  = await reader.ReadAsync(charBuffer, 0, Constants.SMALLBUFFERSIZE);

            response = new string(charBuffer, 0, bytesRead);
            if (!response.StartsWith("2"))
            {
                throw new SmtpException("Unable to connect to remote server '" + Host + "'.  Sent 'EHLO' and received '" + response + "'.");
            }

            // Stand up a TLS/SSL stream.
            if (EnableSsl)
            {
                await writer.WriteLineAsync("STARTTLS");

                response = await reader.ReadLineAsync();

                if (!response.StartsWith("2"))
                {
                    throw new SmtpException("Unable to start TLS/SSL protection with '" + Host + "'.  Received '" + response + "'.");
                }

                SmtpStream = new SslStream(SmtpStream);
                ((SslStream)SmtpStream).AuthenticateAsClient(Host);

                reader           = new StreamReader(SmtpStream);
                writer           = new StreamWriter(SmtpStream);
                writer.AutoFlush = true;
            }

            // Authenticate using the AUTH LOGIN command.
            if (Credentials != null)
            {
                NetworkCredential cred = (NetworkCredential)Credentials;
                await writer.WriteLineAsync("AUTH LOGIN");

                response = await reader.ReadLineAsync();

                if (!response.StartsWith("3"))
                {
                    throw new SmtpException("Unable to authenticate with server '" + Host + "'.  Received '" + response + "'.");
                }
                await writer.WriteLineAsync(Functions.ToBase64String(cred.UserName));

                response = await reader.ReadLineAsync();

                await writer.WriteLineAsync(Functions.ToBase64String(cred.Password));

                response = await reader.ReadLineAsync();

                if (!response.StartsWith("2"))
                {
                    throw new SmtpException("Unable to authenticate with server '" + Host + "'.  Received '" + response + "'.");
                }
            }

            // Build our raw headers block.
            StringBuilder rawHeaders = new StringBuilder(Constants.SMALLSBSIZE);

            // Specify who the message is from.
            rawHeaders.Append(Functions.SpanHeaderLines("From: " + Functions.EncodeMailHeader(Functions.ToMailAddressString(message.From))) + "\r\n");
            await writer.WriteLineAsync("MAIL FROM:<" + message.From.Address + ">");

            response = await reader.ReadLineAsync();

            if (!response.StartsWith("2"))
            {
                throw new SmtpException("Exception communicating with server '" + Host + "'.  Sent 'MAIL FROM' and received '" + response + "'.");
            }

            // Identify all recipients of the message.
            if (message.To.Count > 0)
            {
                rawHeaders.Append(Functions.SpanHeaderLines("To: " + Functions.EncodeMailHeader(Functions.ToMailAddressString(message.To))) + "\r\n");
            }
            foreach (MailAddress address in message.To)
            {
                await writer.WriteLineAsync("RCPT TO:<" + address.Address + ">");

                response = await reader.ReadLineAsync();

                if (!response.StartsWith("2"))
                {
                    throw new SmtpException("Exception communicating with server '" + Host + "'.  Sent 'RCPT TO' and received '" + response + "'.");
                }
            }

            if (message.CC.Count > 0)
            {
                rawHeaders.Append(Functions.SpanHeaderLines("CC: " + Functions.EncodeMailHeader(Functions.ToMailAddressString(message.CC))) + "\r\n");
            }
            foreach (MailAddress address in message.CC)
            {
                await writer.WriteLineAsync("RCPT TO:<" + address.Address + ">");

                response = await reader.ReadLineAsync();

                if (!response.StartsWith("2"))
                {
                    throw new SmtpException("Exception communicating with server '" + Host + "'.  Sent 'RCPT TO' and received '" + response + "'.");
                }
            }

            foreach (MailAddress address in message.Bcc)
            {
                await writer.WriteLineAsync("RCPT TO:<" + address.Address + ">");

                response = await reader.ReadLineAsync();

                if (!response.StartsWith("2"))
                {
                    throw new SmtpException("Exception communicating with server '" + Host + "'.  Sent 'RCPT TO' and received '" + response + "'.");
                }
            }

            // Send the raw message.
            await writer.WriteLineAsync("DATA");

            response = await reader.ReadLineAsync();

            if (!response.StartsWith("3"))
            {
                throw new SmtpException("Exception communicating with server '" + Host + "'.  Sent 'DATA' and received '" + response + "'.");
            }

            // If a read-only mail message is passed in with its raw headers and body, save a few steps by sending that directly.
            if (message is ReadOnlyMailMessage)
            {
                await writer.WriteAsync(((ReadOnlyMailMessage)message).RawHeaders + "\r\n" + ((ReadOnlyMailMessage)message).RawBody + "\r\n.\r\n");
            }
            else
            {
                rawHeaders.Append(Functions.SpanHeaderLines("Subject: " + Functions.EncodeMailHeader(message.Subject)) + "\r\n");
                foreach (string rawHeader in message.Headers)
                {
                    switch (rawHeader.ToUpper())
                    {
                    case "BCC":
                    case "CC":
                    case "FROM":
                    case "SUBJECT":
                    case "TO":
                        break;

                    default:
                        rawHeaders.Append(Functions.SpanHeaderLines(rawHeader + ": " + message.Headers[rawHeader]) + "\r\n");
                        break;
                    }
                }

                await writer.WriteAsync(rawHeaders.ToString() + "\r\n" + message.Body + "\r\n.\r\n");
            }

            response = await reader.ReadLineAsync();

            if (!response.StartsWith("2"))
            {
                throw new SmtpException("Exception communicating with server '" + Host + "'.  Sent message and received '" + response + "'.");
            }

            // Clean up this connection.
            await writer.WriteLineAsync("QUIT");

            writer.Dispose();
            reader.Dispose();

            SmtpStream.Dispose();
            SmtpTcpClient.Close();
        }
Example #5
0
        /// <summary>
        /// Helper function to look up and validate public keys for each recipient.
        /// </summary>
        /// <param name="message">An OpaqueMail.MailMessage that contains the message to send.</param>
        /// <param name="addressesWithPublicKeys">Collection containing recipients with valid public keys.</param>
        /// <param name="addressesNeedingPublicKeys">Collection containing recipients without valid public keys.</param>
        private void ResolvePublicKeys(MailMessage message, out HashSet <string> addressesWithPublicKeys, out Dictionary <string, MailAddress> addressesNeedingPublicKeys)
        {
            // Initialize collections for all recipients.
            addressesWithPublicKeys    = new HashSet <string>();
            addressesNeedingPublicKeys = new Dictionary <string, MailAddress>();

            MailAddressCollection[] addressRanges = new MailAddressCollection[] { message.To, message.CC, message.Bcc };
            foreach (MailAddressCollection addressRange in addressRanges)
            {
                foreach (MailAddress toAddress in addressRange)
                {
                    string canonicalToAddress = toAddress.Address.ToUpper();
                    if (SmimeCertificateCache.ContainsKey(canonicalToAddress))
                    {
                        if (!addressesWithPublicKeys.Contains(canonicalToAddress))
                        {
                            addressesWithPublicKeys.Add(canonicalToAddress);
                        }
                    }
                    else
                    {
                        if (!addressesNeedingPublicKeys.ContainsKey(canonicalToAddress))
                        {
                            addressesNeedingPublicKeys.Add(canonicalToAddress, toAddress);
                        }
                    }
                }
            }

            // If any addresses haven't been mapped to public keys, map them.
            if (addressesNeedingPublicKeys.Count > 0)
            {
                // Read from the Windows certificate store if valid certificates aren't specified.
                if (SmimeValidCertificates == null || SmimeValidCertificates.Count < 1)
                {
                    // Load from the current user.
                    X509Store store = new X509Store(StoreLocation.CurrentUser);
                    store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
                    SmimeValidCertificates = store.Certificates;
                    store.Close();

                    // Add any tied to the local machine.
                    store = new X509Store(StoreLocation.LocalMachine);
                    store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
                    SmimeValidCertificates.AddRange(store.Certificates);
                    store.Close();
                }

                // Loop through certificates and check for matching recipients.
                foreach (X509Certificate2 cert in SmimeValidCertificates)
                {
                    // Look at certificates with e-mail subject names.
                    string canonicalCertSubject = "";
                    if (cert.Subject.StartsWith("E="))
                    {
                        canonicalCertSubject = cert.Subject.Substring(2).ToUpper();
                    }
                    else if (cert.Subject.StartsWith("CN="))
                    {
                        canonicalCertSubject = cert.Subject.Substring(3).ToUpper();
                    }
                    else
                    {
                        canonicalCertSubject = cert.Subject.ToUpper();
                    }

                    int certSubjectComma = canonicalCertSubject.IndexOf(",");
                    if (certSubjectComma > -1)
                    {
                        canonicalCertSubject = canonicalCertSubject.Substring(0, certSubjectComma);
                    }

                    // Only proceed if the key is for a recipient of this e-mail.
                    if (!addressesNeedingPublicKeys.ContainsKey(canonicalCertSubject))
                    {
                        continue;
                    }

                    // Verify the certificate chain.
                    if ((message.SmimeEncryptionOptionFlags & SmimeEncryptionOptionFlags.RequireCertificateVerification) > 0)
                    {
                        if (!cert.Verify())
                        {
                            continue;
                        }
                    }

                    // Ensure valid key usage scenarios.
                    if ((message.SmimeEncryptionOptionFlags & SmimeEncryptionOptionFlags.RequireKeyUsageOfDataEncipherment) > 0 || (message.SmimeEncryptionOptionFlags & SmimeEncryptionOptionFlags.RequireEnhancedKeyUsageofSecureEmail) > 0)
                    {
                        bool keyDataEncipherment = false, enhancedKeySecureEmail = false;
                        foreach (X509Extension extension in cert.Extensions)
                        {
                            if (!keyDataEncipherment && extension.Oid.FriendlyName == "Key Usage")
                            {
                                X509KeyUsageExtension ext = (X509KeyUsageExtension)extension;
                                if ((ext.KeyUsages & X509KeyUsageFlags.DataEncipherment) != X509KeyUsageFlags.None)
                                {
                                    keyDataEncipherment = true;

                                    if (!((message.SmimeEncryptionOptionFlags & SmimeEncryptionOptionFlags.RequireEnhancedKeyUsageofSecureEmail) > 0))
                                    {
                                        break;
                                    }
                                }
                            }
                            if (!enhancedKeySecureEmail && extension.Oid.FriendlyName == "Enhanced Key Usage")
                            {
                                X509EnhancedKeyUsageExtension ext = (X509EnhancedKeyUsageExtension)extension;
                                OidCollection oids = ext.EnhancedKeyUsages;
                                foreach (Oid oid in oids)
                                {
                                    if (oid.FriendlyName == "Secure Email")
                                    {
                                        enhancedKeySecureEmail = true;
                                        break;
                                    }
                                }
                            }
                        }
                        if ((message.SmimeEncryptionOptionFlags & SmimeEncryptionOptionFlags.RequireKeyUsageOfDataEncipherment) > 0 && !keyDataEncipherment)
                        {
                            continue;
                        }
                        if ((message.SmimeEncryptionOptionFlags & SmimeEncryptionOptionFlags.RequireEnhancedKeyUsageofSecureEmail) > 0 && !enhancedKeySecureEmail)
                        {
                            continue;
                        }
                    }

                    // If we've made it this far, we can use the certificate for a recipient.
                    MailAddress originalAddress = addressesNeedingPublicKeys[canonicalCertSubject];
                    SmimeCertificateCache.Add(canonicalCertSubject, cert);
                    addressesWithPublicKeys.Add(canonicalCertSubject);
                    addressesNeedingPublicKeys.Remove(canonicalCertSubject);

                    // Shortcut to abort processing of additional certificates if all recipients are accounted for.
                    if (addressesNeedingPublicKeys.Count < 1)
                    {
                        break;
                    }
                }
            }
        }
Example #6
0
        /// <summary>
        /// Handle an incoming SMTP connection, from connection to completion.
        /// </summary>
        /// <param name="parameters">SmtpProxyConnectionArguments object containing all parameters for this connection.</param>
        private async void ProcessConnection(object parameters)
        {
            // Cast the passed-in parameters back to their original objects.
            SmtpProxyConnectionArguments arguments = (SmtpProxyConnectionArguments)parameters;

            // The overall number of bytes transmitted on this connection.
            ulong bytesTransmitted = 0;

            TcpClient client = null;
            Stream clientStream = null;
            StreamReader clientStreamReader = null;
            StreamWriter clientStreamWriter = null;

            string ip = "";

            try
            {
                client = arguments.TcpClient;
                clientStream = client.GetStream();

                // Placeholder variables to be populated throughout the client session.
                NetworkCredential credential = arguments.RemoteServerCredential;
                string fromAddress = "";
                string identity = "";
                List<string> toList = new List<string>();
                bool sending = false, inPlainAuth = false, inLoginAuth = false;

                // A byte array to streamline bit shuffling.
                char[] buffer = new char[Constants.SMALLBUFFERSIZE];

                // Capture the client's IP information.
                PropertyInfo pi = clientStream.GetType().GetProperty("Socket", BindingFlags.NonPublic | BindingFlags.Instance);
                ip = ((Socket)pi.GetValue(clientStream, null)).RemoteEndPoint.ToString();
                if (ip.IndexOf(":") > -1)
                    ip = ip.Substring(0, ip.IndexOf(":"));

                // If the IP address range filter contains the localhost entry 0.0.0.0, check if the client IP is a local address and update it to 0.0.0.0 if so.
                if (arguments.AcceptedIPs.IndexOf("0.0.0.0") > -1)
                {
                    if (ip == "127.0.0.1")
                        ip = "0.0.0.0";
                    else
                    {
                        IPHostEntry hostEntry = Dns.GetHostEntry(Dns.GetHostName());
                        foreach (IPAddress hostIP in hostEntry.AddressList)
                        {
                            if (hostIP.ToString() == ip)
                            {
                                ip = "0.0.0.0";
                                break;
                            }
                        }
                    }
                }

                clientStreamReader = new StreamReader(clientStream);
                clientStreamWriter = new StreamWriter(clientStream);
                clientStreamWriter.AutoFlush = true;

                // Validate that the IP address is within an accepted range.
                if (!ProxyFunctions.ValidateIP(arguments.AcceptedIPs, ip))
                {
                    ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "Connection rejected from {" + ip + "} due to its IP address.", Proxy.LogLevel.Warning, LogLevel);

                    await Functions.SendStreamStringAsync(clientStreamWriter, "500 IP address [" + ip + "] rejected.\r\n");
                    ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "S: 500 IP address [" + ip + "] rejected.", Proxy.LogLevel.Raw, LogLevel);

                    if (clientStream != null)
                        clientStream.Dispose();
                    if (client != null)
                        client.Close();

                    return;
                }

                ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "New connection established from {" + ip + "}.", Proxy.LogLevel.Information, LogLevel);

                // Send our welcome message.
                await Functions.SendStreamStringAsync(clientStreamWriter, "220 " + WelcomeMessage + "\r\n");
                ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "220 " + WelcomeMessage, Proxy.LogLevel.Raw, LogLevel);

                // Instantiate an SmtpClient for sending messages to the remote server.
                using (OpaqueMail.Net.SmtpClient smtpClient = new OpaqueMail.Net.SmtpClient(arguments.RemoteServerHostName, arguments.RemoteServerPort))
                {
                    smtpClient.EnableSsl = arguments.RemoteServerEnableSsl;
                    smtpClient.Credentials = arguments.RemoteServerCredential;

                    if (arguments.SmimeValidCertificates != null)
                        smtpClient.SmimeValidCertificates = arguments.SmimeValidCertificates;

                    // Loop through each received command.
                    string command = "";
                    bool stillReceiving = true;
                    while (Started && stillReceiving)
                    {
                        int bytesRead = await clientStreamReader.ReadAsync(buffer, 0, Constants.SMALLBUFFERSIZE);

                        if (bytesRead > 0)
                        {
                            bytesTransmitted += (ulong)bytesRead;

                            command += new string(buffer, 0, bytesRead);

                            if (command.EndsWith("\r\n"))
                            {
                                // Handle continuations of current "DATA" commands.
                                if (sending)
                                {
                                    // Handle the finalization of a "DATA" command.
                                    if (command.EndsWith("\r\n.\r\n"))
                                    {
                                        sending = false;

                                        string messageFrom = "", messageSubject = "", messageSize = "";
                                        try
                                        {
                                            string messageText = command.Substring(0, command.Length - 5);

                                            // Export the message to a local directory.
                                            if (!string.IsNullOrEmpty(arguments.ExportDirectory))
                                            {
                                                string messageId = Functions.ReturnBetween(messageText.ToLower(), "message-id: <", ">");
                                                if (string.IsNullOrEmpty(messageId))
                                                    messageId = Guid.NewGuid().ToString();

                                                string userName = "";
                                                if (smtpClient.Credentials != null)
                                                    userName = ((NetworkCredential)smtpClient.Credentials).UserName;

                                                string fileName = ProxyFunctions.GetExportFileName(arguments.ExportDirectory, messageId, arguments.InstanceId, userName);
                                                File.WriteAllText(fileName, messageText);
                                            }

                                            ReadOnlyMailMessage message = new ReadOnlyMailMessage(messageText, ReadOnlyMailMessageProcessingFlags.IncludeRawHeaders | ReadOnlyMailMessageProcessingFlags.IncludeRawBody);

                                            if (!string.IsNullOrEmpty(arguments.FixedFrom))
                                            {
                                                message.From = Functions.FromMailAddressString(arguments.FixedFrom)[0];

                                                if (message.RawHeaders.Contains("From: "))
                                                    message.RawHeaders = Functions.ReplaceBetween(message.RawHeaders, "From: ", "\r\n", Functions.ToMailAddressString(message.From));
                                                else
                                                    message.RawHeaders = message.RawHeaders.Replace("\r\nSubject: ", "\r\nFrom: " + Functions.ToMailAddressString(message.From) + "\r\nSubject: ");
                                            }

                                            if (!string.IsNullOrEmpty(arguments.FixedTo))
                                            {
                                                MailAddressCollection addresses = Functions.FromMailAddressString(arguments.FixedTo);
                                                foreach (MailAddress address in addresses)
                                                {
                                                    bool addressFound = false;
                                                    foreach (MailAddress existingAddress in message.To)
                                                    {
                                                        if (existingAddress.Address.ToUpper() == address.Address.ToUpper())
                                                            addressFound = true;
                                                    }

                                                    if (!addressFound)
                                                        message.To.Add(address);
                                                }

                                                if (message.RawHeaders.Contains("To: "))
                                                    message.RawHeaders = Functions.ReplaceBetween(message.RawHeaders, "To: ", "\r\n", Functions.ToMailAddressString(message.To));
                                                else
                                                    message.RawHeaders = message.RawHeaders.Replace("\r\nSubject: ", "\r\nTo: " + Functions.ToMailAddressString(message.To) + "\r\nSubject: ");
                                            }

                                            if (!string.IsNullOrEmpty(arguments.FixedCC))
                                            {
                                                MailAddressCollection addresses = Functions.FromMailAddressString(arguments.FixedCC);
                                                foreach (MailAddress address in addresses)
                                                {
                                                    bool addressFound = false;
                                                    foreach (MailAddress existingAddress in message.CC)
                                                    {
                                                        if (existingAddress.Address.ToUpper() == address.Address.ToUpper())
                                                            addressFound = true;
                                                    }

                                                    if (!addressFound)
                                                        message.CC.Add(address);
                                                }

                                                if (message.RawHeaders.Contains("CC: "))
                                                    message.RawHeaders = Functions.ReplaceBetween(message.RawHeaders, "CC: ", "\r\n", Functions.ToMailAddressString(message.To));
                                                else
                                                    message.RawHeaders = message.RawHeaders.Replace("\r\nSubject: ", "\r\nTo: " + Functions.ToMailAddressString(message.To) + "\r\nSubject: ");
                                            }

                                            if (!string.IsNullOrEmpty(arguments.FixedBcc))
                                            {
                                                MailAddressCollection addresses = Functions.FromMailAddressString(arguments.FixedBcc);
                                                foreach (MailAddress address in addresses)
                                                {
                                                    bool addressFound = false;
                                                    foreach (MailAddress existingAddress in message.Bcc)
                                                    {
                                                        if (existingAddress.Address.ToUpper() == address.Address.ToUpper())
                                                            addressFound = true;
                                                    }

                                                    if (!addressFound)
                                                        message.Bcc.Add(address);
                                                }
                                            }

                                            // Insert the fixed signature if one exists.
                                            if (!string.IsNullOrEmpty(arguments.FixedSignature))
                                            {
                                                int endBodyPos = message.Body.IndexOf("</BODY>", StringComparison.OrdinalIgnoreCase);
                                                if (endBodyPos > -1)
                                                    message.Body = message.Body.Substring(0, endBodyPos) + arguments.FixedSignature + message.Body.Substring(endBodyPos);
                                                else
                                                    message.Body += arguments.FixedSignature;
                                            }

                                            // If the received message is already signed or encrypted and we don't want to remove previous S/MIME operations, forward it as-is.
                                            string contentType = message.ContentType;
                                            if ((contentType.StartsWith("application/pkcs7-mime") || contentType.StartsWith("application/x-pkcs7-mime") || contentType.StartsWith("application/x-pkcs7-signature")) && !arguments.SmimeRemovePreviousOperations)
                                            {
                                                message.SmimeSigned = message.SmimeEncryptedEnvelope = message.SmimeTripleWrapped = false;
                                                await smtpClient.SendAsync(message);
                                                ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "S: " + message, Proxy.LogLevel.Raw, LogLevel);
                                            }
                                            else
                                            {
                                                messageFrom = message.From.Address;
                                                messageSubject = message.Subject;
                                                messageSize = message.Size.ToString("N0");

                                                ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "Forwarding message from {" + message.From.Address + "} with subject {" + message.Subject + "} and size of {" + message.Size.ToString("N0") + "}.", Proxy.LogLevel.Verbose, LogLevel);

                                                foreach (string toListAddress in toList)
                                                {
                                                    if (!message.AllRecipients.Contains(toListAddress))
                                                    {
                                                        message.AllRecipients.Add(toListAddress);
                                                        message.Bcc.Add(toListAddress);
                                                    }
                                                }

                                                // Attempt to sign and encrypt the envelopes of all messages, but still send if unable to.
                                                message.SmimeSettingsMode = SmimeSettingsMode.BestEffort;

                                                // Apply S/MIME settings.
                                                message.SmimeSigned = arguments.SmimeSigned;
                                                message.SmimeEncryptedEnvelope = arguments.SmimeEncryptedEnvelope;
                                                message.SmimeTripleWrapped = arguments.SmimeTripleWrapped;

                                                // Look up the S/MIME signing certificate for the current sender.  If it doesn't exist, create one.
                                                message.SmimeSigningCertificate = CertHelper.GetCertificateBySubjectName(StoreLocation.LocalMachine, message.From.Address);
                                                if (message.SmimeSigningCertificate == null)
                                                    message.SmimeSigningCertificate = CertHelper.CreateSelfSignedCertificate("E=" + message.From.Address, message.From.Address, StoreLocation.LocalMachine, true, 4096, 10);

                                                ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "C: " + message.RawHeaders + "\r\n\r\n" + message.RawBody, Proxy.LogLevel.Raw, LogLevel);

                                                // Send the message.
                                                await smtpClient.SendAsync(message.AsMailMessage());

                                                // Check the signing certificate's expiration to determine if we should send a reminder.
                                                if (arguments.SendCertificateReminders && message.SmimeSigningCertificate != null)
                                                {
                                                    string expirationDateString = message.SmimeSigningCertificate.GetExpirationDateString();
                                                    TimeSpan expirationTime = DateTime.Parse(expirationDateString) - DateTime.Now;
                                                    if (expirationTime.TotalDays < 30)
                                                    {
                                                        bool sendReminder = true;
                                                        if (CertificateReminders.ContainsKey(message.SmimeSigningCertificate))
                                                        {
                                                            TimeSpan timeSinceLastReminder = DateTime.Now - CertificateReminders[message.SmimeSigningCertificate];
                                                            if (timeSinceLastReminder.TotalHours < 24)
                                                                sendReminder = false;
                                                        }

                                                        // Send the reminder message.
                                                        if (sendReminder)
                                                        {
                                                            OpaqueMail.Net.MailMessage reminderMessage = new OpaqueMail.Net.MailMessage(message.From, message.From);
                                                            reminderMessage.Subject = "OpaqueMail: S/MIME Certificate Expires " + expirationDateString;
                                                            reminderMessage.Body = "Your OpaqueMail S/MIME Certificate will expire in " + ((int)expirationTime.TotalDays) + " days on " + expirationDateString + ".\r\n\r\n" +
                                                                "Certificate Subject Name: " + message.SmimeSigningCertificate.Subject + "\r\n" +
                                                                "Certificate Serial Number: " + message.SmimeSigningCertificate.SerialNumber + "\r\n" +
                                                                "Certificate Issuer: " + message.SmimeSigningCertificate.Issuer + "\r\n\r\n" +
                                                                "Please renew or enroll a new certificate to continue protecting your e-mail privacy.\r\n\r\n" +
                                                                "This is an automated message sent from the OpaqueMail Proxy on " + Functions.GetLocalFQDN() + ".  " +
                                                                "For more information, visit http://opaquemail.org/.";

                                                            reminderMessage.SmimeEncryptedEnvelope = message.SmimeEncryptedEnvelope;
                                                            reminderMessage.SmimeEncryptionOptionFlags = message.SmimeEncryptionOptionFlags;
                                                            reminderMessage.SmimeSettingsMode = message.SmimeSettingsMode;
                                                            reminderMessage.SmimeSigned = message.SmimeSigned;
                                                            reminderMessage.SmimeSigningCertificate = message.SmimeSigningCertificate;
                                                            reminderMessage.SmimeSigningOptionFlags = message.SmimeSigningOptionFlags;
                                                            reminderMessage.SmimeTripleWrapped = message.SmimeTripleWrapped;

                                                            ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "Certificate with Serial Number {" + message.SmimeSigningCertificate.SerialNumber + "} expiring.  Sending reminder to {" + message.From.Address + "}.", Proxy.LogLevel.Information, LogLevel);

                                                            await smtpClient.SendAsync(reminderMessage);

                                                            CertificateReminders[message.SmimeSigningCertificate] = DateTime.Now;
                                                        }
                                                    }
                                                }
                                            }

                                            await Functions.SendStreamStringAsync(clientStreamWriter, "250 Forwarded\r\n");
                                            ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "S: 250 Forwarded", Proxy.LogLevel.Raw, LogLevel);

                                            ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "Message from {" + message.From.Address + "} with subject {" + message.Subject + "} and size of {" + message.Size.ToString("N0") + "} successfully forwarded.", Proxy.LogLevel.Verbose, LogLevel);
                                        }
                                        catch (Exception ex)
                                        {
                                            // Report if an exception was encountering sending the message.
                                            Functions.SendStreamString(clientStreamWriter, "500 Error occurred when forwarding\r\n");
                                            ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "S: 500 Error occurred when forwarding", Proxy.LogLevel.Raw, LogLevel);

                                            if (arguments.DebugMode || System.Diagnostics.Debugger.IsAttached)
                                                ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "Error when forwarding message from {" + messageFrom + "} with subject {" + messageSubject + "} and size of {" + messageSize + "}.  Exception: " + ex.ToString(), Proxy.LogLevel.Error, LogLevel);
                                            else
                                                ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "Error when forwarding message from {" + messageFrom + "} with subject {" + messageSubject + "} and size of {" + messageSize + "}.  Exception: " + ex.Message, Proxy.LogLevel.Error, LogLevel);
                                        }
                                        command = "";
                                    }
                                }
                                else
                                {
                                    ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "C: " + new string(buffer, 0, bytesRead), Proxy.LogLevel.Raw, LogLevel);

                                    // Handle continuations of current "AUTH PLAIN" commands.
                                    if (inPlainAuth)
                                    {
                                        inPlainAuth = false;
                                        // Split up an AUTH PLAIN handshake into its components.
                                        string authString = Encoding.UTF8.GetString(Convert.FromBase64String(command));
                                        string[] authStringParts = authString.Split(new char[] { '\0' }, 3);
                                        if (authStringParts.Length > 2 && arguments.RemoteServerCredential == null)
                                            smtpClient.Credentials = new NetworkCredential(authStringParts[1], authStringParts[2]);

                                        await Functions.SendStreamStringAsync(clientStreamWriter, "235 OK\r\n");
                                        ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "S: 235 OK", Proxy.LogLevel.Raw, LogLevel);

                                        command = "";
                                    }
                                    // Handle continuations of current "AUTH LOGIN" commands.
                                    else if (inLoginAuth)
                                    {
                                        if (smtpClient.Credentials == null)
                                        {
                                            // Handle the username being received for the first time.
                                            smtpClient.Credentials = new NetworkCredential();
                                            ((NetworkCredential)smtpClient.Credentials).UserName = Functions.FromBase64(command.Substring(0, command.Length - 2));

                                            await Functions.SendStreamStringAsync(clientStreamWriter, "334 UGFzc3dvcmQ6\r\n");
                                            ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "S: 334 UGFzc3dvcmQ6", Proxy.LogLevel.Raw, LogLevel);
                                        }
                                        else
                                        {
                                            // Handle the password.
                                            inLoginAuth = false;
                                            ((NetworkCredential)smtpClient.Credentials).Password = Functions.FromBase64(command.Substring(0, command.Length - 2));

                                            await Functions.SendStreamStringAsync(clientStreamWriter, "235 OK\r\n");
                                            ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "S: 235 OK", Proxy.LogLevel.Raw, LogLevel);
                                        }
                                        command = "";
                                    }
                                    else
                                    {
                                        // Otherwise, look at the verb of the incoming command.
                                        string[] commandParts = command.Substring(0, command.Length - 2).Replace("\r", "").Split(new char[] { ' ' }, 2);

                                        if (LogLevel == Proxy.LogLevel.Verbose)
                                            ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "Command {" + commandParts[0] + "} received.", Proxy.LogLevel.Verbose, LogLevel);

                                        switch (commandParts[0].ToUpper())
                                        {
                                            case "AUTH":
                                                // Support authentication.
                                                if (commandParts.Length > 1)
                                                {
                                                    commandParts = command.Substring(0, command.Length - 2).Replace("\r", "").Split(new char[] { ' ' });
                                                    switch (commandParts[1].ToUpper())
                                                    {
                                                        case "PLAIN":
                                                            // Prepare to handle a continuation command.
                                                            inPlainAuth = true;
                                                            await Functions.SendStreamStringAsync(clientStreamWriter, "334 Proceed\r\n");
                                                            ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "S: 334 Proceed", Proxy.LogLevel.Raw, LogLevel);

                                                            break;
                                                        case "LOGIN":
                                                            inLoginAuth = true;
                                                            if (commandParts.Length > 2)
                                                            {
                                                                // Parse the username and request a password.
                                                                smtpClient.Credentials = new NetworkCredential();
                                                                ((NetworkCredential)smtpClient.Credentials).UserName = Functions.FromBase64(commandParts[2]);

                                                                await Functions.SendStreamStringAsync(clientStreamWriter, "334 UGFzc3dvcmQ6\r\n");
                                                                ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "S: 334 UGFzc3dvcmQ6", Proxy.LogLevel.Raw, LogLevel);
                                                            }
                                                            else
                                                            {
                                                                // Request a username only.
                                                                await Functions.SendStreamStringAsync(clientStreamWriter, "334 VXNlcm5hbWU6\r\n");
                                                                ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "S: 334 VXNlcm5hbWU6", Proxy.LogLevel.Raw, LogLevel);
                                                            }
                                                            break;
                                                        default:
                                                            // Split up an AUTH PLAIN handshake into its components.
                                                            string authString = Encoding.UTF8.GetString(Convert.FromBase64String(commandParts[1].Substring(6)));
                                                            string[] authStringParts = authString.Split(new char[] { '\0' }, 3);
                                                            if (authStringParts.Length > 2 && arguments.RemoteServerCredential == null)
                                                                smtpClient.Credentials = new NetworkCredential(authStringParts[1], authStringParts[2]);

                                                            await Functions.SendStreamStringAsync(clientStreamWriter, "235 OK\r\n");
                                                            ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "S: 235 OK", Proxy.LogLevel.Raw, LogLevel);
                                                            break;
                                                    }
                                                }
                                                else
                                                {
                                                    await Functions.SendStreamStringAsync(clientStreamWriter, "500 Unknown verb\r\n");
                                                    ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "S: 500 Unknown verb", Proxy.LogLevel.Raw, LogLevel);
                                                }
                                                break;
                                            case "DATA":
                                                // Prepare to handle continuation data.
                                                sending = true;
                                                command = command.Substring(6);
                                                await Functions.SendStreamStringAsync(clientStreamWriter, "354 Send message content; end with <CRLF>.<CRLF>\r\n");
                                                ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "S: 354 Send message content; end with <CRLF>.<CRLF>", Proxy.LogLevel.Raw, LogLevel);
                                                break;
                                            case "EHLO":
                                                // Proceed with the login and send a list of supported commands.
                                                if (commandParts.Length > 1)
                                                    identity = commandParts[1] + " ";
                                                if (arguments.LocalEnableSsl)
                                                {
                                                    await Functions.SendStreamStringAsync(clientStreamWriter, "250-Hello " + identity + "[" + ip + "], please proceed\r\n250-AUTH LOGIN PLAIN\r\n250-RSET\r\n250 STARTTLS\r\n");
                                                    ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "S: 250-Hello " + identity + "[" + ip + "], please proceed\r\n250-AUTH LOGIN PLAIN\r\n250-RSET\r\n250 STARTTLS", Proxy.LogLevel.Raw, LogLevel);
                                                }
                                                else
                                                {
                                                    await Functions.SendStreamStringAsync(clientStreamWriter, "250-Hello " + identity + "[" + ip + "], please proceed\r\n250-AUTH LOGIN PLAIN\r\n250 RSET\r\n");
                                                    ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "S: 250-Hello " + identity + "[" + ip + "], please proceed\r\n250-AUTH LOGIN PLAIN\r\n250 RSET", Proxy.LogLevel.Raw, LogLevel);
                                                }
                                                break;
                                            case "HELO":
                                                // Proceed with the login.
                                                if (commandParts.Length > 1)
                                                    identity = commandParts[1];
                                                await Functions.SendStreamStringAsync(clientStreamWriter, "250 Hello " + identity + " [" + ip + "], please proceed\r\n");
                                                ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "S: Hello " + identity + " [" + ip + "], please proceed", Proxy.LogLevel.Raw, LogLevel);
                                                break;
                                            case "MAIL":
                                            case "SAML":
                                            case "SEND":
                                            case "SOML":
                                                // Accept the from address.
                                                if (commandParts.Length > 1 && commandParts[1].Length > 5)
                                                    fromAddress = commandParts[1].Substring(5);
                                                await Functions.SendStreamStringAsync(clientStreamWriter, "250 OK\r\n");
                                                ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "S: 250 OK", Proxy.LogLevel.Raw, LogLevel);
                                                break;
                                            case "NOOP":
                                                // Prolong the current session.
                                                await Functions.SendStreamStringAsync(clientStreamWriter, "250 Still here\r\n");
                                                ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "S: 250 Still here", Proxy.LogLevel.Raw, LogLevel);
                                                break;
                                            case "PASS":
                                                // Support authentication.
                                                if (commandParts.Length > 1 && arguments.RemoteServerCredential == null)
                                                    ((NetworkCredential)smtpClient.Credentials).Password = commandParts[1];
                                                await Functions.SendStreamStringAsync(clientStreamWriter, "235 OK\r\n");
                                                ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "S: 235 OK", Proxy.LogLevel.Raw, LogLevel);
                                                break;
                                            case "QUIT":
                                                // Wait one second then force the current connection closed.
                                                await Functions.SendStreamStringAsync(clientStreamWriter, "221 Bye\r\n");
                                                ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "S: 221 Bye", Proxy.LogLevel.Raw, LogLevel);

                                                Thread.Sleep(1000);

                                                if (clientStream != null)
                                                    clientStream.Dispose();
                                                if (client != null)
                                                    client.Close();
                                                break;
                                            case "RCPT":
                                                // Acknolwedge recipients.
                                                if (commandParts.Length > 1 && commandParts[1].Length > 6)
                                                    toList.Add(commandParts[1].Substring(5, commandParts[1].Length - 6));
                                                await Functions.SendStreamStringAsync(clientStreamWriter, "250 OK\r\n");
                                                ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "S: 250 OK", Proxy.LogLevel.Raw, LogLevel);
                                                break;
                                            case "RSET":
                                                // Reset the current message arguments.
                                                fromAddress = "";
                                                toList.Clear();

                                                await Functions.SendStreamStringAsync(clientStreamWriter, "250 OK\r\n");
                                                ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "S: 250 OK", Proxy.LogLevel.Raw, LogLevel);
                                                break;
                                            case "STARTTLS":
                                                // If supported, upgrade the session's security through a TLS handshake.
                                                if (arguments.LocalEnableSsl)
                                                {
                                                    await Functions.SendStreamStringAsync(clientStreamWriter, "220 Go ahead\r\n");
                                                    ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "S: 220 Go ahead", Proxy.LogLevel.Raw, LogLevel);

                                                    if (!(clientStream is SslStream))
                                                    {
                                                        clientStream = new SslStream(clientStream);
                                                        ((SslStream)clientStream).AuthenticateAsServer(arguments.Certificate);

                                                        clientStreamReader = new StreamReader(clientStream);
                                                        clientStreamWriter = new StreamWriter(clientStream);
                                                        clientStreamWriter.AutoFlush = true;
                                                    }
                                                }
                                                else
                                                {
                                                    await Functions.SendStreamStringAsync(clientStreamWriter, "500 Unknown verb\r\n");
                                                    ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "S: 500 Unknown verb", Proxy.LogLevel.Raw, LogLevel);
                                                }
                                                break;
                                            case "USER":
                                                // Support authentication.
                                                if (commandParts.Length > 1 && arguments.RemoteServerCredential == null)
                                                    ((NetworkCredential)smtpClient.Credentials).UserName = commandParts[1];

                                                await Functions.SendStreamStringAsync(clientStreamWriter, "235 OK\r\n");
                                                ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "S: 235 OK", Proxy.LogLevel.Raw, LogLevel);
                                                break;
                                            case "VRFY":
                                                // Notify that we can't verify addresses.
                                                await Functions.SendStreamStringAsync(clientStreamWriter, "252 I'm just a proxy\r\n");
                                                ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "S: 252 I'm just a proxy", Proxy.LogLevel.Raw, LogLevel);
                                                break;
                                            default:
                                                await Functions.SendStreamStringAsync(clientStreamWriter, "500 Unknown verb\r\n");
                                                ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "S: 500 Unknown verb", Proxy.LogLevel.Raw, LogLevel);
                                                break;
                                        }

                                        command = "";
                                    }
                                }
                            }
                        }
                        else
                            stillReceiving = false;
                    }
                }
            }
            catch (ObjectDisposedException)
            {
                // Ignore either stream being closed.
            }
            catch (SocketException ex)
            {
                if (arguments.DebugMode || System.Diagnostics.Debugger.IsAttached)
                    ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "Exception communicating with {" + arguments.RemoteServerHostName + "} on port {" + arguments.RemoteServerPort + "}: " + ex.ToString(), Proxy.LogLevel.Error, LogLevel);
                else
                    ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "Exception communicating with {" + arguments.RemoteServerHostName + "} on port {" + arguments.RemoteServerPort + "}: " + ex.Message, Proxy.LogLevel.Error, LogLevel);
            }
            catch (Exception ex)
            {
                if (arguments.DebugMode || System.Diagnostics.Debugger.IsAttached)
                    ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "Exception: " + ex.ToString(), Proxy.LogLevel.Error, LogLevel);
                else
                    ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "Exception: " + ex.Message, Proxy.LogLevel.Error, LogLevel);
            }
            finally
            {
                ProxyFunctions.Log(LogWriter, SessionId, arguments.ConnectionId, "Connection from {" + ip + "} closed after transmitting {" + bytesTransmitted.ToString("N0") + "} bytes.", Proxy.LogLevel.Information, LogLevel);

                // Clean up after any unexpectedly closed connections.
                if (clientStreamWriter != null)
                    clientStreamWriter.Dispose();
                if (clientStreamReader != null)
                    clientStreamReader.Dispose();
                if (clientStream != null)
                    clientStream.Dispose();
                if (client != null)
                    client.Close();
            }
        }