Click here to Skip to main content
Click here to Skip to main content

MailMergeLib - A .NET Mail Client Library

, 31 Mar 2013
Rate this:
Please Sign up or sign in to vote.
MailMergeLib is a SMTP mail client library. It makes use of .NET System.Net.Mail and provides comfortable mail merge capabilities. MailMergeLib corrects a number of the most annoying bugs and RFC violations that .NET 2.0 to .NET 4.0 suffer from.
Screenshot of a merged email

Introduction

There are numerous occasions where an application has to send e-mails: Feedback forms in the web, webmailers, loggers and so forth. And there are many commercial solutions available: EASendMail SMTP Component, MailBee.NET SMTP Component, Aspose.Network.Mail or aspNetEmail, just to mention some of them. If you're looking for a free solution with source code, supplying comparable features, you'll find it worthwhile to have a closer look at MailMergeLib:

Feature Extending System.Net.Mail
Sender addresses (From, Sender) √ Name and address may contain {placeholders}
Recipients addresses (To, Cc, Bcc) √ Name and address may contain {placeholders}
Return addresses (ConfirmReading, Return Receipt, ReplyTo) √ Name and address may contain {placeholders}
Test address (for tests before sending bulk mails) √ Name may contain {placeholders}
HTML Body with automatically including Linked Images √ Can be a template containing {placeholders}. It is optional.
Text Body √ Can be a template containing {placeholders}. The HTML Body can be converted (i.e. without tags or entities) to the Text Body. It is optional.
Synchronous programming model √ Events firing when mail or mailmerge is starting, in progress, failure or ending
Asynchronous programming mode √ Events firing when mail or mailmerge is starting, in progress, failure or ending
Message output √ SMTP server, Directory, IIS Pickup Directory, EML file
Calculate size of a mail message
Mail Merge from DataTable or DataSet / DataMember
Mail Merge from any IList or IEnumerable Datasource √ Even enumerations like Arrays of anonymous types will work
Delay time between mail messages when merging
MaxFailures, RetryDelayTime and MaxFailures for error handling
Ignore empty mail messages during mail merge
X-Mailer Header
Mail priority Header and other custom headers
Set local host name for SMTP hello command √ A question of privacy: By default the computer name is used.
{placeholders} √ Can be formatted with standard .NET format strings (culture specific) or a custom IFormatProvider, and they can have special display values for Null or Empty values

Internally MailMergeLib is using (and extending) the .NET System.Net.Mail library. Generally speaking, this works quite nice. Unfortunately Microsoft has not yet overcome its quite annoying bugs and RFC violations that their library was suffering from the very beginning. But fortunately MailMergeLib provides ways to fix the bugs in System.Net.Mail. - please see section "Bug fixes for System.Net.Mail"

Using the Code

For sending a mail in System.Net.Mail, you'll first create a MailMessage, and then send it with SmtpClient. In MailMergeLib this is quite similar: you'll create a MailMergeMessage and then send it with MailMergeSender.

MailMergeMessage

Create a New Message

So first let's create the message and adjust some settings:

MailMergeMessage mmm = new MailMergeMessage();
mmm.CharacterEncoding = Encoding.ASCII;
mmm.CultureInfo = new System.Globalization.CultureInfo("en-US");
mmm.TextTransferEncoding = System.Net.Mime.TransferEncoding.SevenBit;
mmm.BinaryTransferEncoding = System.Net.Mime.TransferEncoding.Base64;

Here's how to set the properties for a message which will contain an attachment:

  • CharacterEncoding: The encoding used for any plain or HTML text (e.g.: ASCII, UTF8)
  • CultureInfo: Relevant for formatting placeholders that contain dates, currency or numeric data.
  • TextTransferEncoding: The transfer encoding for any plain or HTML text (7bit or quoted-printable)
  • BinaryTransferEncoding: The transfer encoding to use for attachments (Base64 for binary data).

