This blog post originally appeared on my website. You can check it out here(https://www.spenceralessi.com/Using-Powershell-and-Microsoft-EWS-Managed-API-to-download-attachments-in-Exchange-2016).
Have you ever used a piece of software that provides no way of saving attachments or reports directly to a file? Yeah, me too. It's frustrating. I'm very passionate about automating repetitive tasks and the frustration I've had over this particular issue has caused me to look into a solution. What I found was that I can use my beloved Powershell, in combination with Microsoft Exchange Web Services Managed API, to download attachments from my (or any other) outlook mailbox. So, to solve this little problem and add a bit of automation I have created a Powershell script, that runs from a scheduled task, that will do just that. This blog post describes the details of how this Powershell script works. I call it, not so cleverly, EWSEmailAttachmentSaver.
Although this blog post is more related to system administration than security in terms of who would be creating this type of script at a given organization, my opinion is that the same qualities and skills that make up a good sysadmin overlap with the qualities and skills that make up a good information security practitioner. Fortunately, I work for an organization that has a relatively small IT department and I am given the freedom and autonomy to work on projects like this. Also, i've worked my way up the ranks from Help Desk and I am used to creating these scripts and automated processes, because that's what I have been doing the last 8 years. So now onto explaining the script..
But first one quick note.
I think it's really import to make sure you provide good documentation with and/or within your scripts. As I grow and develop my own skills I am reminded regularly how important good documentation is. I will go into more details about this in another post. But know that, if you plan to use a script in a production environment, please do document well, use common language and built-in functions. It helps troubleshoot issues immensely in the future, especially if someone else takes over your scripts.
Upon deciding I wanted to create a script to automate some of my daily reports I found some very helpful blog posts. The two main blog posts this script was built from are:
Glen's examples and write ups were very helpful in understanding EWS and how to write some Powershell to work with the API. Thanks Glen! If you are interested in learning more about Exchange or Office365 and Powershell, be sure to check out his blog. Glen's Exchange and Office 365 Dev Blog
This script requires:
- Exchange 2007 or newer
- Exchange Web Services (EWS) Managed API 2.2
User Defined Variables
At the top of the script, under the comment section you will see a handful of user defined variables. If you use this script, most of your changes will occur here. This is all pretty standard stuff.
This script has several functions:
!(Test-Path $logpath) I first check to see if the path to the log file exists, if not I create it. If it does exist, I use the
Add-content cmdlet to send information i've specified to the log file.
This function loops through the processed folders path until target folder is found. In my script, i've conveniently make the processed folder directly underneath the root of my mailbox.
Fun little side-note, the root of your mailbox is called the Top Information Store and is sometimes displayed like this
Note, that my script assumes that the processed folder is a subfolder of the root of the users mailbox (e.g.
Fun little fact I found out was that, if you change the processed folder to be underneath any other folder, including the Inbox, the script requires slight modification.
If your processed folder is a subfolder under any other folder you must change
To quickly view the outlook folder location, right click on a folder in outlook, then click properties.
Example: processed folder is a subfolder of the root mailbox:
$processedfolderpath = "/ProcessedFolder" $tftargetidroot = New-Object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot,$mailbox)
Example, processed folder is a subfolder of Inbox:
$processedfolderpath = "/Inbox/ProcessedFolder" $tftargetidroot = New-Object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox,$processedfolderpath)
This is the main driver function that controls most of the scripts actions. Essentially this function loops through the emails that have been found using our filters, which I will explain in a minute, then it:
- Determines the download location based on the attachment name
- Saves the attachment to the download location, then closes it
- Marks the email(s) as read then moves them to the processed folder
As you can see there are some splitting of attachment names and some hackery to make sure I move the files to the correct monthly folder. This is just my own OCD, it's not really necessary. :)
Now that i've explained what the functions do, we can move on to explaining the Exchange EWS API. To learn more about it, see Download the Microsoft Exchange Web Services Managed API 2.2 from.
Download and Install the EWS Managed API
Once you download and install the Exchange EWS API components you need to load the appropriate EWS dll for the API namespace you want to use. By loading the
Microsoft.Exchange.WebServices.Data namespace we have access to a majority of the EWS classes and methods. Here is how you load the EWS dll.
$dllpath = "C:\Program Files\Microsoft\Exchange\Web Services\2.2\Microsoft.Exchange.WebServices.dll" [void][Reflection.Assembly]::LoadFile($dllpath)
Once you load the Webservices dll you can begin working with it. To read more about the EWS API see: Microsoft EWS Managed API Reference. Also note, there are multiple namespaces, for example, for things such as Autodiscover and Authentication, I suggest reviewing them if you want to learn more or mess around with other functionality.
Create an EWS Service Object
Now you need to create an EWS Service Object for the target mailbox. There are many ways you can authentication to the EWS API. For my script I chose to just use my organizations Autodiscover URL, which allows me to authenticate using the user who is running the script.
$exchangeservice.UseDefaultCredentials = $true $exchangeservice.AutodiscoverUrl($mailbox)
This also makes it convenient for me when I create a scheduled task out of this script. I can permission a service account accordingly without having to worry about hard coding credentials in my script. Hard coded creds should be avoided at all costs.
Bind to the Inbox
Now you need to simply Bind to the users Inbox. There are again a few ways to do this. I chose to use the
WellKnownFolderName defines common folder names that are used in a users mailbox.
$inboxfolderid = New-Object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox,$mailbox) $inboxfolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($exchangeservice,$inboxfolderid)
Configure Search Filter
One of the last things to do is to create some search filters for the emails we are targeting. This time we are going to use the
EmailMessageSchema class combined with the
HasAttachments fields. The subject I am targeting can be seen in the user defined variables section. I am looking for emails that contain the subject "Path Report." The other filters should be pretty self explanatory.
One cool thing to note is you can chain these filters together, throw an
And at the end and create some logic out of it.
Once you create those variables you add them all up into a collection and use that to find all the emails you're targeting.
$sfunread = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::IsRead, $false) $sfsubject = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+ContainsSubstring ([Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::Subject, $subjectfilter) $sfattachment = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::HasAttachments, $true) $sfcollection = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+SearchFilterCollection([Microsoft.Exchange.WebServices.Data.LogicalOperator]::And); $sfcollection.add($sfunread) $sfcollection.add($sfsubject) $sfcollection.add($sfattachment)
"View" the Results
I create a view filter so as to limit the query overhead. I chose to make this script view 10 items at a time. This was a tip I found from Using PowerShell and EWS to monitor a mailbox.
$view = New-Object -TypeName Microsoft.Exchange.WebServices.Data.ItemView -ArgumentList 10 $foundemails = $inboxfolder.FindItems($sfcollection,$view)
Then I just call
FindTargetEmail($subject) and you're done.
Now hit that command line, navigate to the folder where your script resides, and run it using
.\EWSEmailAttachmentSaver.ps1. Make sure you always test your code in a development or test environment BEFORE moving to production. Test, test, test!
If anyone gets value from this, I would love to know what specifically. And if you have any comments, questions or feedback about anything I wrote above, I would love to continue the dialog on Twitter. Hit me up @techspence