Created
December 12, 2017 17:48
-
-
Save JPaulDuncan/7e599869ec8f95a3439b755c6866b8ec to your computer and use it in GitHub Desktop.
Microsoft Exchange Web Services (EWS) Connecting, filtering, attachment downloading, and archiving.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
| } | |
| } |
Author
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
Grab the Microsoft.Exchange.WebServices, Newtonsoft.Json, and log4net nuget packages.