Skip to content

Instantly share code, notes, and snippets.

@JPaulDuncan
Created December 12, 2017 17:48
Show Gist options
  • Select an option

  • Save JPaulDuncan/7e599869ec8f95a3439b755c6866b8ec to your computer and use it in GitHub Desktop.

Select an option

Save JPaulDuncan/7e599869ec8f95a3439b755c6866b8ec to your computer and use it in GitHub Desktop.
Microsoft Exchange Web Services (EWS) Connecting, filtering, attachment downloading, and archiving.
using log4net;
using Microsoft.Exchange.WebServices.Data;
using System;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Net;
using System.ServiceProcess;
using System.Timers;
namespace Sonic.Import.Mail
{
public partial class MailImportService : ServiceBase
{
public MailImportService()
{
InitializeComponent();
}
private ILog _log = LogManager.GetLogger(typeof(MailImportService));
private bool _isProcessing = false;
private Timer _timer = new Timer();
/// <summary>
/// Helper function for debugging the app in the IDE.
/// </summary>
/// <param name="args"></param>
internal void Debug(string[] args)
{
OnStart(args);
Console.ReadLine();
OnStop();
}
/// <summary>
/// Starts the service processing.
/// </summary>
/// <param name="args"></param>
protected override void OnStart(string[] args)
{
try
{
_log.Info("MailImport Service Started: " + string.Join(",", args));
var pollingInterval = ConfigurationManager.AppSettings["pollingInterval"];
var interval = 0;
interval = int.TryParse(pollingInterval, out interval) ? int.Parse(pollingInterval) : (60 * 5);
_timer.AutoReset = true;
_timer.Interval = interval * 1000;
_timer.Elapsed += (sender, e) => Process(sender, e);
_timer.Start();
}
catch (Exception ex)
{
Console.WriteLine(ex);
_log.Error("MailImport.OnStart", ex);
StopService();
}
}
/// <summary>
/// Just logs when the service stops.
/// </summary>
protected override void OnStop()
{
_log.Info("MailImport Service Stopped");
}
/// <summary>
/// Processes the emails when the timer fires.
/// </summary>
/// <param name="source"></param>
/// <param name="args"></param>
private void Process(object source, ElapsedEventArgs args)
{
if (_isProcessing) { return; }
_timer.Stop();
_isProcessing = true;
try
{
var account = ConfigurationManager.AppSettings["account"];
var password = ConfigurationManager.AppSettings["password"];
var folderName = ConfigurationManager.AppSettings["searchFolderName"];
var baseDirectory = ConfigurationManager.AppSettings["baseDirectory"];
var bodySearchPhrase = ConfigurationManager.AppSettings["bodySearchPhrase"];
String[] excludeStrings = { "\\", ",", ":", "*", "?", "\"", "<", ">", "|" };
// Get our certs and create our service instance.
ServicePointManager.ServerCertificateValidationCallback = CertificateValidationCallBack;
ExchangeService service = new ExchangeService(ExchangeVersion.Exchange2010_SP2)
{
Credentials = new WebCredentials(account, password)
};
// Set up our EWS connection
if (!AutodiscoverUrl(service, account))
{
StopService();
}
// Kill existing search folder
DeleteFolder(service, WellKnownFolderName.SearchFolders, folderName);
// Create new Search folder
CreateSearchFolder(service, folderName);
// Get the new search folder
SearchFilter filter = new SearchFilter.IsEqualTo(FolderSchema.DisplayName, folderName);
SearchFolder searchFolder = GetFolder(service, filter, WellKnownFolderName.SearchFolders) as SearchFolder;
// Get the archive folder
SearchFilter archiveFilter = new SearchFilter.IsEqualTo(FolderSchema.DisplayName, "Archive");
Folder archiveFolder = GetFolder(service, archiveFilter, WellKnownFolderName.MsgFolderRoot);
// Set the number of items we're checking at a time.
const Int32 pageSize = 10;
// Create our view
ItemView itemView = new ItemView(pageSize);
// Set up our find results
FindItemsResults<Item> findResults = null;
do
{
Console.WriteLine("Searching ...");
// Search for our items
findResults = searchFolder.FindItems(itemView);
Console.WriteLine("Found {0} total items.", findResults.TotalCount);
if (findResults != null && findResults.TotalCount > 0)
{
Console.WriteLine("Loading {0} items.", findResults.Count());
// What do we have here?
foreach (Item item in findResults)
{
// If this is an email message, let's go.
if (item is EmailMessage)
{
// Casting
EmailMessage email = item as EmailMessage;
// Binding
email = EmailMessage.Bind(service,
item.Id,
new PropertySet(
BasePropertySet.FirstClassProperties,
EmailMessageSchema.Attachments,
EmailMessageSchema.Body,
EmailMessageSchema.From)
);
// Loading
email.Load();
// See if we have to check the body.
bool isValid = true;
if (!string.IsNullOrEmpty(bodySearchPhrase))
{
if (email.Body.Text.IndexOf(bodySearchPhrase, StringComparison.CurrentCultureIgnoreCase) < 0)
{
Console.WriteLine("Message from {0} fails the body search phrase search.", email.From.Address);
isValid = false;
}
}
// Only deal with items that have attachments.
if (email.HasAttachments && isValid)
{
Console.WriteLine("Valid message from {0}.", email.From.Address);
Console.WriteLine("Found {0} attachments.", email.Attachments.Count);
// Go through all the attachments
foreach (var attachment in email.Attachments)
{
// Create target directory based on who sent us the thing.
var directoryName = email.From.Address;
// Ensure we don't have goofy directory names.
foreach (string str in excludeStrings)
{
directoryName = directoryName.Replace(str, "");
}
// Add to the passed base directory.
directoryName = Path.Combine(baseDirectory, directoryName);
// Create our target directory if it isn't already there.
if (!Directory.Exists(directoryName))
{
Console.WriteLine("Creating new directory: " + directoryName);
Directory.CreateDirectory(directoryName);
}
// Make sure the attachment isn't inline (cut & pasted in) and it's an actual file.
if ((!attachment.IsInline) && attachment is FileAttachment)
{
// Casting
FileAttachment fileAttachment = attachment as FileAttachment;
// Filename
var fileName = fileAttachment.Name;
Console.WriteLine("Saving attachment {0}.", fileName);
// Load it
fileAttachment.Load(Path.Combine(directoryName, fileName));
fileAttachment.Load();
}
}
}
// Move item to Archive
item.Move(archiveFolder.Id);
}
}
}
else
{
Console.WriteLine("No results.");
}
itemView.Offset += pageSize; // Next amount
} while (findResults.MoreAvailable);
Console.WriteLine("Done.");
_isProcessing = false;
_timer.Start();
}
catch (Exception ex)
{
Console.WriteLine(ex);
_log.Error("MailImport.Process", ex);
StopService();
}
}
#region Support Functions
/// <summary>
/// Stops the timer and Windows service.
/// </summary>
private void StopService()
{
_isProcessing = false;
if (_timer != null)
{
_timer.Stop();
}
if (CanStop == true)
{
Stop();
}
}
/// <summary>
/// Grabs the folder based on the given filter and Parent folder.
/// </summary>
/// <param name="service"></param>
/// <param name="filter"></param>
/// <param name="folder"></param>
/// <returns></returns>
private static Folder GetFolder(ExchangeService service, SearchFilter filter, WellKnownFolderName parentFolder)
{
if (service == null)
{
return null;
}
PropertySet propertySet = new PropertySet(BasePropertySet.IdOnly);
FolderView folderView = new FolderView(5);
folderView.PropertySet = propertySet;
FindFoldersResults searchResults = null;
if (filter == null)
{
searchResults = service.FindFolders(parentFolder, folderView);
}
else
{
searchResults = service.FindFolders(parentFolder, filter, folderView);
}
return searchResults.FirstOrDefault();
}
/// <summary>
/// Deletes the folder based on the folder name and parent folder.
/// </summary>
/// <param name="service"></param>
/// <param name="parentFolder"></param>
/// <param name="folderName"></param>
private static void DeleteFolder(ExchangeService service, WellKnownFolderName parentFolder, String folderName)
{
SearchFilter searchFilter = new SearchFilter.IsEqualTo(FolderSchema.DisplayName, folderName);
Folder folder = GetFolder(service, searchFilter, parentFolder);
if (folder != null)
{
Console.WriteLine("Delete the folder '{0}'", folderName);
folder.Delete(DeleteMode.HardDelete);
}
}
/// <summary>
/// This method creates and sets the search folder.
/// </summary>
private static SearchFolder CreateSearchFolder(ExchangeService service, String displayName)
{
if (service == null)
{
return null;
}
SearchFilter.SearchFilterCollection filters = new SearchFilter.SearchFilterCollection(LogicalOperator.And);
var subjectSearchPhrase = ConfigurationManager.AppSettings["subjectSearchPhrase"];
var searchDays = ConfigurationManager.AppSettings["searchDays"];
// Only search within a certain number of days.
var days = 0;
days = int.TryParse(searchDays, out days) ? int.Parse(searchDays) : 30;
DateTime startDate = DateTime.Now.AddDays(-days);
DateTime endDate = DateTime.Now;
filters.Add(new SearchFilter.IsGreaterThanOrEqualTo(EmailMessageSchema.DateTimeCreated, startDate));
filters.Add(new SearchFilter.IsLessThanOrEqualTo(EmailMessageSchema.DateTimeCreated, endDate));
// Only have attachments
filters.Add(new SearchFilter.IsEqualTo(EmailMessageSchema.HasAttachments, true));
// Do we need to search the subject line?
if (!string.IsNullOrEmpty(subjectSearchPhrase))
{
filters.Add(new SearchFilter.ContainsSubstring(EmailMessageSchema.Subject, subjectSearchPhrase, ContainmentMode.Substring, ComparisonMode.IgnoreCase));
}
// We're looking in the inbox.
FolderId folderId = new FolderId(WellKnownFolderName.Inbox);
// See if we already have our search filter folder.
Boolean isDuplicateFolder = true;
SearchFolder searchFolder = GetFolder(service, new SearchFilter.IsEqualTo(FolderSchema.DisplayName, displayName), WellKnownFolderName.SearchFolders) as SearchFolder;
// If there isn't an existing search folder, we create a new one.
if (searchFolder == null)
{
searchFolder = new SearchFolder(service);
isDuplicateFolder = false;
}
// Tell it where to start searching.
searchFolder.SearchParameters.RootFolderIds.Add(folderId);
// Tell it how deep to look.
searchFolder.SearchParameters.Traversal = SearchFolderTraversal.Shallow;
// Add our filter.
searchFolder.SearchParameters.SearchFilter = filters;
// Update the filters if it exists, otherwise, create it.
if (isDuplicateFolder)
{
searchFolder.Update();
}
else
{
searchFolder.DisplayName = displayName;
searchFolder.Save(WellKnownFolderName.SearchFolders);
}
return searchFolder;
}
/// <summary>
/// Determines the location of the Exchange Web Services (EWS) based on the given account name.
/// </summary>
/// <param name="service"></param>
/// <param name="account"></param>
/// <returns></returns>
private Boolean AutodiscoverUrl(ExchangeService service, string account)
{
Boolean isSuccess = false;
try
{
Console.WriteLine("Connecting to Exchange Online......");
service.AutodiscoverUrl(account, RedirectionUrlValidationCallback);
Console.WriteLine("Connected to Exchange Online.");
isSuccess = true;
}
catch (Exception ex)
{
Console.WriteLine(ex);
_log.Error("MailImport.AutodiscoverUrl", ex);
StopService();
}
return isSuccess;
}
#endregion
#region Callback Methods
/// <summary>
/// Returns where the EWS endpoint is located.
/// </summary>
/// <param name="redirectionUrl"></param>
/// <returns></returns>
public bool RedirectionUrlValidationCallback(string redirectionUrl)
{
// The default for the validation callback is to reject the URL.
bool result = false;
Uri redirectionUri = new Uri(redirectionUrl);
Console.WriteLine("Exchange Online located at: " + redirectionUri.ToString());
// Validate the contents of the redirection URL. In this simple validation
// callback, the redirection URL is considered valid if it is using HTTPS
// to encrypt the authentication credentials.
if (redirectionUri.Scheme == "https")
{
result = true;
}
return result;
}
/// <summary>
/// Validates the certificate for the EWS calls.
/// </summary>
/// <param name="sender"></param>
/// <param name="certificate"></param>
/// <param name="chain"></param>
/// <param name="sslPolicyErrors"></param>
/// <returns></returns>
public bool CertificateValidationCallBack(
object sender,
System.Security.Cryptography.X509Certificates.X509Certificate certificate,
System.Security.Cryptography.X509Certificates.X509Chain chain,
System.Net.Security.SslPolicyErrors sslPolicyErrors)
{
// If the certificate is a valid, signed certificate, return true.
if (sslPolicyErrors == System.Net.Security.SslPolicyErrors.None)
{
return true;
}
// If there are errors in the certificate chain, look at each error to determine the cause.
if ((sslPolicyErrors & System.Net.Security.SslPolicyErrors.RemoteCertificateChainErrors) != 0)
{
if (chain != null && chain.ChainStatus != null)
{
foreach (System.Security.Cryptography.X509Certificates.X509ChainStatus status
in chain.ChainStatus)
{
if ((certificate.Subject == certificate.Issuer) &&
(status.Status ==
System.Security.Cryptography.X509Certificates.X509ChainStatusFlags.UntrustedRoot))
{
// Self-signed certificates with an untrusted root are valid.
continue;
}
else
{
if (status.Status !=
System.Security.Cryptography.X509Certificates.X509ChainStatusFlags.NoError)
{
// If there are any other errors in the certificate chain, the certificate is invalid,
// so the method returns false.
return false;
}
}
}
}
// When processing reaches this line, the only errors in the certificate chain are
// untrusted root errors for self-signed certificates. These certificates are valid
// for default Exchange server installations, so return true.
return true;
}
else
{
// In all other cases, return false.
return false;
}
}
#endregion
}
}
@JPaulDuncan
Copy link
Author

Grab the Microsoft.Exchange.WebServices, Newtonsoft.Json, and log4net nuget packages.

@JPaulDuncan
Copy link
Author

The app.config file:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,Log4net" />
  </configSections>
  <appSettings>
    <add key="pollingInterval" value="1800"/> <!-- 1800 Seconds: 30 minutes -->
    <add key="searchDays" value="30"/> <!-- Days back I want to go -->
    <add key="searchFolderName" value="myoutlooksearchfoldername" />
    <add key="account" value="[email protected]" />
    <add key="password" value="myemailpassword" />
    <add key="baseDirectory" value="c:\emails"/> <!-- Where I want to put the emails -->
    <add key="bodySearchPhrase" value="text in the body to filter by"/>
    <add key="subjectSearchPhrase" value="text in the subject to filter by"/>
  </appSettings>
  <log4net>
    <root>
      <appender-ref ref="LogFileAppender" />
      <appender-ref ref="ConsoleAppender" />
    </root>
    <appender name="LogFileAppender" type="log4net.Appender.RollingFileAppender">
      <threshold value="ERROR" />
      <file value="c:\emails\log.txt" />
      <appendToFile value="true" />
      <rollingStyle value="Size" />
      <maxSizeRollBackups value="5" />
      <maximumFileSize value="10MB" />
      <staticLogFileName value="true" />
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%d [%t] %-5p %c %m%n" />
      </layout>
    </appender>
    <appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
      <threshold value="INFO" />
      <layout type="log4net.Layout.PatternLayout">
        <param name="Header" value="[Header]\r\n" />
        <param name="Footer" value="[Footer]\r\n" />
        <param name="ConversionPattern" value="%d [%t] %-5p %c %m%n" />
      </layout>
    </appender>
  </log4net>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
  </startup>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.Data.Services.Client" publicKeyToken="31bf3856ad364e35" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-5.8.3.0" newVersion="5.8.3.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.Data.Edm" publicKeyToken="31bf3856ad364e35" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-5.8.3.0" newVersion="5.8.3.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.Data.OData" publicKeyToken="31bf3856ad364e35" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-5.8.3.0" newVersion="5.8.3.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="System.IdentityModel.Tokens.Jwt" publicKeyToken="31bf3856ad364e35" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-5.1.5.0" newVersion="5.1.5.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.IdentityModel.Protocol.Extensions" publicKeyToken="31bf3856ad364e35" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-1.0.40306.1554" newVersion="1.0.40306.1554" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-10.0.0.0" newVersion="10.0.0.0" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
</configuration>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment