From 16069b7b16943b3ef58ea03f30a36dee8775a0d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Walukiewicz?= Date: Thu, 2 Jan 2014 16:21:55 +0100 Subject: [PATCH] Initial commit. v0.0.0 --- .gitignore | 179 ++++++++++ EwsMailDl.sln | 26 ++ EwsMailDl/EmailDownloader.cs | 190 +++++++++++ EwsMailDl/EwsMailDl.csproj | 133 ++++++++ EwsMailDl/Program.cs | 486 +++++++++++++++++++++++++++ EwsMailDl/Properties/AssemblyInfo.cs | 36 ++ EwsMailDl/ServiceInstaller.cs | 56 +++ EwsMailDl/Settings.cs | 186 ++++++++++ license.md | 20 ++ readme.md | 140 ++++++++ 10 files changed, 1452 insertions(+) create mode 100644 .gitignore create mode 100644 EwsMailDl.sln create mode 100644 EwsMailDl/EmailDownloader.cs create mode 100644 EwsMailDl/EwsMailDl.csproj create mode 100644 EwsMailDl/Program.cs create mode 100644 EwsMailDl/Properties/AssemblyInfo.cs create mode 100644 EwsMailDl/ServiceInstaller.cs create mode 100644 EwsMailDl/Settings.cs create mode 100644 license.md create mode 100644 readme.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..800cdf0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,179 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +x64/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets +!packages/*/build/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +#NUNIT +*.VisualState.xml +TestResult.xml + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding addin-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +*.ncrunch* +_NCrunch_* +.*crunch*.local.xml + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml + +# NuGet Packages Directory +## TODO: If you have NuGet Package Restore enabled, uncomment the next line +#packages/ +## TODO: If the tool you use requires repositories.config, also uncomment the next line +#!packages/repositories.config + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +sql/ +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file to a newer +# Visual Studio version. Backup files are not needed, because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +App_Data/*.mdf +App_Data/*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# ========================= +# Windows detritus +# ========================= + +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ \ No newline at end of file diff --git a/EwsMailDl.sln b/EwsMailDl.sln new file mode 100644 index 0000000..ce257df --- /dev/null +++ b/EwsMailDl.sln @@ -0,0 +1,26 @@ + +Microsoft Visual Studio Solution File, Format Version 11.00 +# Visual C# Express 2010 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EwsMailDl", "EwsMailDl\EwsMailDl.csproj", "{EA83B646-A276-44EB-A735-67D61738531B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {EA83B646-A276-44EB-A735-67D61738531B}.Debug|x64.ActiveCfg = Debug|x64 + {EA83B646-A276-44EB-A735-67D61738531B}.Debug|x64.Build.0 = Debug|x64 + {EA83B646-A276-44EB-A735-67D61738531B}.Debug|x86.ActiveCfg = Debug|x86 + {EA83B646-A276-44EB-A735-67D61738531B}.Debug|x86.Build.0 = Debug|x86 + {EA83B646-A276-44EB-A735-67D61738531B}.Release|x64.ActiveCfg = Release|x64 + {EA83B646-A276-44EB-A735-67D61738531B}.Release|x64.Build.0 = Release|x64 + {EA83B646-A276-44EB-A735-67D61738531B}.Release|x86.ActiveCfg = Release|x86 + {EA83B646-A276-44EB-A735-67D61738531B}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/EwsMailDl/EmailDownloader.cs b/EwsMailDl/EmailDownloader.cs new file mode 100644 index 0000000..18324fb --- /dev/null +++ b/EwsMailDl/EmailDownloader.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using Microsoft.Exchange.WebServices.Data; + +namespace EwsMailDl +{ + class EmailDownloader + { + private EventLog eventLog; + + private List emailIdCache = new List(10); + + private CancellationTokenSource tokenSource; + + private BlockingCollection emailIdQueue; + + private ExchangeService exchangeService; + + private string savePath; + + private IList subjectFilters; + + private bool timestamp; + + public EmailDownloader(EventLog eventLog, BlockingCollection emailIdQueue, CancellationTokenSource tokenSource, Settings settings) + { + this.eventLog = eventLog; + this.tokenSource = tokenSource; + this.emailIdQueue = emailIdQueue; + this.exchangeService = settings.CreateExchangeService(); + this.savePath = settings.SavePath; + this.subjectFilters = settings.SubjectFilters; + this.timestamp = settings.Timestamp; + } + + public void Run() + { + while (!tokenSource.IsCancellationRequested && !emailIdQueue.IsAddingCompleted) + { + ItemId emailId = null; + + try + { + emailId = emailIdQueue.Take(tokenSource.Token); + } + catch (OperationCanceledException) + { + if (tokenSource.IsCancellationRequested) + { + if (Environment.UserInteractive) + { + Console.WriteLine("Downloader cancelled!"); + } + + break; + } + } + + if (emailId == null || emailIdCache.Contains(emailId.UniqueId)) + { + if (Environment.UserInteractive) + { + Console.WriteLine("Ignoring a duplicate e-mail: {0}", emailId); + } + + continue; + } + + if (emailIdCache.Count == emailIdCache.Capacity) + { + emailIdCache.RemoveAt(0); + } + + emailIdCache.Add(emailId.UniqueId); + + EmailMessage email = null; + + try + { + email = EmailMessage.Bind( + exchangeService, + emailId, + new PropertySet( + EmailMessageSchema.Subject, + EmailMessageSchema.Attachments, + EmailMessageSchema.DateTimeReceived + ) + ); + } + catch (Exception) { } + + if (email == null) + { + continue; + } + + if (MatchEmail(email)) + { + if (Environment.UserInteractive) + { + Console.WriteLine("Processing a new matching e-mail: {0}", email.Subject); + } + + DownloadAndDelete(email); + } + else if (Environment.UserInteractive && email != null) + { + Console.WriteLine("Ignoring a not matching e-mail: {0}", email.Subject); + } + } + } + + private bool MatchEmail(EmailMessage email) + { + return email.Attachments.Count > 0 + && (subjectFilters.Count == 0 || subjectFilters.Any(phrase => email.Subject.ToLower().Contains(phrase.ToLower()))); + } + + private void DownloadAndDelete(EmailMessage email) + { + foreach (var attachment in email.Attachments) + { + if (!(attachment is FileAttachment)) + { + continue; + } + + var fileAttachment = attachment as FileAttachment; + + try + { + if (Environment.UserInteractive) + { + Console.WriteLine("Downloading an attachment: {0}", fileAttachment.Name); + } + + var filePath = CreateFilePath(email.DateTimeReceived, fileAttachment.Name); + + fileAttachment.Load(filePath); + File.SetCreationTime(filePath, email.DateTimeReceived); + } + catch (Exception x) + { + HandleException("Failed to download the attachment", x); + } + } + + try + { + if (Environment.UserInteractive) + { + Console.WriteLine("Deleting the e-mail..."); + } + + email.Delete(DeleteMode.HardDelete); + } + catch (Exception x) + { + HandleException("Failed to delete the e-mail", x); + } + } + + private string CreateFilePath(DateTime dateTime, string fileName) + { + if (timestamp) + { + fileName = String.Format("{0}@{1}", (dateTime - new DateTime(1970, 1, 1).ToLocalTime()).TotalSeconds, fileName); + } + + return Path.Combine(savePath, fileName); + } + + private void HandleException(string prefix, Exception x) + { + if (Environment.UserInteractive) + { + Console.WriteLine(prefix + ": " + x); + } + else + { + this.eventLog.WriteEntry(prefix + ": " + x, EventLogEntryType.Warning); + } + } + } +} diff --git a/EwsMailDl/EwsMailDl.csproj b/EwsMailDl/EwsMailDl.csproj new file mode 100644 index 0000000..532c9df --- /dev/null +++ b/EwsMailDl/EwsMailDl.csproj @@ -0,0 +1,133 @@ + + + + Debug + x86 + 8.0.30703 + 2.0 + {EA83B646-A276-44EB-A735-67D61738531B} + Exe + Properties + EwsMailDl + EwsMailDl + v4.0 + + + 512 + false + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + true + + + x86 + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + x86 + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + true + bin\x64\Debug\ + DEBUG;TRACE + full + x64 + prompt + true + false + + + bin\x64\Release\ + TRACE + true + pdbonly + x64 + prompt + true + false + + + EwsMailDl.Program + + + + C:\Program Files\Microsoft\Exchange\Web Services\2.0\Microsoft.Exchange.WebServices.dll + + + + + + + + + + + + + + + Component + + + + Component + + + + + + False + Microsoft .NET Framework 4 %28x86 and x64%29 + true + + + False + .NET Framework 3.5 SP1 Client Profile + false + + + False + .NET Framework 3.5 SP1 + false + + + False + Windows Installer 3.1 + true + + + + + + + + \ No newline at end of file diff --git a/EwsMailDl/Program.cs b/EwsMailDl/Program.cs new file mode 100644 index 0000000..d89a82d --- /dev/null +++ b/EwsMailDl/Program.cs @@ -0,0 +1,486 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.ComponentModel; +using System.Configuration.Install; +using System.Diagnostics; +using System.Net; +using System.Net.Security; +using System.Reflection; +using System.Security.Cryptography.X509Certificates; +using System.ServiceProcess; +using System.Threading; +using Microsoft.Exchange.WebServices.Data; + +namespace EwsMailDl +{ + class Program : ServiceBase + { + private CancellationTokenSource tokenSource = null; + + private IList newEmailIdList = null; + + private BlockingCollection emailIdQueue = null; + + private EmailDownloader downloader = null; + + private Thread downloaderThread = null; + + private StreamingSubscriptionConnection subConn = null; + + private string[] programArgs = null; + + private Settings settings = null; + + private FolderId folderId = null; + + private bool downloadingOld = false; + + static int Main(string[] args) + { + if (Environment.UserInteractive) + { + if (args.Length > 0) + { + if (args[0] == "/i") + { + return InstallService(args); + } + + if (args[0] == "/u") + { + return UninstallService(); + } + } + + new Program().StartMonitor(args); + } + else + { + ServiceBase.Run(new Program(args)); + } + + return 0; + } + + private static int InstallService(string[] args) + { + var service = new Program(); + + try + { + var installerArgs = new string[args.Length]; + + for (var i = 1; i < args.Length; ++i) + { + installerArgs[i - 1] = args[i]; + } + + installerArgs[args.Length - 1] = Assembly.GetExecutingAssembly().Location; + + ManagedInstallerClass.InstallHelper(installerArgs); + } + catch (Exception x) + { + if (x.InnerException != null && x.InnerException.GetType() == typeof(Win32Exception)) + { + Win32Exception wx = (Win32Exception)x.InnerException; + + Console.WriteLine("Error 0x{0:X}: Service already installed!", wx.ErrorCode); + + return wx.ErrorCode; + } + + Console.WriteLine(x.ToString()); + + return -1; + } + + return 0; + } + + private static int UninstallService() + { + var service = new Program(); + + try + { + ManagedInstallerClass.InstallHelper(new string[] { "/u", Assembly.GetExecutingAssembly().Location }); + } + catch (Exception x) + { + if (x.InnerException.GetType() == typeof(Win32Exception)) + { + Win32Exception wx = (Win32Exception)x.InnerException; + + Console.WriteLine("Error 0x{0:X}: Service not installed!", wx.ErrorCode); + + return wx.ErrorCode; + } + else + { + Console.WriteLine(x.ToString()); + + return -1; + } + } + + return 0; + } + + public Program() + { + ServiceName = "EwsMailDl"; + EventLog.Log = "Application"; + CanHandlePowerEvent = false; + CanHandleSessionChangeEvent = false; + CanPauseAndContinue = false; + CanShutdown = false; + CanStop = true; + } + + public Program(string[] args) : this() + { + programArgs = args; + } + + protected override void OnStart(string[] serviceArgs) + { + new Thread(new ThreadStart(() => StartMonitor(serviceArgs))).Start(); + } + + private void StartMonitor(string[] serviceArgs) + { + try + { + settings = new Settings(); + + if (programArgs != null) + { + settings.ReadFromArgs(programArgs); + } + + if (serviceArgs.Length > 0) + { + settings.ReadFromArgs(serviceArgs); + } + + folderId = settings.CreateFolderId(); + } + catch (Exception x) + { + HandleException(x); + return; + } + + programArgs = null; + serviceArgs = null; + + if (Environment.UserInteractive) + { + Console.WriteLine("EwsMailDl"); + Console.WriteLine("--"); + Console.WriteLine(settings); + Console.WriteLine("--"); + } + + ServicePointManager.ServerCertificateValidationCallback = CertificateValidationCallback; + + newEmailIdList = new List(); + tokenSource = new CancellationTokenSource(); + emailIdQueue = new BlockingCollection(); + downloader = new EmailDownloader(EventLog, emailIdQueue, tokenSource, settings); + + StreamingSubscription sub = null; + + try + { + sub = settings.CreateExchangeService().SubscribeToStreamingNotifications(new FolderId[] { folderId }, EventType.NewMail); + } + catch (Exception x) + { + HandleException(x); + return; + } + + subConn = new StreamingSubscriptionConnection(sub.Service, settings.Lifetime); + + subConn.OnNotificationEvent += OnNotificationEvent; + subConn.OnSubscriptionError += OnSubscriptionError; + subConn.OnDisconnect += OnDisconnect; + + subConn.AddSubscription(sub); + + OpenSubscription(subConn); + + downloaderThread = new Thread(downloader.Run); + + try + { + DownloadAndDeleteOld(); + + downloaderThread.Start(); + + if (Environment.UserInteractive) + { + downloaderThread.Join(); + } + } + catch (Exception x) + { + HandleException(x); + } + } + + private void DownloadAndDeleteOld(int nextPageOffset = 0) + { + downloadingOld = true; + + if (tokenSource.IsCancellationRequested) + { + return; + } + + var exchangeService = settings.CreateExchangeService(); + var searchFilter = settings.CreateSearchFilter(); + var view = new ItemView(24, nextPageOffset) + { + PropertySet = PropertySet.IdOnly + }; + + view.OrderBy.Add(EmailMessageSchema.DateTimeReceived, SortDirection.Ascending); + + if (Environment.UserInteractive) + { + Console.WriteLine("Searching for {0} old e-mails at offset {1}...", view.PageSize, nextPageOffset); + } + + var results = exchangeService.FindItems(folderId, searchFilter, view); + + if (Environment.UserInteractive) + { + Console.Write("...found {0} old e-mails", results.Items.Count); + + if (results.MoreAvailable) + { + Console.WriteLine(" and more are available ({0} total)...", results.TotalCount); + } + else + { + Console.WriteLine("..."); + } + } + + foreach (var item in results.Items) + { + if (item is EmailMessage) + { + emailIdQueue.Add(item.Id); + } + } + + if (results.NextPageOffset.HasValue) + { + DownloadAndDeleteOld(results.NextPageOffset.Value); + } + else + { + if (newEmailIdList != null) + { + if (newEmailIdList.Count > 0) + { + foreach (var emailId in newEmailIdList) + { + emailIdQueue.Add(emailId); + } + + Console.WriteLine("Enqueued {0} new e-mails!", newEmailIdList.Count); + } + + newEmailIdList = null; + } + + downloadingOld = false; + } + } + + private void HandleException(Exception x) + { + if (Environment.UserInteractive) + { + Console.WriteLine(x); + + Environment.Exit(1); + } + else + { + throw x; + } + } + + protected override void OnStop() + { + if (emailIdQueue != null) + { + emailIdQueue.CompleteAdding(); + } + + if (tokenSource != null) + { + tokenSource.Cancel(false); + } + + if (subConn != null && subConn.IsOpen) + { + try + { + subConn.Close(); + } + catch (Exception) { } + } + + try + { + if (downloaderThread != null) + { + downloaderThread.Join(1337); + } + } + catch (Exception) { } + } + + private void OnNotificationEvent(object sender, NotificationEventArgs args) + { + if (emailIdQueue.IsAddingCompleted) + { + return; + } + + foreach (NotificationEvent notificationEvent in args.Events) + { + if (notificationEvent.EventType == EventType.NewMail && notificationEvent is ItemEvent) + { + var itemId = (notificationEvent as ItemEvent).ItemId; + + if (newEmailIdList == null) + { + emailIdQueue.Add(itemId); + } + else + { + newEmailIdList.Add(itemId); + } + } + } + } + + private void OnSubscriptionError(object sender, SubscriptionErrorEventArgs args) + { + if (args.Exception != null) + { + if (Environment.UserInteractive) + { + Console.WriteLine(args.Exception.Message); + } + else + { + throw args.Exception; + } + } + } + + private void OnDisconnect(object sender, SubscriptionErrorEventArgs args) + { + var message = "Subscription disconnected" + + (args.Exception == null ? " :(" : (": " + args.Exception.Message)); + + if (Environment.UserInteractive) + { + Console.WriteLine(message); + } + else + { + EventLog.WriteEntry(message, EventLogEntryType.Warning); + } + + OpenSubscription(sender as StreamingSubscriptionConnection); + + if (!downloadingOld) + { + DownloadAndDeleteOld(); + } + } + + private void OpenSubscription(StreamingSubscriptionConnection subConn) + { + if (tokenSource.IsCancellationRequested) + { + return; + } + + if (Environment.UserInteractive) + { + Console.WriteLine("Connecting the subscription..."); + } + + try + { + subConn.Open(); + } + catch (Exception x) + { + HandleException(x); + return; + } + + if (subConn.IsOpen) + { + var message = "Subscription connected :)"; + + if (Environment.UserInteractive) + { + Console.WriteLine(message); + } + else + { + EventLog.WriteEntry(message, EventLogEntryType.Information); + } + } + } + + private static bool CertificateValidationCallback( + object sender, + X509Certificate certificate, + X509Chain chain, + SslPolicyErrors sslPolicyErrors) + { + if (sslPolicyErrors == SslPolicyErrors.None) + { + return true; + } + + if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateChainErrors) == 0) + { + return false; + } + + if (chain != null && chain.ChainStatus != null) + { + foreach (X509ChainStatus status in chain.ChainStatus) + { + if ((certificate.Subject == certificate.Issuer) && (status.Status == X509ChainStatusFlags.UntrustedRoot)) + { + continue; + } + + if (status.Status != X509ChainStatusFlags.NoError) + { + return false; + } + } + } + + return true; + } + } +} diff --git a/EwsMailDl/Properties/AssemblyInfo.cs b/EwsMailDl/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..8902d7a --- /dev/null +++ b/EwsMailDl/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("EwsMailDl")] +[assembly: AssemblyDescription("Exchange E-mail Attachment Downloader")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Łukasz Walukiewicz")] +[assembly: AssemblyProduct("EwsMailDl")] +[assembly: AssemblyCopyright("Copyright © 2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("2d811a21-7db3-4c1c-a7cf-2118d020c834")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("0.0.0")] +[assembly: AssemblyFileVersion("0.0.0")] diff --git a/EwsMailDl/ServiceInstaller.cs b/EwsMailDl/ServiceInstaller.cs new file mode 100644 index 0000000..fb43ebc --- /dev/null +++ b/EwsMailDl/ServiceInstaller.cs @@ -0,0 +1,56 @@ +using System.ComponentModel; +using System.Configuration.Install; +using System.ServiceProcess; +using System.Text; +using Microsoft.Win32; + +namespace EwsMailDl +{ + [RunInstaller(true)] + public class ProgramInstaller : Installer + { + private ServiceProcessInstaller processInstaller; + private ServiceInstaller serviceInstaller; + + public ProgramInstaller() + { + processInstaller = new ServiceProcessInstaller(); + serviceInstaller = new ServiceInstaller(); + + processInstaller.Account = ServiceAccount.LocalSystem; + serviceInstaller.StartType = ServiceStartMode.Automatic; + serviceInstaller.ServiceName = "EwsMailDl"; + serviceInstaller.DisplayName = "EwsMailDl"; + serviceInstaller.Description = "Monitors and downloads matching e-mail attachments arriving to the specified Exchange account."; + + Installers.Add(serviceInstaller); + Installers.Add(processInstaller); + + processInstaller.AfterInstall += OnAfterInstall; + } + + protected void OnAfterInstall(object sender, InstallEventArgs args) + { + var cmd = new StringBuilder(); + + foreach (string key in Context.Parameters.Keys) + { + if (key == "logtoconsole" || key == "assemblypath" || key == "logfile") + { + continue; + } + + cmd.AppendFormat(" /{0}=\"{1}\"", key, Context.Parameters[key]); + } + + var keyName = "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\services\\" + serviceInstaller.ServiceName; + var valueName = "ImagePath"; + var value = Registry.GetValue(keyName, "ImagePath", null); + + if (value != null) + { + Registry.SetValue(keyName, valueName, (value as string) + cmd.ToString(), RegistryValueKind.ExpandString); + } + } + } +} diff --git a/EwsMailDl/Settings.cs b/EwsMailDl/Settings.cs new file mode 100644 index 0000000..eb97eb6 --- /dev/null +++ b/EwsMailDl/Settings.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Configuration.Install; +using System.Text; +using Microsoft.Exchange.WebServices.Data; + +namespace EwsMailDl +{ + class Settings + { + public ExchangeVersion Version { get; set; } + + public Uri Url { get; set; } + + public string Username { get; set; } + + public string Password { get; set; } + + public int Lifetime + { + get { return _lifetime; } + set { _lifetime = value < 1 ? 1 : value > 30 ? 30 : value; } + } + + public string SavePath { get; set; } + + public string FolderName { get; set; } + + public FolderId FolderId { get; set; } + + public IList SubjectFilters { get; set; } + + public bool Timestamp { get; set; } + + private int _lifetime = 30; + + public Settings() + { + Version = ExchangeVersion.Exchange2010_SP2; + Url = new Uri("https://localhost/EWS/Exchange.asmx"); + Username = "someone@localhost"; + Password = "T0PS3CR3T"; + SavePath = Environment.CurrentDirectory; + FolderName = "Inbox"; + FolderId = null; + SubjectFilters = new List(); + Timestamp = false; + } + + public void ReadFromArgs(string[] args) + { + var context = new InstallContext(null, args); + + foreach (DictionaryEntry param in context.Parameters) + { + string argName = param.Key as string; + string argValue = param.Value as string; + + if (argName == null || argValue == null) + { + continue; + } + + switch (argName) + { + case "version": + Version = (ExchangeVersion)Enum.Parse(typeof(ExchangeVersion), argValue, true); + break; + + case "url": + Url = new Uri(argValue); + break; + + case "username": + Username = argValue; + break; + + case "password": + Password = argValue; + break; + + case "lifetime": + Lifetime = Int32.Parse(argValue); + break; + + case "foldername": + FolderName = argValue; + break; + + case "folderid": + FolderId = new FolderId(argValue); + break; + + case "savepath": + SavePath = argValue; + break; + + case "subject": + SubjectFilters.Add(argValue); + break; + + case "timestamp": + Timestamp = true; + break; + } + } + } + + public override string ToString() + { + var sb = new StringBuilder(); + + sb.AppendFormat("Version : {0}", Version); + sb.AppendLine(); + sb.AppendFormat("URL : {0}", Url); + sb.AppendLine(); + sb.AppendFormat("Username : {0}", Username); + sb.AppendLine(); + sb.AppendFormat("Lifetime : {0}", Lifetime); + sb.AppendLine(); + sb.AppendFormat("Folder name: {0}", FolderName); + sb.AppendLine(); + sb.AppendFormat("Folder ID : {0}", FolderId); + sb.AppendLine(); + sb.AppendFormat("Save path : {0}", SavePath); + sb.AppendLine(); + sb.AppendFormat("Timestamp : {0}", Timestamp ? "Yes" : "No"); + sb.AppendLine(); + sb.AppendFormat("Subject : {0}", String.Join(" OR ", SubjectFilters)); + + return sb.ToString(); + } + + public FolderId CreateFolderId() + { + if (FolderId != null) + { + return FolderId; + } + + WellKnownFolderName folderName; + + if (Enum.TryParse(FolderName, true, out folderName)) + { + return new FolderId(folderName); + } + + var results = CreateExchangeService().FindFolders( + WellKnownFolderName.Root, + new SearchFilter.IsEqualTo(FolderSchema.DisplayName, FolderName), + new FolderView(1) { PropertySet = PropertySet.IdOnly, Traversal = FolderTraversal.Deep } + ); + + return results.Folders.Count > 0 ? results.Folders[0].Id : null; + } + + public SearchFilter CreateSearchFilter() + { + var hasAttachments = new SearchFilter.IsEqualTo(ItemSchema.HasAttachments, true); + + if (SubjectFilters.Count == 0) + { + return hasAttachments; + } + + var subjectFilters = new SearchFilter.SearchFilterCollection(LogicalOperator.Or); + + foreach (var subjectFilter in SubjectFilters) + { + subjectFilters.Add(new SearchFilter.ContainsSubstring(ItemSchema.Subject, subjectFilter, ContainmentMode.Substring, ComparisonMode.Exact)); + } + + return new SearchFilter.SearchFilterCollection(LogicalOperator.And, hasAttachments, subjectFilters); + } + + public ExchangeService CreateExchangeService() + { + return new ExchangeService(Version) + { + Credentials = new WebCredentials(Username, Password), + Url = Url + }; + } + } +} diff --git a/license.md b/license.md new file mode 100644 index 0000000..a830971 --- /dev/null +++ b/license.md @@ -0,0 +1,20 @@ +Copyright (c) 2014 Łukasz Walukiewicz + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..9e5cce5 --- /dev/null +++ b/readme.md @@ -0,0 +1,140 @@ +# EwsMailDl + +Microsoft Exchange E-mail Attachment Downloader. + +1. Connects to the specified Exchange server. +2. Subscribes to streaming notifications of type `NewMail` in the specified folder. +3. Downloads attachments from all existing e-mails (e-mails with attachments + and subjects matching the specified filters). +4. Downloads attachments from any new, matching e-mails. +5. If the subscription closes, repeat from step 3. +6. If any error occurs, crash (Windows Service Recovery should take care + of restarting the service). + +After downloading all attachments from the matched e-mail, the e-mail is deleted. + +Tested with Exchange server version 2010 SP2. + +## Requirements + +### .NET Framework + + * __Version__: 4.x + * __Website__: http://www.microsoft.com/net + * __Download__: http://www.microsoft.com/net/downloads + +### EWS Managed API + + * __Version__: 2.x + * __Website__: http://msdn.microsoft.com/en-us/library/dd633709(v=exchg.80).aspx + * __Download__: http://www.microsoft.com/en-us/download/details.aspx?id=35371 + +## Usage + +EwsMailDl can be run as a console application or a service application. + +### Console + +To run EwsMailDl as a console application, execute the following command: + +``` +EwsMailDl.exe +``` + +where `` is a list of the [configuration arguments](#configuration). + +This mode is intended for testing purposes only. + +### Service + +To install EwsMailDl as a service, execute the following command: + +``` +EwsMailDl.exe /i +``` + +where `` is a list of the [configuration arguments](#configuration). + +The serivce can be then started using the standard `net start` or `sc start` +commands. + +This mode is intended for use in production. The created `EwsMailDl` service +should be configured to restart on failure (*Recovery* tab in the service's +properties), because it will crash on any error. + +To uninstall the service, execute the following command: + +``` +EwsMailDl.exe /u +``` + +### Configuration + +Configuration arguments are specified in the following format: + +``` +/="" /="" ... +``` + +for example: + +``` +/quas="q" /wex="w" /exort="e" +``` + +Available configuration arguments are: + + * `version` - a version of the Exchange server we are connecting to. + Valid values are: `Exchange2010_SP1`, `Exchange2010_SP2` or `Exchange2013`. + Defaults to `Exchange2010_SP2`. + + * `url` - an URL to the server's EWS. For example, if the server we're trying + to connect to is `mail.example.com`, then the EWS URL should be: + `https://mail.example.com/EWS/Exchange.asmx`. + + * `username` - a username of the e-mail account we're trying to connect to. + + * `password` - a password of the e-mail account we're trying to connect to. + + * `lifetime` - a number of minutes (between 1 and 30) the subscription + notification is active on the server. Defaults to 30 minutes. + + * `folderName` - a name of the folder in the user's account we're going to + be monitoring for e-mails. Can be a `WellKnownFolderName` or any other + user-created folder. Defaults to `Inbox`. + + * `folderId` - an ID of the folder in the user's account we're going to be + monitoring for e-mails. Optional. If specified, the `folderName` is not used. + + * `savePath` - a path to a folder where the attachments should be downloaded to. + Defaults to the current directory. + + * `subject` - a filter for the e-mails to download. Only e-mails with a subject + containing the specified string will be taken into consideration (they must + have attachments too). + Can be specified multiple times. + Multiple filters are concatenated using `OR`. + + * `timestamp` - determines whether to prepend `@` to + the downloaded attachment file names, where `` is the e-mail's + date received as a UNIX timestamp. For example, attachment named `Test.html` + that arrived at 2014-01-02 12:00:00 GMT will be saved as `1388664000000@Test.html`. + +#### Example + +``` +EwsMailDl.exe ^ + /url="http://mail.example.com/EWS/Exchange.asmx" ^ + /username="code1\foobar" ^ + /password="top $$$ecret" ^ + /folderName="Baz" ^ + /savePath="C:/attachments" ^ + /subject="FOO" ^ + /subject="BAR" ^ + /timestamp="1" +``` + +## License + +This project is released under the +[MIT License](https://raw.github.com/morkai/EwsMailDl/master/license.md).