/// <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())); }
/// <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); }
/// <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()); }
/// <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(); }
/// <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; } } } }
/// <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(); } }