Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
West Wind WebSurge - Rest Client and Http Load Testing for Windows

Griping about System.Net.Email.SmptClient/MailMessage


:P
On this page:

For many years I've been using a home built SMTP client I built using low level Sockets. Single class, set a few properties and it goes off and sends emails. With about 5 lines of code I can be off sending an email from just about any app Web or otherwise. I created the custom class originally to get around the System.Web.Mail and the CDO mail requirements in .NET 1.x which involved COM interop and certain permissions on the server. When .NET 2.0 rolled around with the System.Net.Email namespace I figured that eventually I'd get around to replacing my code with the SmtpClient class.

As I'm working through some oldish code in my libraries and dealing with reducing permissions required to run several apps in medium trust, I'm now finally getting around to replacing my custom wwSmtp class by reimplementing it using the SmtpClient. Using the basic class is easy enough to just fire off a message and go.

Here's basically what I ended up with to match my existing class structure. It's nothing fancy, just a basic wrapper around SmtpClient to mimic my old wwSmtp class interface, but it demonstrates a more complete example usage of SmtpClient and MailMessage in the SendMessage method then what you may find in the MSDN docs:

using System;
using System.Text;
using System.Net.Sockets;
using System.Threading;
using System.IO;
using System.Net.Mail;
using System.Net;
using System.Security;
 
namespace Westwind.InternetTools
{
    /// <summary>
    /// SMTP Wrapper around System.Net.Email.SmtpClient. Provided 
    /// here mainly to provide compatibility with existing wwSmtp code
    /// and to provide a slightly more user friendly front end interface
    /// on a single object.
    /// </summary>
    public class wwSmtpNative
    {
 
        /// <summary>
        /// Mail Server to send message through. Should be a domain name 
        /// (mail.yourserver.net) or IP Address (211.123.123.123).
        /// 
        /// You can also provide a port number as part of the string which will 
        /// override the ServerPort (yourserver.net:211)
        /// <seealso>Class wwSmtp</seealso>
        /// </summary>
        public string MailServer = "";
 
        /// <summary>
        /// Port on the mail server to send through. Defaults to port 25.
        /// </summary>
        public int ServerPort = 25;
 
        /// <summary>
        /// Email address or addresses of the Recipient. Comma delimit multiple addresses. To have formatted names use
        /// "Rick Strahl" &lt;rstrahl@west-wind.com&gt;
        /// </summary>
        public string Recipient = "";
 
        /// <summary>
        /// Carbon Copy Recipients
        /// </summary>
        public string CC = "";
 
        /// <summary>
        /// Blind Copy Recipients
        /// </summary>
        public string BCC = "";
 
        /// <summary>
        /// Email address of the sender
        /// </summary>
        public string SenderEmail = "";
 
        /// <summary>
        /// Display name of the sender (optional)
        /// </summary>
        public string SenderName = "";
 
        /// <summary>
        /// The ReplyTo address
        /// </summary>
        public string ReplyTo = "";
 
        /// <summary>
        /// Message Subject.
        /// </summary>
        public string Subject = "";
 
        /// <summary>
        /// The body of the message.
        /// </summary>
        public string Message = "";
 
        /// <summary>
        /// Username to connect to the mail server.
        /// </summary>
        public string Username = "";
        /// <summary>
        /// Password to connect to the mail server.
        /// </summary>
        public string Password = "";
 
        /// <summary>
        /// The content type of the message. text/plain default or you can set to any other type like text/html
        /// </summary>
        public string ContentType = "text/plain";
 
        /// <summary>
        /// Character Encoding for the message.
        /// </summary>
        public string CharacterEncoding = "8bit";
 
        /// <summary>
        /// The character Encoding used to write the stream out to disk
        /// Defaults to the default Locale used on the server.
        /// </summary>
        public System.Text.Encoding Encoding = Encoding.Default;
 
 
        /// <summary>
        /// Determines the priority of the message
        /// </summary>
        public MailPriority Priority = MailPriority.Normal;
 
        /// <summary>
        /// An optional file name that appends logging information for the TCP/IP messaging
        /// to the specified file.
        /// </summary>
        public string LogFile = "";
 
