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.From = new MailAddress(gmailUserAddress);
message.Subject = subject == null ? "" : subject;
message.Body = body == null ? "" : body;
message.IsBodyHtml = isBodyHtml;
foreach (string email in emailTo)
{
message.To.Add(email);
}
if (ccTo != null && ccTo.Length > 0)
{
foreach (string emailCc in ccTo)
{
message.CC.Add(emailCc);
}
}
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 { }
}
}
}
}
}
}
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):
="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:
="1.0"="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