Add Content including Placeholders

mmm.Subject = "CRON Job Status Report for Domain {DomainName}";
mmm.PlainText = "CRON Job successfully completed: 
		{JobEndDateTime:"{0:yyyy-MM-dd hh:mm}"}";

One big advantage of MailMergeLib comes from placeholders. {Placeholders} can be used in e-mail addresses, in the subject, in the plain text or HTML body, in text attachment content and in attachment names. Placeholders are the names of e.g. IList item properties, or the column names of a data table.

Add a DataSource

Working with placeholders requires to set the Datasource property of the MailMergeMessage. This is extremely flexible and can be any data source that you could use with .NET BindingSource: One of the most simple ones are data tables, but you can also use entity collections of O/R mappers, or even an array of anonymous type:

mmm.DataSource = new[]
     {
        new {AdminEmail = harry@example.com, AdminName = "Harry", 
		DomainName="example1.com", JobEndDateTime = DateTime.Now},
        new {AdminEmail = "jeff@example.com", AdminName = "Jeff", 
		DomainName="example2.com", JobEndDateTime = DateTime.Now.AddDays(1)}
     };

In case of O/R mappers, an entity field could look like this: Department.Leader.FirstName. So you could use the field name as a placeholder: "{Department.Leader.FirstName}". Your DataSource would then be e.g. an entity collection Collection<Department>.

Formatting Capabilities

The placeholders and the formatting capabilities allow separation of code and design. In order to change the content of an e-mail, just change the template file for the plain text and you're done without a need to touch your code.

It is possible to add standard .NET formatting attributes to your placeholders. For a date, this could look like this: {JobEndDateTime:"{0:yyyy-MM-dd hh:mm}"}. But what if a value is null? Just add another format string for Null values: {DomainName:"{0}{null:Domain unknown.}"}. In case of strings, there is another format option with "empty" in addition to "null".

If a column of a DataTable has ExtendedProperties for null, empty and/or format these properties can be used. Examples:

myDataTable.Columns["DomainName"].ExtendedProperties.Add("format", "{0}");
myDataTable.Columns["DomainName"].ExtendedProperties.Add("empty", "Domain unknown.");

Of course, data types and formatting attributes must fit each other.

You can insert a text file by using the special syntax {IncludeFile:"file"}."IncludeFile" is the variable name (e.g. the column name of a DataTable) while file means to interpret the variable value as a file which content shall be included. Such text files may also include placeholders.

If all these formatting capabilities are still not enough, set your own IFormatProvider:

mmm.GetTextVariableManager().FormatProvider = myFormatProvider;

Message Body

The message body parts can be added or changed quite easily either by using them as a parameter in the constructor, or by setting the properties. You can either supply HTML or plain text or both.

mmm.HtmlText = new System.IO.StreamReader("HtmlBody.html").ReadToEnd();
// set the plain text either by converting Html or by setting 
// it independently (or leave it empty).
mmm.PlainText = _mmm.ConvertHtmlToPlainText(null);  	// use the default converter 
						// or provide your own

MailMergeMessage supplies the ParsingHtmlConverter (default) for converting HTML to plain text. It uses the Microsoft open source Html Agility Pack (HAP) (and thus the assembly HtmlAgilityPack.dll is required) and has a similar approach like Markdown by John Gruber. It converts the most common HTML tags and attributes which appear in e-mails, not on web pages, to plain text with quite good results. If this is not available or fails, the very basic RegExHtmlConverter is used (similar to this solution), unless you provide your custom converter implementing IHtmlConverter.

More magic comes with image tags included in the HTML content: Just set the directory where your Linked Images are located...

mmm.FileBaseDir = "Drive:\\PathToImageDirectory";