        /// <summary>
        /// Determines whether wwSMTP passes back errors as exceptions or
        /// whether it sets error properties. Right now only error properties
        /// work reliably.
        /// </summary>
        public bool HandleExceptions = true;
 
        /// <summary>
        /// An Error Message if the result is negative or Error is set to true;
        /// </summary>
        public string ErrorMessage = "";
 
        /// <summary>
        /// Error Flag set when an error occurs.
        /// </summary>
        public bool Error = false;
 
        /// <summary>
        /// Connection timeouts for the mail server in seconds. If this timeout is exceeded waiting for a connection
        /// or for receiving or sending data the request is aborted and fails.
        /// </summary>
        public int Timeout = 30;
 
        /// <summary>
        /// Event fired when sending of a message or multiple messages
        /// is complete and the connection is to be closed. This event
        /// occurs only once per connection as opposed to the MessageSendComplete
        /// event which fires after each message is sent regardless of the
        /// number of SendMessage operations.
        /// </summary>
        public event delSmtpNativeEvent SendComplete;
 
        /// <summary>
        /// Event that's fired after each message is sent. This
        /// event differs from SendComplete that it fires
        /// after each send operation of each message rather
        /// than before closing the connection.
        /// </summary>
        public event delSmtpNativeEvent MessageSendComplete;
 
        /// <summary>
        /// Event fired when an error occurs during processing and before
        /// the connection is closed down.
        /// </summary>
        public event delSmtpNativeEvent SendError;
 
 
        /// <summary>
        /// Connects to the mail server.
        /// </summary>
        /// <returns>True or False</returns>
        public bool Connect() 
        {
            return true;
        }
 
        /// <summary>
        /// Fully self contained mail sending method. Sends an email message by connecting 
        /// and disconnecting from the email server.
        /// </summary>
        /// <returns>true or false</returns>
        public bool SendMail() 
        {
           //if (!string.IsNullOrEmpty(this.LogFile))
           //    this.LogString("\r\n*** Starting SMTP connection - " + DateTime.Now.ToString());
 
            // *** Allow for server:Port syntax (west-wind.com:1212)
            int serverPort = this.ServerPort;
            string server = this.MailServer;
 
            // *** if there's a port we need to split the address
            string[] parts = server.Split(':');
            if (parts.Length > 1) 
            {
                server = parts[0];
                serverPort = int.Parse(parts[1]);
            }
 
            if (server == null || server == "") 
            {
                this.SetError("No Mail Server specified.");
                return false;
            }
 
            SmtpClient smtp = null;
            try
            {
                smtp = new SmtpClient(server, serverPort);
            }
            catch (SecurityException ex)
            {
                this.SetError("Unable to create SmptClient due to missing permissions. If you are using a port other than 25 for your email server, SmtpPermission has to be explicitly added in Medium Trust.");
                return false;
            }
 
            // *** This is a Total Send Timeout not a Connection timeout!
            smtp.Timeout = this.Timeout * 1000;
 
            if (!string.IsNullOrEmpty(this.Username))
                smtp.Credentials = new NetworkCredential(this.Username, this.Password);
 
            // *** Create and configure the message 
            MailMessage msg = this.GetMessage(); 
 
            try
            {
                smtp.Send(msg);
            }
            catch (Exception ex)
            {
                this.SetError(ex.Message);
                if (this.SendError != null)
                    this.SendError(this);
 
                return false;
            }
 
            if (this.SendComplete != null)
                this.SendComplete(this);
 
            return true;
        }
 
