Click here to Skip to main content
15,868,016 members
Articles / Programming Languages / C#

WCF Service to Send Emails With Attachments

Rate me:
Please Sign up or sign in to vote.
4.63/5 (19 votes)
11 Nov 2013CPOL4 min read 83.6K   20   61   31
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.

C#
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:

C#
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: 

C#
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

XML
<%@ 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
<?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: 

C#
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
<?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:

C#
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)


Written By
Web Developer
Australia Australia
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionCloud Email delivery service Pin
Jamil Hallal5-Aug-16 22:01
professionalJamil Hallal5-Aug-16 22:01 
AnswerRe: Cloud Email delivery service Pin
kiquenet.com8-Mar-19 23:02
professionalkiquenet.com8-Mar-19 23:02 
QuestionFileAttachment is missing from Browse Code Pin
TheBigEasy24-Sep-14 1:44
TheBigEasy24-Sep-14 1:44 
AnswerRe: FileAttachment is missing from Browse Code Pin
Max Vagner24-Sep-14 3:41
Max Vagner24-Sep-14 3:41 
GeneralRe: FileAttachment is missing from Browse Code Pin
TheBigEasy24-Sep-14 4:34
TheBigEasy24-Sep-14 4:34 
QuestionError when attaching files with 64kb+ size Pin
noobisce25-Aug-14 1:29
noobisce25-Aug-14 1:29 
AnswerRe: Error when attaching files with 64kb+ size Pin
Max Vagner25-Aug-14 14:05
Max Vagner25-Aug-14 14:05 
GeneralRe: Error when attaching files with 64kb+ size Pin
noobisce25-Aug-14 16:42
noobisce25-Aug-14 16:42 
GeneralRe: Error when attaching files with 64kb+ size Pin
Max Vagner27-Aug-14 15:00
Max Vagner27-Aug-14 15:00 
GeneralRe: Error when attaching files with 64kb+ size Pin
noobisce15-Sep-14 18:41
noobisce15-Sep-14 18:41 
QuestionType 'System.Net.Mail.MailAddress' cannot be serialized. Pin
JBobby4-Jul-14 22:00
JBobby4-Jul-14 22:00 
AnswerRe: Type 'System.Net.Mail.MailAddress' cannot be serialized. Pin
Max Vagner6-Jul-14 1:18
Max Vagner6-Jul-14 1:18 
QuestionI Got error when I was copy and paste the web-config Pin
Surezz24-Jun-14 0:36
Surezz24-Jun-14 0:36 
AnswerRe: I Got error when I was copy and paste the web-config Pin
Max Vagner24-Jun-14 1:09
Max Vagner24-Jun-14 1:09 
QuestionStreams Pin
Sacha Barber11-Nov-13 5:57
Sacha Barber11-Nov-13 5:57 
AnswerRe: Streams Pin
Max Vagner11-Nov-13 9:03
Max Vagner11-Nov-13 9:03 
QuestionGetting Error with FileAttachment Parameter Pin
nbpatel29-Nov-13 0:50
nbpatel29-Nov-13 0:50 
AnswerRe: Getting Error with FileAttachment Parameter Pin
Max Vagner9-Nov-13 23:34
Max Vagner9-Nov-13 23:34 
GeneralRe: Getting Error with FileAttachment Parameter Pin
nbpatel210-Nov-13 17:11
nbpatel210-Nov-13 17:11 
GeneralRe: Getting Error with FileAttachment Parameter Pin
Max Vagner10-Nov-13 20:00
Max Vagner10-Nov-13 20:00 
GeneralRe: Getting Error with FileAttachment Parameter Pin
nbpatel210-Nov-13 21:16
nbpatel210-Nov-13 21:16 
GeneralRe: Getting Error with FileAttachment Parameter Pin
Max Vagner11-Nov-13 0:05
Max Vagner11-Nov-13 0:05 
GeneralRe: Getting Error with FileAttachment Parameter Pin
Stephen Zhou13-Nov-13 5:46
Stephen Zhou13-Nov-13 5:46 
GeneralRe: Getting Error with FileAttachment Parameter Pin
Max Vagner13-Nov-13 19:40
Max Vagner13-Nov-13 19:40 
GeneralRe: Getting Error with FileAttachment Parameter Pin
nbpatel227-Nov-13 0:01
nbpatel227-Nov-13 0:01 
Hi Max,

I am waiting for your code, but it is yet to be approved by Website Admin team. Appreciate If you can send me on my mail address.

niraj.patel@essar.com

Thanks in Advance

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

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