... and edit your HTML image tags as usual. (Instead of setting the FileBaseDir, you can also use the HTML tag <base href="file://Drive:/PathToImageDirectory">, or simply provide absolute paths to your image files. Your image files will be automatically attached as "linked images".

<img src="imagefile.png" width="48" height="48" alt="icon: job successfully completed" />

Attachments

You may also want to add some personalized attachments by adding placeholders to the file name. E.g.:

mmm.FileAttachments.Add
    (new FileAttachment("testmail_{AdminName}.pdf", "sample.pdf", "application/pdf"));

And that's the way to add string attachments:

mmm.StringAttachments.Add(new StringAttachment
    ("Some programmatically created content", "logfile.txt", "text/plain"));

Mail Addresses

For sending a mail, we need supply at least one recipient's address and the sender's address. Again, using placeholders makes it possible to create personalized e-mails.

mmm.MailMergeAddresses.Add(new MailMergeAddress
	(MailAddressType.To, "{AdminEmail}", "{AdminName}", Encoding.ASCII));
mmm.MailMergeAddresses.Add(new MailMergeAddress
	(MailAddressType.From, "sender@example.com", Encoding.ASCII));

Miscellaneous

If you're using a data source with variables, it may well happen that you have recipients with empty e-mail fields. That's why you may want to not throw an exception with empty addresses.

mmm.IgnoreEmptyRecipientAddr = true;

Want to change MailMergeLib's identification? Set the mail's Xmailer header to anything you like.

mmm.Xmailer = "MailMergeLib 3.0";

MailMergeSender

First of all, MailMergeSender is much like System.Net.Mail.SmtpClient: Create an instance of the class and provide some SMTP related settings.

SMTP Settings

Setup the mail sender:

MailMergeSender mailSender = new MailMergeSender();
mailSender.MessageOutput = MessageOutput.SmtpServer;

Set up SMTP server login details:

mailSender.SmtpHost = "smtp.server.com";
mailSender.SmtpPort = 25;
mailSender.SetSmtpAuthentification("username", "password");
mailSender.LocalHostName = "my.localhostname.com"; // used in SMTP Hello command

EventHandlers

With SmtpClient, you cannot add custom event handlers. MailMergeSender will raise events OnBeforeSend, OnAfterSend, OnSendFailure, OnMergeBegin, OnMergeComplete, and OnMergeProgress:

mailSender.OnAfterSend += (delegate(object obj, MailSenderAfterSendEventArgs args)
         {    // do something useful here, like updating a progress bar   });

Sending a Message

For the Send job, there are several alternatives:

  1. Send a single message as an asynchronous operation:
    mmm.DataSource = myDataSource;  // setting a DataSource is optional
    mmm.CurrentDataPosition = 2;    // only if a DataSource is set
    mailSender.SendAsync(mmm);
  2. Send all messages for all items in the DataSource as an asynchronous operation:
    mmm.DataSource = myDataSource;
    mailSender.SendAllAsync(mmm);
  3. Send messages as a synchronous operation for all items in the DataSource:
    mmm.DataSource = myDataSource;
    mailSender.SendAll(mmm);
  4. Or just send a single message providing a Dictionary with name/value pairs synchronously:
    Dictionary<string, string> nameValuePairs = new Dictionary<string, string>();
    nameValuePairs.Add("Email", "to@addr.com");
    nameValuePairs.Add("Nickname", "Bill");
    mmm.SetVariables(nameValuePairs);
    mailSender.Send(mmm);

Cancelling a Send Operation

Asynchronous Send operations can be cancelled at any time:

mailSender.SendCancel();

Influencing Error Handling

Timeout in milliseconds:

mailSender.Timeout = 100000;

Maximum number of failures until sending a message will finally fail:

mailSender.MaxFailures = 3;

Retry delay time between failures:

mailSender.RetryDelayTime = 3000;

Delay time between each message:

mailSender.DelayBetweenMessages = 1000;

Bug Fixes for System.Net.Mail

Sure, with System.Net.Mail, you can send mail messages. The point is: some bugs and RFC violations will increase the spam rating of spam filters for your message, and mail clients may even show parts as garbage. MailMergeLib addresses the following issues:

Bugs fixed in .NET 4.0:

  1. Quoted-Printable encoding is not limited to a maximum of 76 characters in System.Net.Mail. RFC 2045 requires that Quoted-Printable encoding encodes lines be no more than 76 characters long. If longer lines are to be encoded with the Quoted-Printable encoding, "soft" line breaks must be used.
  2. White space in display names of a MailAddress must be encoded (RFC2047 and RFC 822 violation).
  3. With MailMessage.Headers, there is a bug where headers will have white space in an encoded text. This will lead to non-RFC 2047 compliant messages.
  4. MailMessage.Subject is not encoded correctly. Same problem as with headers.
  5. When talking to an SMTP server, SmtpClient should double all dots at the beginning of a line for all text-based content. This works fine if MailMessage.Body is used. But it does not work if AlternateViews or text attachments are used. The result is, that these dots get lost or - even worse - all content after the dot gets lost if it is followed by CRLF (RFC 2821 violation).

Bugs fixed in .NET 3.5 SP1:

  1. MailMessage.MailAddressCollection.Clear does not clear the corresponding headers. If you want to re-use an existing MailMessage object for different recipients, you will have to remove the recipients headers yourself (e.g. MailMessage.Headers.Remove("to"))
  2. Setting the transfer encoding to TransferEncoding.SevenBit turns into a header text sevenbit, instead of 7bit. sevenbit is not RFC compliant and causes problems with some mail clients.
  3. MailMessage.To.ToString() returns the encoded string only after the message was sent. According to the documentation, this should be the case no matter whether the message was already sent or not.
  4. Display names of mail addresses that are not all 7bit characters must be encoded. This works fine with every address type except for to addresses. to addresses will never be encoded, no matter which Encoding parameter is used for a new MailAddress.

Bugs not fixed up to .NET 4.0:

  1. Attachment.ContentDisposition.FileName must be encoded, if it contains other characters than 7bit or spaces. System.Net.Mail does not encode filenames, but throws a FormatException 'MailHeaderFieldInvalidCharacter', so that such filenames cannot be used at all.
  2. If the display name of an attachment contains non-ASCII characters, System.Net.Mail throws an exception instead on encoding them. Anyway: The display name of an attachment must be encoded if it contains non-ASCII characters (e.g. Chinese, German Umlaut, etc.)
  3. Each line of characters MUST be no more than 998 characters, and SHOULD be no more than 78 characters, excluding the CRLF (RFC2822 violation).
  4. Long file names are improperly wrapped in the Content-Type and Content-Disposition header sections of the attachment. When wrapping takes place depends on the number of non-sevenbit characters and the character-encoding in use (defaults to UTF8).

So although Microsoft has supplied some bug fixes, quite a few can be considered as persistent bugs without too much hope for getting them fixed. All bugfixes are included in a static class Bugfixer which hopefully won't be needed in one of the glorious future versions of .NET. Pie in the Microsoft sky?

The bug fixes supplied make heavy use of System.Reflection in order to hack System.Net.Mail. The drawback of this is, that any changes introduced by Microsoft to non-public class members will break bug fixing. In the best case an Exception is thrown, in the worst case you do not notice it. You are warned: Every update of the .NET Framework requires testing of Bugfixer.

Note: If needed, you can easily switch off all bug fixing by setting the IsSwitchedOn property of Bugfixer to false.

Conclusion

By fixing the bugs in System.Net.Mail, it was interesting to dig into its design and to supply modifications using System.Reflection. Although all the bugs mentioned were reported to Microsoft a very long time ago, they didn't remove them. Life could be so easy... Anyway, MailMergeLib works well (again, after .NET 3.5 SP1 and .NET 4.0 partly broke it) and gives a lot more than just bug fixes. It should be noted, however, that it is not designed to send bulk mail with thousands of messages at a time.

Special Thanks To

  • Mike Bridge for his QPEncoder in DotNetOpenMail
  • Developers of Html Agility Pack (HAP)
  • John Gruber for sharing his Markdown xslt-based text-to-HTML conversion tool
  • Mono developers for their AttachmentBase.MimeTypes
  • .NET Reflector for this great tool to analyze System.Net.Mail and to find out ways to fix its bugs
  • All users giving their feedback and votes in the forum

History

  • 2007-07-10: Initial public release (MailMergeLib 2.0)
  • 2007-07-11: Minor doc and code update
  • 2007-10-08: SSL support added (thanks to WPKF), GetEncodedMailAddress fixed
  • 2007-12-28: Re-designed bug fixes in BugFixer class that .NET 2.0 SP1 / .NET 3.5 broke:
    • CorrectSubjectEncodedWordRFC2047compliant
    • AddToAddressWithCorrectEncoding
    • ClearMessageHeaders
  • 2008-01-01: Improvement of the Tools class
    • CalcMailSize(MailMessage msg) now gives accurate size instead of an estimation
    • Added GetMailAsMimeString (MailMessage msg) to retrieve the message content (same as in an EML file)
  • 2008-01-12: Minor improvements
    • Tools.WrapLine(string input, int length)will now allow empty lines (thanks to woetertie)
    • QPEncoder from DotNetOpenMail might return a string 1 byte longer than RFC2027-compliant, fixed
  • 2008-05-22: Minor improvement
    • Fixed potential problem with zip files
  • 2008-08-30: Minor improvements
    • Verified: Bugs still exist in .NET 3.5 SP1, and bugfixing of MailMergeLib works with it
    • Updated documentation
  • 2009-08-02:
    • Verified: Bugs still exist in .NET 4.0, and bugfixing of MailMergeLib works with it
    • SmtpClient's private member localHostName (up to .NET 3.5 SP1) changed to clientDomain in .NET 4.0 and Windows 7. MailMergeLib can deal with both now.
    • Fixed bug in System.Net.Mail with non-ASCII characters in the display name of an attachment (thanks to xrascal)
    • Updated documentation
  • 2009-12-23:
    • Project upgraded to Visual Studio 2008 and .NET 3.5 (MailMergeLib 3.0)
    • Many improvements, extensive code rewrites and refactoring, but still compatible with MailMergeLib 2.0. Depreciated class members are marked.
    • Fixed: Potential problem in QPEncoder (thanks to Pierre Arnould)
    • Fixed: Another bug of System.Net.Mail: text based attachment or body text is no more corrupted if it starts with a dot.
    • Added: More formatting options in placeholders
    • Added: Converter for generating a plain text body from HTML text
    • Added: MailMergeLib can handle almost any IList or IEnumerable data source
    • Improved and extended documentation
  • 2010-01-19:
    • Fixed a small bug in the library introduced with the last version
  • 2010-01-04:
    • Fixed: Bug in AttachmentBuilder(FileAttachment fa) which was introduced with the last version
  • 2010-02-04:
    • Fixed: Bug in MailMergeSender could cause an infinite loop under rare conditions. Thanks to Abdelkrim
  • 2010-05-01:
    • MailMergeLib can now compile und run under .NET Framework 2.0 through 4.0 (MailMergeLib 4.0)
    • Added StreamAttachment
    • Marked bug fixes for System.Net.Mail as obsolete where not needed for .NET 4.0
    • Improved and updated documentation
  • 2010-06-06:
    • Mails with TransferEncoding.QuotedPrintable and lines staring with a dot are now properly fixed by Bugfixer also under .NET 2.0
    • Upgraded from HtmlAgilityPack 1.3.0 to current stable 1.4.0
  • 2010-07-02:
    • Implemented workaround: System.Net.Mail wraps long filenames improperly in the Content-Type and Content-Disposition header sections of the attachment
  • 2010-09-23:
    • Attachments of a message are disposed() now after sending a message 
    • Fixed a bug in method MailMergeMessage.SetVariables
  • 2010-10-05:
    • Fixed CorrectSubjectEncodedWordRFC2047compliant for .NET 2.0
  • 2010-10-08:
    • Branch for .NET 2.0 to .NET 3.5 is now separated and will be discontinued
    • Code migrated to .NET 4.0 and VS 2010 (MailMergeLib 4.1)
  • 2013-03-31
    • By default MailMergeSender reads default values from configuration file using System.Configuration.ConfigurationManager
    • Make use of SMTP connection pooling in .NET 4.0. 
    • Underlying SmtpClient will be disposed when disposing MailMergeSender, which is possible since .NET 4.0.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Norbert Bietsch

Germany Germany
No Biography provided

Comments and Discussions

 
GeneralMy vote of 5 Pinmembercsharpbd16-Nov-12 9:43 
GeneralRe: My vote of 5 PinmemberNorbert Bietsch16-Nov-12 11:18 
SuggestionReturning the merged content Pinmemberdale_burrell2-Mar-12 11:32 
AnswerRe: Returning the merged content PinmemberNorbert Bietsch2-Mar-12 11:53 
SuggestionMaking the placeholder delimiters configurable Pinmemberdale_burrell2-Mar-12 11:26 
AnswerRe: Making the placeholder delimiters configurable [modified] PinmemberNorbert Bietsch2-Mar-12 11:44 
GeneralMy vote of 5 Pinmemberwaaqee16-Feb-12 1:42 
GeneralRe: My vote of 5 PinmemberNorbert Bietsch16-Feb-12 8:31 
Thanks a lot and stay tuned. I plan to add the Razor.Engine as an additional parser option, which is really cool Smile | :)
GeneralRe: My vote of 5 Pinmemberwaaqee16-Feb-12 19:36 
GeneralReally nice approach PinmemberNorbert Bietsch17-Feb-12 22:36 
GeneralMy vote of 5 Pinmembernz_ham23-Nov-11 11:30 
GeneralMy vote of 5 PinmemberLeonardoX2214-Oct-11 2:22 
Question1 Send or SendAsync operation at a time Pinmembervicentillo7-Oct-11 0:35 
GeneralRe: 1 Send or SendAsync operation at a time PinmemberNorbert Bietsch9-Oct-11 4:07 
GeneralRe: 1 Send or SendAsync operation at a time Pinmembervicentillo10-Oct-11 3:17 
GeneralThanks! Pinmembertoiyabe29-Jul-11 5:27 
GeneralRe: Thanks! PinmemberNorbert Bietsch29-Jul-11 8:31 
Generalmissing file name when Includefile:"file" Pinmemberbquick15-Apr-11 3:07 
AnswerRe: missing file name when Includefile:"file" PinmemberNorbert Bietsch15-Apr-11 22:46 
GeneralRe: missing file name when Includefile:"file" Pinmemberbquick18-Apr-11 3:08 
GeneralSymantec Endpoint antivirus workaround [modified] PinmemberNikolay Unguzov4-Apr-11 1:03 
Question"An invalid character was found in the mail header: 'с'." for .TransferEncoding PinmemberNikolay Unguzov23-Mar-11 8:16 
QuestionRe: "An invalid character was found in the mail header: 'с'." for .TransferEncoding PinmemberNorbert Bietsch27-Mar-11 7:12 
AnswerRe: "An invalid character was found in the mail header: 'с'." for .TransferEncoding [modified] PinmemberNikolay Unguzov2-Apr-11 21:30 
AnswerRe: "An invalid character was found in the mail header: 'с'." for .TransferEncoding PinmemberNorbert Bietsch3-Apr-11 11:40 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140821.2 | Last Updated 31 Mar 2013
Article Copyright 2007 by Norbert Bietsch
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid