Wednesday, 8 January 2014

Save attachments from an Exchange mail box using the EWS Managed API

So from C# I wanted be able to save email attachments being sent to an Exchange mailbox. I bet you're all like 'good luck dude, that POP/imap/mapi stuff is terrible and really I can't believe no one has sorted out a good way to do it yet'.  Yeah well screw you, have you heard about the Exchange Web Service Managed API? It's pretty sweet. It's easy to setup, easy to use & pretty intuitive. We get to use predefined objects AND I LOVE OBJECTS. This is how to do it.

The summary version:
You get a dll off microsoft, reference it in your project, give it some authentication details. It will work out where your exchange server is (i.e. you don't need to tell it the server name/ip or manually add the EWS as a service reference to your project).  Then you get to mess around with the supplied objects from that dll to pull and push data from the exchange server.  When pulling back objects from the server (e.g. an email) you often get the ability to pull back just a little bit of info (e.g. the id) or to get more details (e.g. base properties or even specific properties).  If you make a change to an object you need to push that change back to the server by calling a method on that object (e.g. message.Update(...)).

The longer version:

First you need to download the Microsoft dlls here that will give you the nifty objects and handle all the client/server communication:

http://www.microsoft.com/en-nz/download/details.aspx?id=35371

This is an MSI.  But whatevs, it puts a couple of DLLs on your system that you can then copy and reference from your project:



First you've got to create your service object.  Supply it the schema definition that you want to work with.  From what I understand this is backwards compatible; for example if you hook it up to Exchange2010 SP1, when you move to SP2 your code against the SP1 schema will still work fine, but you have the option of updating your code to run against SP2 which will have more functionality. The below code uses Microsoft Black Magic® to figure out where your exchange server is and how to talk to it's associated EWS.

static ExchangeService _service = new ExchangeService(ExchangeVersion.Exchange2010_SP2); 

static Program() 

    _service.Credentials = new WebCredentials("myusername", "password");
    _service.AutodiscoverUrl("my.email@somewhere.com"); 


Next, get a reference to a specific folder in the Exchange mailbox.  The below method takes a folder name and searches in the inbox for that folder. This uses the FindFolders method on the service object. To use it just give it a starting point (e.g. the inbox or the root) a search filter (an object that describes what we're looking for) and a view (an object that describes what we want returned e.g. what properties we require on the results objects).

public static Folder GetFolderByName(string folderName)
{
    FolderView view = new FolderView(10);
    view.PropertySet = new PropertySet(BasePropertySet.IdOnly); // just give us the id in the results object
    view.PropertySet.Add(FolderSchema.DisplayName); // on second thoughts, also give us the folder name
    view.Traversal = FolderTraversal.Deep; // search sub folders

    SearchFilter searchFilter = new SearchFilter.IsEqualTo(FolderSchema.DisplayName, folderName);

    FindFoldersResults findFolderResults = _service.FindFolders(WellKnownFolderName.Inbox, searchFilter, view);
    if(findFolderResults.Count() > 1)
        throw new Exception(string.Format("Found {0} folders with title '{1}' - the name needs to be unique; don't know which one to use", findFolderResults.Count(), folderName));

    return findFolderResults.FirstOrDefault();
}



Once you've got a folder you can find the unread emails in that folder.  This runs a search similar to the above, calling the FindItems method on the service object.  Pass it the folder id you want to search in and a filter (describing what we're looking for) and a view (describing what we want returned).  Then we do some clowning around to get the message as an EmailMessage type with some specific properties loaded (.Attachments and .HasAttachments) rather than the generic Item type.


public static List<EmailMessage> GetUnreadEmailsInFolder(Folder folder)
{
    var messages = new List<EmailMessage>();

    // only get unread emails
    SearchFilter folderSearchFilter = new SearchFilter.IsEqualTo(EmailMessageSchema.IsRead, false);
    // we just need the id in our results
    var itemView = new ItemView(10) {PropertySet = new PropertySet(BasePropertySet.IdOnly)};

    FindItemsResults<Item> findResults = _service.FindItems(folder.Id, folderSearchFilter, itemView);
         
    foreach (Item item in findResults.Items.Where(i => i is EmailMessage))
    {
        EmailMessage message = EmailMessage.Bind(_service, item.Id, new PropertySet(BasePropertySet.IdOnly, ItemSchema.Attachments, ItemSchema.HasAttachments));
        messages.Add(message);
    }

    return messages;
}


Now we have a list of emails.  Loop through them, get any attachments from the Exchange server and save them to disk.  When dealing with attachments it seems like you need to explicitly call the .Load() method of the attachment to pull it from Exchange.
To set the email as having been read just update the IsRead property and then call the .Update(...) method to push the change back to Exchange.

public static void ProcessEmail(EmailMessage message)
{
    string saveDir = ConfigurationManager.AppSettings["AttachmentSaveDirectory"];
    if (message.HasAttachments)
    {
        foreach (Attachment attachment in message.Attachments.Where(a=> a is FileAttachment))
        {
            FileAttachment fileAttachment = attachment as FileAttachment;
            fileAttachment.Load(); // populate the content property of the attachment

            using (FileStream fs = new FileStream(saveDir + attachment.Name, FileMode.Create))
            {
                using (BinaryWriter w = new BinaryWriter(fs))
                {
                    w.Write(fileAttachment.Content);
                }
            }
        }
    }
    message.IsRead = true;
    message.Update(ConflictResolutionMode.AutoResolve); // push changes back to server
}



Pack it up pack it in, jobs done.

1 comment:

  1. These examples are also interesting.

    http://www.independentsoft.de/exchangewebservices/tutorial/index.html

    - Pari

    ReplyDelete