C# Saving Exchange .EML file from within Windows Service

Viewed 964

I'm currently writing a Windows Service to log in to a specific Exchange account, get any new emails, parse them, and save the email in the appropriate folder.

Everything is working perfectly, except saving the email.

Relevant code blocks (try / catch blocks and irrelevant stuff removed to keep it short):-

Set up the Service

ExchangeService service = new ExchangeService(ExchangeVersion.Exchange2013_SP1);
service.Credentials = new WebCredentials(emailAddress, password);
service.AutodiscoverUrl(emailAddress, RedirectionUrlValidationCallback);
CheckForNewEmails(service);

Getting and checking new emails

private static void CheckForNewEmails(ExchangeService service)
{
    int offset = 0;
    int pageSize = 50;
    bool more = true;
    ItemView view = new ItemView(pageSize, offset, OffsetBasePoint.Beginning);
    view.PropertySet = PropertySet.IdOnly;
    FindItemsResults<Item> findResults;
    List<EmailMessage> emails = new List<EmailMessage>();

    while (more)
    {
        findResults = service.FindItems(WellKnownFolderName.Inbox, view);
        foreach (var item in findResults.Items)
        {
            emails.Add((EmailMessage)item);
        }
        more = findResults.MoreAvailable;
        if (more)
        {
            view.Offset += pageSize;
        }
    }

    if (emails.Count > 0)
    {
        PropertySet properties = (BasePropertySet.FirstClassProperties);
        service.LoadPropertiesForItems(emails, properties);
        var mailItems = new List<DatabaseService.MailItem>();
        var dbService = new DatabaseService();
        var defaultUser = dbService.GetDefaultUser(defaultUserID);

        foreach (var email in emails)
        {
            var mailItem = new DatabaseService.MailItem();
            mailItem.mail = email;
            mailItem.MessageID = email.InternetMessageId;
            mailItem.Sender = email.Sender.Address;
            dbService.FindLinks(service, ref mailItem, defaultUser);
            mailItems.Add(mailItem);
            LogMessage += (string.Format("Message ID : {1}{0}Sent : {2}{0}From : {3}{0}Subject : {4}{0}Hash : {5}{0}{6}{0}{0}", Environment.NewLine,
                                         mailItem.MessageID ,
                                         email.DateTimeSent.ToString("dd/MM/yyyy hh:mm:ss"),
                                         email.Sender,
                                         email.Subject,
                                         mailItem.Hash,
                                         mailItem.LinkString
                                         ));
        }
    }
}

Finding who it should be linked to

public void FindLinks(ExchangeService service, ref MailItem mailItem, User defaultUser)
{
    string address = mailItem.Sender;

    // get file hash
    var tempPath = Path.GetTempPath();
    var fileName = GetFilenameFromSubject(mailItem);
    var fullName = Path.Combine(tempPath, fileName);
    SaveAsEML(service, mailItem, fullName);
    var sha = new SHA256Managed();
    mailItem.Hash = Convert.ToBase64String(sha.ComputeHash(File.OpenRead(fullName)));
    File.Delete(fullName);

    using (var db = DatabaseHelpers.GetEntityModel())
    {
        // Do all the linking stuff
    }
}

And finally, the problem area: Saving the file to disk (in this case to a temporary folder so I can get the file hash, to check it's not a duplicate (e.g. CC'd etc))

Looking through StackOverflow and various other sources, there seem to be two ways to do this:-

1) Using the MailItem.Load method

private string SaveAsEML(ExchangeService service, MailItem mailItem, string savePath)
{
    using (FileStream fileStream = File.Open(savePath, FileMode.Create, FileAccess.Write))
    {
        mailItem.mail.Load(new PropertySet(ItemSchema.MimeContent));
        fileStream.Write(mailItem.mail.MimeContent.Content, 0, mailItem.mail.MimeContent.Content.Length);
    }
}

Using the above code, the file is created and has the correct content.

However, attempting to access any of the email properties AFTER this point results in a Null Exception crash (big crash too, no Try/Catch or UnhandledException trap will pick it up, just kills the Service)

In the above code, this line crashes the entire service:-

LogMessage += (string.Format("Message ID : {1}{0}Sent : {2}{0}From : {3}{0}Subject : {4}{0}Hash : {5}{0}{6}{0}{0}", Environment.NewLine,
               mailItem.MessageID ,
               email.DateTimeSent.ToString("dd/MM/yyyy hh:mm:ss"),
               email.Sender,
               email.Subject,
               mailItem.Hash,
               mailItem.LinkString
));

Specifically, referencing email.DateTimeSent

I added in some diagnostic code directly before this line:-

if (email == null) { Log it's null; } else { Log NOT null;}
if (email.DateTimeSent == null) { Log it's null; } else { Log NOT null; }

The first line logs NOT null, so email still exists.

However, the second line instantly crashes with a null exception error, without logging anything.

If I comment out the line SaveAsEML(service, mailItem, fullName); from FindLinks then everything works perfectly (except of course the file isn't saved).

2) Binding a Property Set

private string SaveAsEML(ExchangeService service, MailItem mailItem, string savePath)
{
    using (FileStream fileStream = File.Open(savePath, FileMode.Create, FileAccess.Write))
    {
        PropertySet props = new PropertySet(EmailMessageSchema.MimeContent);
        var email = EmailMessage.Bind(service, mailItem.mail.Id, props);
        fileStream.Write(mailItem.mail.MimeContent.Content, 0, mailItem.mail.MimeContent.Content.Length);
    }
}

Doing it this way, nothing crashes, it goes through every email just fine, and can reference email.DateTimeSent and all the other properties.

Unfortunately, it creates zero-length files, no content.

Been banging my head against the wall for hours now over this (took me an hour of adding diagnostics everywhere just to track down the crash happening when referencing properties), and it's undoubtedly something trivial I've overlooked, so if some kind soul could point out my stupidity I'd be most grateful!

Edit:

I was able to work around the above problem easily enough by simply saving the value of the properties before using them:-

foreach (var email in emails)
{
    var dateTimeSent = email.DateTimeSent;
    var sender = email.Sender;
    var subject = email.Subject;
    var mailItem = new DatabaseService.MailItem();
    mailItem.mail = email;
    mailItem.MessageID = email.InternetMessageId;
    mailItem.Sender = email.Sender.Address;
    dbService.FindLinks(service, ref mailItem, defaultUser);
    mailItems.Add(mailItem);
    LogMessage += (string.Format("Message ID : {1}{0}Sent : {2}{0}From : {3}{0}Subject : {4}{0}Hash : {5}{0}{6}{0}{0}", Environment.NewLine,
                                 mailItem.MessageID,
                                 dateTimeSent.ToString("dd/MM/yyyy hh:mm:ss"),
                                 sender,
                                 subject,
                                 mailItem.Hash ?? "No Hash",
                                 mailItem.LinkString ?? "No Linkstring"
                                 ));
}

This allowed me to use the MailItem.Load method which saved the file, and thus do what I was after in this specific case.

However there are other things this Service will need to do, and I really don't want to have to save a copy of every single property I'll need to access.

1 Answers

The reason this would fail

    private string SaveAsEML(ExchangeService service, MailItem mailItem, string savePath)
    {
        using (FileStream fileStream = File.Open(savePath, FileMode.Create, FileAccess.Write))
        {
            PropertySet props = new PropertySet(EmailMessageSchema.MimeContent);
            var email = EmailMessage.Bind(service, mailItem.mail.Id, props);
            fileStream.Write(mailItem.mail.MimeContent.Content, 0, mailItem.mail.MimeContent.Content.Length);
        }
    }

Is that you have used Bind to Load the message with the MimeContent in the email variable and then you haven't used that. mailItem.mail won't be linked at all to email variable created as part of this operation (even though they are the same object on the server). Locally these are just two separate variables. EWS is client/server so you make a request and the Managed API will return a local object that represents the result of the operation. But that object is disconnected hence when you do the bind above it just generates another client object to represent the result of that operation. eg so the above should have been

     private string SaveAsEML(ExchangeService service, MailItem mailItem, string savePath)
    {
        using (FileStream fileStream = File.Open(savePath, FileMode.Create, FileAccess.Write))
        {
            PropertySet props = new PropertySet(EmailMessageSchema.MimeContent);
            var email = EmailMessage.Bind(service, mailItem.mail.Id, props);
            fileStream.Write(email.MimeContent.Content, 0, email .MimeContent.Content.Length);
        }
    }
Related