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

WCF Service to Send Emails With Attachments

, 11 Nov 2013
Rate this:
Please Sign up or sign in to vote.
Setting, configuring, and calling a WCF service to send emails with multiple file attachments.

Introduction 

I wanted to create a simple WCF service to be able to send emails with multiple large attachments. The service runs on a dedicated web server and is used by various applications running on different application servers. The source code was modified to use GMail for this example. 

In order to send a binary file to a WCF service the file content is converted to a Base64 text string. The file is then re-assembled on the server and attached to the email. You will need a temp/spooling folder on the server with read/write/modify permissions for the users group. File attachments don't need to be re-created on the server.  Everything is done in memory (see A Better  Way To Handle Attachments section at the end). 

This article will also cover server and client side configuration of the WCF service to be able to send large files. 

Let's Get Started 

Create a new WCF Service Application and call it EmailServices. Add a new class called FileAttachment which will hold file content and information about each file attachment.

sing System.Runtime.Serialization;
using System.IO;
 
namespace EmailServices
{
    [DataContract]
    public class FileAttachment
    {
        [DataMember]
        public string FileContentBase64 { get; set; }
 
        [DataMember]
        public FileInfo Info { get; set; }
    }
}

Rename IService1.cs to IMail.cs and replace its content with the following code:

using System.ServiceModel;
 
namespace EmailServices
{
    [ServiceContract]
    public interface IMail
    {
 
        [OperationContract]
        int SendEmail(string gmailUserAddress, string gmailUserPassword, string[] emailTo, 
          string[] ccTo, string subject, string body, bool isBodyHtml, FileAttachment[] attachments);
 
    }
}

The interface for our service exposes a single method called SendEmail which returns an integer. I prefer to return an integer rather than true/false value because web services are notoriously hard to debug and numeric codes can provide more information on where the error occurred.

Next, rename the Service1 class to Mail and replace its content with the following code: 

using System;
using System.Collections.Generic;
using System.Net.Mail;
using System.IO;
using System.Net.Mime;
using System.Net;
 
namespace EmailServices
{
    public class Mail : IMail
    {
        private static string SMTP_SERVER = "smtp.gmail.com";
        private static int SMTP_PORT = 587;
        private static string TEMP_FOLDER = @"C:\temp\";
 
        public int SendEmail(string gmailUserAddress, string gmailUserPassword, string[] emailTo, 
          string[] ccTo, string subject, string body, bool isBodyHtml, FileAttachment[] attachments)
        {
            int result = -100;
            if (gmailUserAddress == null || gmailUserAddress.Trim().Length == 0)
            {
                return 10;
            }
            if (gmailUserPassword == null || gmailUserPassword.Trim().Length == 0)
            {
                return 20;
            }
            if (emailTo == null || emailTo.Length == 0)
            {
                return 30;
            }
 
            string tempFilePath = "";
            List<string> tempFiles = new List<string>();
 
            SmtpClient smtpClient = new SmtpClient(SMTP_SERVER, SMTP_PORT);
            smtpClient.EnableSsl = true;
            smtpClient.DeliveryMethod = SmtpDeliveryMethod.Network;
            smtpClient.UseDefaultCredentials = false;
            smtpClient.Credentials = new NetworkCredential(gmailUserAddress, gmailUserPassword);
 
            using (MailMessage message = new MailMessage())
            //Message object must be disposed before deleting temp attachment files
            {
                message.From = new MailAddress(gmailUserAddress);
                message.Subject = subject == null ? "" : subject;
                message.Body = body == null ? "" : body; 
                message.IsBodyHtml = isBodyHtml;
 
                foreach (string email in emailTo)
                {
                    //TODO: Check email is valid
                    message.To.Add(email);
                }
 
                if (ccTo != null && ccTo.Length > 0)
                {
                    foreach (string emailCc in ccTo)
                    {
                        //TODO: Check CC email is valid
                        message.CC.Add(emailCc);
                    }
                }
 //There is a better way!!! See bellow...
                if (attachments != null && attachments.Length > 0)
                {
                    foreach (FileAttachment fileAttachment in attachments)
                    {
                        if (fileAttachment.Info == null || fileAttachment.FileContentBase64 == null)
                        {
                            continue;
                        }
 
                        tempFilePath = CreateTempFile(TEMP_FOLDER, fileAttachment.FileContentBase64);
 
                        if (tempFilePath != null && tempFilePath.Length > 0)
                        {
                            Attachment attachment = new Attachment(tempFilePath, MediaTypeNames.Application.Octet); 
                            ContentDisposition disposition = attachment.ContentDisposition;
                            disposition.FileName = fileAttachment.Info.Name;
                            disposition.CreationDate = fileAttachment.Info.CreationTime;
                            disposition.ModificationDate = fileAttachment.Info.LastWriteTime;
                            disposition.ReadDate = fileAttachment.Info.LastAccessTime;
                            disposition.DispositionType = DispositionTypeNames.Attachment;
                            message.Attachments.Add(attachment);
                            tempFiles.Add(tempFilePath);
                        }
                        else
                        {
                            return 50;
                        }
                    }
                }
 
                try
                {
                    smtpClient.Send(message);
                    result = 0;
                }
                catch
                {
                    result = 60;
                }
            }
 
            DeleteTempFiles(tempFiles.ToArray());
            return result;
        }
 

 
        private static string CreateTempFile(string destDir, string fileContentBase64)
        {
            string tempFilePath = destDir + (destDir.EndsWith("\\") ? 
              "" : "\\") + Guid.NewGuid().ToString();
 
            try
            {
                using (FileStream fs = new FileStream(tempFilePath, FileMode.Create))
                {
                    byte[] bytes = System.Convert.FromBase64String(fileContentBase64); ;
                    fs.Write(bytes, 0, bytes.Length);
                }
            }
            catch
            {
                return null;
            }
 
            return tempFilePath;
        }
 