        /// <summary>
        /// Configures the message interface
        /// </summary>
        /// <param name="msg"></param>
        protected virtual MailMessage GetMessage()
        {
            MailMessage msg = new MailMessage();
 
            msg.Body = this.Message;
            msg.Subject = this.Subject;
            msg.From = new MailAddress(this.SenderEmail, this.SenderName);
 
            if (!string.IsNullOrEmpty(this.ReplyTo))
                msg.ReplyTo = new MailAddress(this.ReplyTo);
 
            // *** Send all the different recipients
            this.SendRecipients(msg.To, this.Recipient);
            this.SendRecipients(msg.CC, this.CC);
            this.SendRecipients(msg.Bcc, this.BCC);
 
            //using (MemoryStream ms = new MemoryStream())
            //{
            //    ms.Write(this.Encoding.GetBytes(this.Message));
            //    ms.Position = 0L;
 
            //    AlternateView vw = new AlternateView(ms);
            //    vw.ContentType = "text/html";
            //    vw.TransferEncoding = System.Net.Mime.TransferEncoding.Base64;
 
            //    msg.AlternateViews.Add(vw);
            //}                       
 
            //msg.Headers.Add("x-mailer", "wwSmtp .Net");
 
            if (this.ContentType.StartsWith("text/html"))
                msg.IsBodyHtml = true;
            else
                msg.IsBodyHtml = false;
            //msg.Headers.Add("Content-Type",this.ContentType);
            //msg.Headers.Add("Content-Transfer-Encoding", this.CharacterEncoding);
 
            msg.Priority = this.Priority;
 
            msg.BodyEncoding = this.Encoding;
 
            return msg;
        }
 
 
 
        /// <summary>
        /// Sends all recipients from a comma or semicolon separated list.
        /// </summary> 
        /// <param name="recipients"></param>
        /// <returns></returns>
        void SendRecipients(MailAddressCollection address, string recipients)
        {
            if (recipients == null || recipients.Length == 0)
                return;
 
            string[] recips = recipients.Split(',', ';');
 
            for (int x = 0; x < recips.Length; x++)
            {
                address.Add(new MailAddress(recips[x]));
            }
        }
 
        /// <summary>
        /// Strips out just the email address from a full email address that might contain a display name
        /// in the format of: "West Wind Web Monitor" <rstrahl@west-wind.com>
        /// </summary>
        /// <param name="lcFullEmail">Full email address to parse. Note currently only "<" and ">" tags are recognized as message delimiters</param>
        /// <returns>only the email address</returns>
        string GetEmailFromFullAddress(string fullEmail)
        {
            if (fullEmail.IndexOf("<") > 0)
            {
                int lnIndex = fullEmail.IndexOf("<");
                int lnIndex2 = fullEmail.IndexOf(">");
                string lcEmail = fullEmail.Substring(lnIndex + 1, lnIndex2 - lnIndex - 1);
                return lcEmail;
            }
 
            return fullEmail;
        }
 
        /// <summary>
        /// Provided for compatibility with wwSmtp to provide Async send operation.        
        /// </summary>
        /// <returns></returns>
        public void SendMailAsync() 
        {
            ThreadStart delSendMail = new ThreadStart(this.SendMailRun);
            delSendMail.BeginInvoke(null, null);
 
//            Thread mailThread = new Thread(delSendMail);
//            mailThread.Start();
        }
 
        protected void SendMailRun() 
        {
            // Create an extra reference to insure GC doesn't collect
            // the reference from the caller
            wwSmtpNative Email = this
            Email.SendMail();
        }
 
 
 
        /// <summary>
        /// Logs a message to the specified LogFile
        /// </summary>
        /// <param name="FormatString"></param>
        /// <param name="?"></param>
        protected void LogString(string message)
        {
            if (string.IsNullOrEmpty(this.LogFile))
                return;
 
            if (!message.EndsWith("\r\n"))
                message += "\r\n";
 
            using (StreamWriter sw = new StreamWriter(this.LogFile, true))
            {
                sw.Write(message);
            }
        }
 
 
        /// <summary>
        /// Internally used to set errors
        /// </summary>
        /// <param name="errorMessage"></param>
        private void SetError(string errorMessage)
        {
            if (errorMessage == null || errorMessage.Length == 0) 
            {
                this.ErrorMessage = "";
                this.Error = false;
                return;
            }
 
            ErrorMessage = errorMessage;
            Error = true;
        }
 
    }
 
    public delegate void delSmtpNativeEvent(wwSmtpNative Smtp);
 
}

In the process of creating the new class I lost a few features namely the ability to log the full conversation (although there may be a way to trace that - haven't found it in a quick check though) and in tighter control regarding the message format and encoding.

The real problem is if you want to build something that's maybe a little more generic. The issue lies with the way the content type, encoding and transfer encoding is handled. These things are crucially important but System.Net.MailMessage doesn't expose shit to make this process even remotely accessible. All you can do with MailMessage is basically apply a BodyEncoding and hope for the best. Based on the encoding MailMessage will then apply what it thinks are the appropriate mail headers.

The problem is that Encoding alone doesn't really describe what a mail message needs for encoding. You do need to know the ContentType whether it's plain text or text/html or text/xml or whatever. MailMessage doesn't have a ContentType. The closest thing it has is a .IsBodyHtml property which will toggle the the content type to text/html and text/plain. Other than that - no choices unless you use the .AlternateViews property which allows you to provide a specific content type. But that's not really  good solution for scenarios when you are interested in other types of content.

Then there's the content encoding. If you choose plain text, MailMessage uses Content-Transfer-Encoding: Quoted-Printable; which is among the most fragile mail formats possible. If you've ever received emails that look like this:

=0D=0A=0D=0AHi Ricky, =0D=0A=0D=0AYour order # b3a8f3cd for $299.00 has=

been declined!=0D=0A=0D=0AYour credit card was declined for this transaction.=

=0D=0APlease check your card and especially the card billing info =0D=0Aprovided=

and resubmit the order or use another card for =0D=0Athe transaction.=0D=0A=

=0D=0AReason for decline:=0D=0A=0D=0AInvalid Credit Card Number=0D=0A=0D=0AYou=

can re-submit the order at:=0D=0A=0D=0Ahttp://Localhost/ReloadOrder.aspx?InvNo=3Db3a8f3cd=

=0D=0A=0D=0AAll of your contact information is still online so you won't=

=0D=0Ahave to reenter this information. Simply reselect your items=0D=0Ato=

purchase.=0D=0A=0D=0ARegards,=0D=0A=0D=0AThe West Wind Technologies Team

you get the idea <s>. Or you may have seen partials of these encoding markers - excess = signs or the line break characters on occasion which is caused by a client or server that didn't quite do the encoding/decoding right. It's a shitty format to decode too because it breaks at a given line length and there's a possibility of misinterpreting characters as breaks when they are not.

IAC, MailMessage/SmtpClient uses Quoted-Printable for plain text formatting.

The message above was generated from my actual calling code which manually tried to add a separate Content-Transfer-Encoding. Ironically Hotmail parsed this message correctly, while Outlook produced the result above. I suppose both are looking at the duplicated headers in a different order and Hotmail got it right? <shrug>

If you specify Encoding.UTF8 or any other Unicode flavor for the BodyEncoding instead you get base64 Content-Transfer-Encoding. Base64 is a binary representation of the text which is more reliable, but unfortunately some older mail clients don't parse it. For a generic client this can be a problem.

Now, SmptClient includes a .Headers property that you can add headers to, but unfortunately you can't override headers that the message adds. So while you can specify:

msg.Headers.Add("Content-Type",this.ContentType);

That content will be duplicated by what the BodyEncoding setting generates. Ultimately this means you really get ZERO control over how the content is formatted. The only option I see if you're doing anything with content type in the message is simply to deal with HTML and Plain text and that's it.

if (this.ContentType.StartsWith("text/html"))
    msg.IsBodyHtml = true;
else
    msg.IsBodyHtml = false;
    //msg.Headers.Add("Content-Type",this.ContentType);
 

Given these limitations it would seem best to use the following settings:

  • UTF-8 Encoding
  • Base64 Transfer Encoding
  • Specifying IsBodyHtml

and forget about any other scenarios. This may hose a few ancient mail clients but at least current clients will reliably receive data without having to screw around with the potentially ambiguous quoted printable implementation. If you need any other type of content (say you want to send text/xml content) it's also best to send UTF-8 and simply set an invalid content type.

There are a few other oddities in the SmtpCient API. For example, the Timeout property specifies how long the Email Sending process should take. Rather than allowing for connection timeouts or individual socket send timeouts you have to specify the overall time. WTF? If a message is slow to send it's probably OK, but now the value of the Timeout property has to be long enough to allow for long connections to the server and if the there's a server connection problem you can't just wait for just 5 seconds and abort and instead have to wait for 30 seconds or whatever the full message send timeout is.

Outlook 2007 and SMTP Email Headers

BTW, if you ever needed to find headers in Outlook 2007 and have been frustrated by the Ribbon's removal of the Message Options dialog, you can now find that dialog off the context menu:

MessageOptions

MessageHeaders[6]

I know I was digging for that damn dialog trying to find it off the ribbon and menu customization for a while before I accidentally stumbled on the context menu option BEFORE you have the message in the editor. Grrrr....

Posted in .NET  ASP.NET  

The Voices of Reason


 

Duncan Smart
January 14, 2008

# re: Griping about System.Net.Email.SmptClient/MailMessage

So it begs the question Rick - why did you bother updating your existing code?

Rick Strahl
January 14, 2008

# re: Griping about System.Net.Email.SmptClient/MailMessage

Permissions. Using my own wwSMTP class worked fine for all scenarios I used, but it requires SocketPermissions to run which is a problem in medium trust and opens up all outbound port access.

Using the other class you can get away with SmtpPermission which is easier to get ISPs to grant because it's more common and more focused.

Richard Deeming
January 15, 2008

# re: Griping about System.Net.Email.SmptClient/MailMessage

You can get a log of the SMTP sending process by configuring the appropriate trace sources and switches. The documentation [1] isn't great, but the following configuration should work for a console application:

<system.diagnostics>
<trace autoflush="true" indentsize="4"/>

<sources>
<source name="System.Net" >
<listeners>
<add name="textFileListener"/>
</listeners>
</source>

<source name="System.Net.Sockets">
<listeners>
<add name="textFileListener"/>
</listeners>
</source>
</sources>

<sharedListeners>
<add
name="textFileListener"
type="System.Diagnostics.TextWriterTraceListener"
initializeData="System.Net.trace.log"
/>
</sharedListeners>

<switches>
<add name="System.Net" value="Verbose" />
<add name="System.Net.Sockets" value="Verbose" />
</switches>
</system.diagnostics>

This will dump verbose trace information to the "System.Net.trace.log" file in the same folder as the application. For ASP.NET, the System.Web assembly also defines the WebPageTraceListener and IisTraceListener classes.

[1] http://msdn2.microsoft.com/en-us/library/1txedc80.aspx

Rick Strahl
January 15, 2008

# re: Griping about System.Net.Email.SmptClient/MailMessage

@Richard - thanks for posting this here!

Andrus
February 05, 2008

# re: Griping about System.Net.Email.SmptClient/MailMessage

Line

MailMessage msg = this.GetMessage(msg);

causes compile error

Cannot implicitly convert type 'void' to 'System.Net.Mail.MailMessage'

How to fix ?

Jesper
February 07, 2008

# re: Griping about System.Net.Email.SmptClient/MailMessage

You can actually get the message options (Outlook 2007) in the bloody ribbon if you click the tiny icon where it says "options" (on the fourth ribbon group)...
BTW thanks for the article!

Dan
April 02, 2008

# re: Griping about System.Net.Email.SmptClient/MailMessage

I'm not sure why you think AlternateViews aren't a good solution for the content type problem. For instance, if you want to send an iCal invitation, you can create a MailMessage with an empty body and a single AlternateView with a content type of "text/calendar". I don't see how this breaks the RFC and Outlook and a few of the webmail clients I have access to handle it fine. I agree that everything would be better if MailMessage allowed you to set the content-type header.

vivian
June 15, 2008

# re: Griping about System.Net.Email.SmptClient/MailMessage

Dan, Do you ahve an example of how to send an iCal invitation? I am trying to do this from C# but will take a VB example. I want the email itself to be the invitation. Not looking to attach a vcs or ics file. Thanks!

Michael
June 16, 2008

# re: Griping about System.Net.Email.SmptClient/MailMessage

Hate to jump into your conversation but I'm having a difficulty with a send mail app in VB2008. The program works fine the problem is that not all email servers receive the email. I can send to my Shaw.ca account and to gmail fine but Yahoo and hotmail won't receive not to mention my company account (I don't get a notification either). You make the comment that 'With about 5 lines of code you are off sending emails', you make it sound like everything is received no problem. So am I missing something? Trying to follow up on 'encoding' lead me here. If anyone has any ideas why these servers won't receive my emails please let me know and thank you.

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024