        private static void DeleteTempFiles(string[] tempFiles)
        {
            if (tempFiles != null && tempFiles.Length > 0)
            {
                foreach (string filePath in tempFiles)
                {
                    if (File.Exists(filePath))
                    {
                        try
                        {
                            File.Delete(filePath);
                        }
                        catch { } //Do nothing
                    }
                }
            }
        }
    }
}

That's the guts of our service. Now let's go through it. 

I've hard-coded the SMTP address, port number, and temp folder location for this exercise, but the proper way is to put this information in a web config file and use WebConfigurationManager to pull it out at run time.

The code does some basic validation at the beginning to check that required information is provided. 

The interesting bit is where the program re-creates each file in the temp folder. I use Guid.NewGuid() to generate a unique file name to avoid collisions. The problem with this approach is that you need to change the file name and other properties when it's attached to the message. That's where FileInfo comes handy.

You also need to keep a list of all temp files created so they can be deleted after the email is sent. Keep in mind that MailMessage object has to be disposed off before you try to delete the temp file, otherwise you'll get an error that the file is still being used. 

Also, when the service runs on IIS, the account it runs as needs to have read/write/modify permission to the temp folder. I just gave all permissions to the users group.

Right mouse click on the Mail.svc file and select "View Markup". Make sure that service name is EmailServices.Mail - not  EmailServices.Service1

<%@ ServiceHost Language="C#" Debug="true" Service="EmailServices.Mail" CodeBehind="Mail.svc.cs" %>

And now is the tricky bit with the web.config file (thanks to David Casey for helping me with this):

<?xml version="1.0"?>
<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.0" />
    <httpRuntime executionTimeout="60" maxRequestLength="10240" />
  </system.web>
 
  <system.serviceModel>
    <services>
      <service name="EmailServices.Mail">
        <endpoint address="http://localhost:4280/Mail.svc"
                  contract="EmailServices.IMail"
                  binding="basicHttpBinding"
                  bindingConfiguration="EmailBinding" />
      </service>
    </services>
 
    <behaviors>
      <serviceBehaviors>
        <behavior>
          <serviceMetadata httpGetEnabled="true"/>
          <serviceDebug includeExceptionDetailInFaults="false"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
 
    <bindings>
      <basicHttpBinding>
        <binding name="EmailBinding"
                 closeTimeout="00:01:00"
                 openTimeout="00:01:00"
                 receiveTimeout="00:01:00"
                 sendTimeout="00:01:00"
                 allowCookies="false"
                 bypassProxyOnLocal="false"
                 hostNameComparisonMode="StrongWildcard"
                 maxBufferSize="2147483647"
                 maxBufferPoolSize="2147483647"
                 maxReceivedMessageSize="2147483647"
                 messageEncoding="Text"
                 textEncoding="utf-8"
                 transferMode="Buffered"
                 useDefaultWebProxy="true">
          <readerQuotas
                 maxDepth="32"
                 maxStringContentLength="2147483647"
                 maxArrayLength="20971520"
                 maxBytesPerRead="4096"
                 maxNameTableCharCount="16384" />
          <security mode="None">
            <transport clientCredentialType="None" proxyCredentialType="None"
                realm="" />
            <message clientCredentialType="UserName" algorithmSuite="Default" />
          </security>
        </binding>
      </basicHttpBinding>
    </bindings>
  </system.serviceModel>
 
  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true"/>
  </system.webServer>
 
</configuration>

This is my debug configuration. I've capped the email size to 10MB and timeout to 1 minute. The endpoint address and port number may be different on your machine. If you hit F5 to run the project now, the ASP.NET Development Server should fire up - note the port number it runs on and adjust your config accordingly.

Now let's test our email service.

Make sure the service is running on the ASP.NET Development Server or IIS and you can access http://localhost:4280/Mail.svc in your browser. Create a new console application project in a separate solution. Right mouse click on the References folder in Solution Explorer and select "Add Service Reference..." 

Paste http://localhost:4280/Mail.svc in the address field and click the "Go" button. Our service should appear in the Services list. Type EmailServRef in the namespace filed and click OK. The following code shows an example of calling our service: 

using System;
using System.IO;
using System.Collections.Generic;
 
namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            string emailFrom = "my_email@gmail.com";
            string password = "myPassword";
            string emailTo = "mybestfriend@emailaddress.com";
            string fileAttachmentPath = @"C:\TextFile.txt";
            int result = -1;
 
            try
            {
                using (EmailServRef.MailClient client = new EmailServRef.MailClient())
                {
                    List<EmailServRef.FileAttachment> allAttachments = new List<EmailServRef.FileAttachment>();
                    EmailServRef.FileAttachment attachment = new EmailServRef.FileAttachment();
                    attachment.Info = new FileInfo(fileAttachmentPath);
                    attachment.FileContentBase64 = Convert.ToBase64String(File.ReadAllBytes(fileAttachmentPath));
                    allAttachments.Add(attachment);
 
                    result = client.SendEmail(emailFrom, password, new string[] { emailTo }, null, 
                      "It works!!!", "Body text", false, allAttachments.ToArray());
                    Console.WriteLine("Result: " + result);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
            Console.ReadLine();
        }
    }
}

And you also need to adjust the app.config file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.serviceModel>
        <bindings>
            <basicHttpBinding>
                <binding name="BasicHttpBinding_IMail" 
                         closeTimeout="00:01:00"
                         openTimeout="00:01:00" 
                         receiveTimeout="00:10:00" 
                         sendTimeout="00:01:00"
                         allowCookies="false" 
                         bypassProxyOnLocal="false" 
                         hostNameComparisonMode="StrongWildcard"
                         maxBufferSize="2147483647" 
                         maxBufferPoolSize="2147483647" 
                         maxReceivedMessageSize="2147483647"
                         messageEncoding="Text" 
                         textEncoding="utf-8" 
                         transferMode="Buffered"
                         useDefaultWebProxy="true">
                    <readerQuotas 
                         maxDepth="32" 
                         maxStringContentLength="2147483647" 
                         maxArrayLength="20971520"
                         maxBytesPerRead="4096" 
                         maxNameTableCharCount="16384" />
                    <security mode="None">
                        <transport clientCredentialType="None" proxyCredentialType="None"
                            realm="" />
                        <message clientCredentialType="UserName" algorithmSuite="Default" />
                    </security>
                </binding>
            </basicHttpBinding>
        </bindings>
        <client>
            <endpoint address="http://localhost:4280/Mail.svc" binding="basicHttpBinding"
                bindingConfiguration="BasicHttpBinding_IMail" contract="EmailServRef.IMail"
                name="BasicHttpBinding_IMail" />
        </client>
    </system.serviceModel>
</configuration>

Make any required adjustments as you feel like and run the app. Did it work? 

All that's left now is to deploy the email service on IIS and to give it a proper web address like http://EmailServices/Mail.svc.

That's it. Let me know if you have good ideas on how to improve it.

A Better Way to Handle Attachments

Added 2013-11-10:

I've realized, that you don't need to re-create attachment files on the server. It's better to use MemoryStream to accomplish this task in-memory:

if (attachments != null && attachments.Length > 0)
{
    foreach (FileAttachment fileAttachment in attachments)
    {
        byte[] bytes = System.Convert.FromBase64String(fileAttachment.FileContentBase64);
        MemoryStream memAttachment = new MemoryStream(bytes);
        Attachment attachment = new Attachment(memAttachment, fileAttachment.Info.Name);
        message.Attachments.Add(attachment);
    }
} 

History

  • Created on 2012-11-11
  • Added a better way to handle attachments 2013-11-10

License

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

About the Author

Max Vagner
Web Developer Avis
Australia Australia
No Biography provided

Comments and Discussions

 
QuestionType 'System.Net.Mail.MailAddress' cannot be serialized. PinmemberJBobby4-Jul-14 22:00 
AnswerRe: Type 'System.Net.Mail.MailAddress' cannot be serialized. PinmemberMax Vagner6-Jul-14 1:18 
QuestionI Got error when I was copy and paste the web-config PinmemberSurezz24-Jun-14 0:36 
AnswerRe: I Got error when I was copy and paste the web-config PinmemberMax Vagner24-Jun-14 1:09 
QuestionStreams PinmvpSacha Barber11-Nov-13 5:57 
AnswerRe: Streams PinmemberMax Vagner11-Nov-13 9:03 
QuestionGetting Error with FileAttachment Parameter Pinmembernbpatel29-Nov-13 0:50 
AnswerRe: Getting Error with FileAttachment Parameter PinmemberMax Vagner9-Nov-13 23:34 
GeneralRe: Getting Error with FileAttachment Parameter Pinmembernbpatel210-Nov-13 17:11 
GeneralRe: Getting Error with FileAttachment Parameter PinmemberMax Vagner10-Nov-13 20:00 
GeneralRe: Getting Error with FileAttachment Parameter Pinmembernbpatel210-Nov-13 21:16 
GeneralRe: Getting Error with FileAttachment Parameter PinmemberMax Vagner11-Nov-13 0:05 
GeneralRe: Getting Error with FileAttachment Parameter PinmemberStephen Zhou13-Nov-13 5:46 
GeneralRe: Getting Error with FileAttachment Parameter PinmemberMax Vagner13-Nov-13 19:40 
GeneralRe: Getting Error with FileAttachment Parameter Pinmembernbpatel227-Nov-13 0:01 
Questionhi max Pinmemberkhatkarmadhu4-Sep-13 20:04 
QuestionRegarding mail sending with wcf PinmemberTridip Bhattacharjee6-Feb-13 20:07 
thanks for your article but i just do not understand why any people like to send mail with WCF because when you send mail and send attachment then those things will go first to server where WCF service is running and from there mail will be send. do not u think it will be bit slower process....rather we can send mail directly. would u plzz tell me what was your thought behind this mail sending with wcf.
tbhattacharjee

AnswerMessage Removed Pinmemberbburnette557-Feb-13 3:02 
GeneralRe: Regarding mail sending with wcf PinmemberMember 1011024011-Nov-13 4:02 
GeneralRe: Regarding mail sending with wcf PinmemberMax Vagner11-Nov-13 8:59 

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
Web04 | 2.8.140721.1 | Last Updated 11 Nov 2013
Article Copyright 2012 by Max Vagner
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid