Friday, January 15, 2010

Writing a simple scripted process to download attachmentts in Exchange 2007/ 2010 using the EWS Managed API

Every complicated thing in life is made up of smaller simpler building blocks, when it comes to writing a script (or any code really) the more of these little building blocks you have to figure out the more the process of solving a problem can become bewildering. The Internet generally provides you with lots of half eaten sandwiches of information something someone else has taken a bite out but a lot of the time half done, and as with any code its usefulness declines over time as new and better API's and methods are derived. In this post I'm going to go through a simple scripted process that hopefully covers a few more of these smaller building blocks that you might face when asked to come up with a simple costless solution to perform an automated business function with a script.

So the process im going to look at is one that comes up a lot and that is you have an Email that comes into to certain mailbox every day with a certain subject in my case "Daily Export" this email has an attachment that must be downloaded to a fileserver. After the message is downloaded the email should then be moved into a processed folder which is a subfolder of the inbox of the mailbox and marked as Read.

So first this script will be designed to be run once a day from a scheduled task. First off with this script is we need a few variables that need to be hardcoded with some settings.

$MailboxName = "user@mailbox.com"

This is the emailaddress of the mailbox the script is going to retrieve the message from

$Subject = "Daily Export"

This is the subject of the Email that has the attachment to process.

$ProcessedFolderPath = "/Inbox/Processed"

This is the Folder path to where the message is going to moved to this is needed for the fuction that will find the FolderID of this folder which is important when you go to move messages with EWS.

$downloadDirectory = "c:\temp\"

This is the folder where attachments will be downloaded to

The next part of this script is the FindTargetFolder function this is a function that takes a folderpath in the format of /FirstLevelFolder/SecondLevelFolder etc and then uses this to find the folderID of the folder when the email once processed will be moved to.

The next part of the script is some standard EWS Managed API Powershell stuff that I've talked about a lot before in this post

Then we come to the search filter which is a little interesting this search filter is doing a bitwise AND which means that all three conditions must be meet for it to return items.

$Sfir = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::IsRead, $false)
$Sfsub = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.ItemSchema]::Subject, $Subject)
$Sfha = 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)
;

So what this filter does is looks for Emails that are unread, have attachments and have a subject "Daily Export". If all these are true then these items will be returned into the finditems collection which brings us to the next part of the script.

$miMailItems.Load()
foreach($attach in $miMailItems.Attachments){
$attach.Load()
$fiFile = new-object System.IO.FileStream(($downloadDirectory + “\” + $attach.Name.ToString()), [System.IO.FileMode]::Create)
$fiFile.Write($attach.Content, 0, $attach.Content.Length)
$fiFile.Close()
write-host "Downloaded Attachment : " + (($downloadDirectory + “\” + $attach.Name.ToString()))
}
$miMailItems.isread = $true

This part of the script loads the messsages and then loops through the attachments and downloads them to the specified path.

$miMailItems.isread = $true
$miMailItems.Update([Microsoft.Exchange.WebServices.Data.ConflictResolutionMode]::AlwaysOverwrite)

These two lines set the message to read and then updates it.

[VOID]$miMailItems.Move($Global:findFolder.Id.UniqueId)

This last line moves the message to the processed folder.

That's it although generally not really but its as far as this half eaten sandwich is going to go but its pretty easy from here. I've put a download of this script here

32 comments:

David Claux said...

You can improve this script by loading the attachments' content directly to a stream rather than first buffering them to memory, via the attachment.Load(stream) overload.

Glen said...

Cool thanks David.

Cheers
Glen

Tim Berk said...

Things for posting this. So what if the daily email with attachment were sent to a Public Folder? How would one modify the script to access the attachment, save it to a file share and mark the email as read?

Thanks again.

~tb

Glen said...

Have a look at http://msgdev.mvps.org/exdevblog/pfdnlattach.zip

cheers
Glen

boycie said...

Hi Glen,

I was hoping you could assist me with this. I have been playing with your download script, all working accept the last command to move the email. I get the following error:

Cannot convert argument "0", with value: "AAMkADYxNDkyNTRmLWQyMGEtNDRmZS05ZWQyLTA5NTBmMTkxNjYzMQAuAAAAAADBW5dJ9iIySZ5hrXKVzkVmAQAG2OVxctjaSJTXinsYLTbWAEcpIAAQAAA=", for "Move" to type "Microsof
t.Exchange.WebServices.Data.WellKnownFolderName": "Cannot convert value "AAMkADYxNDkyNTRmLWQyMGEtNDRmZS05ZWQyLTA5NTBmMTkxNjYzMQAuAAAAAADBW5dJ9iIySZ5hrXKVzkVmAQAG2OVxctjaSJTXinsYLTbWAEcpIAAQAAA=
" to type "Microsoft.Exchange.WebServices.Data.WellKnownFolderName" due to invalid enumeration values. Specify one of the following enumeration values and try again. The possible enumeration va
lues are "Calendar, Contacts, DeletedItems, Drafts, Inbox, Journal, Notes, Outbox, SentItems, Tasks, MsgFolderRoot, PublicFoldersRoot, Root, JunkEmail, SearchFolders, VoiceMail"."
At C:\Users\gboyce\Documents\Scripts\Powershell\Mailbox DLAttachemnt\dnlattach2.ps1:63 char:25
+ [VOID]$miMailItems.Move <<<< ($Global:findFolder.Id.UniqueId)
+ CategoryInfo : NotSpecified: (:) [], MethodException
+ FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgument

I am new to powershell and dont know where to start with this error.

I hope you can help!

Geoff

Glen said...

Hi Geoff try replacing $Global:findFolder.Id.UniqueId with

$Global:findFolder.Id

I've had that problem before (sorry google comment spam detection removed you comment).

Cheers
Glen

Magnus said...

Hi,

I am trying this script out as it would be just great for one of my customers.
It works fine except for the last move line here also.

I receive this errormsg:
Exception calling "Move" with "1" argument(s): "Value cannot be null.
Parameter name: destinationFolderId"


+ [VOID]$miMailItems.Move <<<< ($Global:findFolder.Id.UniqueId)
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : DotNetMethodException

I have tried removing the .UniqueId but no luck
(running EWS 1.1 and Exchange 2010, fresh on PS..)


Hope you can help,
Thanks!

Glen said...

That means its failed to find the target folder check the path your using. eg it should be in the format

"/Inbox/Yourfolder"

Cheers
Glen

Magnus said...

Thanks for your response
Ok, one down one to go..
Turned out the inbox was called innboks (Norwegian).
Changed this and the script now runs successfully on the administrator mailbox.
But when running on a testmailbox (domain admins is given full mbx permissions to this, and the move folder is already created) the attachment is saved but the emails are not moved, they are deleted. No errormessages appear running the script. It beats me..;)

Magnus said...

update follows:
The email is not deleted, my bad.
It is moved from the testmailbox subfolder to the administrator mailbox subfolder,i.e. the user running the script.

Cheers
Magnus

Glen said...

Okay sorry there was a bug in the script I've fixed this and updated the download the lines that need to be added/changed where

$tfTargetidRoot = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot,$MailboxName)
$tfTargetFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$tfTargetidRoot)

Cheers
Glen

Magnus said...

Great, It's now working thanks!
Additional question, (if you don't mind!) would it be possible to use $SubjecttoSearch as a query to process all emails with similar subjects

Cheers
Magnus

BrittAdams said...

How can I make the $Subject ="" look for a variable or just part of a subject?

Glen said...

Change the Search filter from

Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.ItemSchema]::Subject, $Subject)

to

Microsoft.Exchange.WebServices.Data.SearchFilter+Contains([Microsoft.Exchange.WebServices.Data.ItemSchema]::Subject, $Subject)

This will means it will do a substring on the Subject see the contains searchfilter doco on MSDN for more information

Cheers
Glen

Adam Pierce said...

Hi Glen,

is it possible to apply the rule to any message received instead of looking for a specific subject?

Cheers

Adam

Glen said...

Yes just get modify the searchfilter by removing or remarking the line

$sfCollection.add($Sfsub)

Cheers
Glen

JB said...

I have the script running well for emails that have file attachments, but my customer would like it to handle attachments that are in emails that are embedded as attachments in emails. Can I recurse? It seems that when the attachment is another email message, it is treated differently than a normal file (eg. a text file, or an Excel file). Any suggestions?
Thanks!

Murilo Paes said...

Hi Glen
Can I search for a especific extension (like .xml or .doc)?
Thanks
Murilo

Glen Scales said...

Within Attachment processing code use something like
$attach.Load()
if($attach.Name.Contains(".xml") -bor $attach.Name.Contains(".doc")){
$fiFile = new-object System.IO.FileStream(($downloadDirectory + “\” + $attach.Name.ToString()), [System.IO.FileMode]::Create)
$fiFile.Write($attach.Content, 0, $attach.Content.Length)
$fiFile.Close()
write-host "Downloaded Attachment : " + (($downloadDirectory + “\” + $attach.Name.ToString()))
}
}

Cheers
Glen

Unknown said...

Is there any way to make this work with office 365?

Glen Scales said...

For Office365 try this http://msgdev.mvps.org/exdevblog/dnlattachOffice365.zip

Cheers
Glen

Nick Latocha said...

Is there any way of specifying a username and password rather than getting the user that runs the script?

Thanks
Nick

Nick Latocha said...
This comment has been removed by the author.
Selspiero said...

hi, i'd like to use this script, but cannot work out how to run it without it failing with an exception on calling 'bind' an underlying connection was closed?

thanks!

René Jensen said...

Hi Glen,

Thanks for this great script, it works very well on all sorts of attachments.

I do have one type of attachment I cant get it to work on, .eml attached files. When someone encapsules an email as an eml file in an email object I get this error:

Exception calling "Write" with "3" argument(s): "Buffer cannot be null.
Parameter name: array"
At C:\Script_Download_Attach\Read.ps1:36 char:16
+ $fiFile.Write <<<< ($attach.Content, 0, $attach.Content.Length)
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : DotNetMethodException

It does create a file with the name of the eml file attached in the email, its just with a size of 0

Does this make sense, what I am trying to do ?

Slingy said...

Hi Glen

Your script has really come in handy for me. Thankyou very much for making this available.
I was wondering though..
How would I edit this script to download attachments from emails with a multitude of different subject lines without running a separate script for each one?

Glen Scales said...

You'd probably be better to remove the Subject filter from the SearchFilter collection. This will then return all the Items for the specified period and filter the Subject at the client side with an if, regex etc

Cheers
Glen

Slingy said...

Thanks Glen. I think this might be beyond me...
I've got about a dozen subjects to download the attachments for. But there are plenty of other emails in the inbox with attachments that I don't want. I can't make any changes at the Exchange server end. Thinking I might just set up rules to file away the emails I need and then run this script for all emails in that Outlook folder.

Anonymous said...

Hi, I have installed EwsManagedApi.msi to C:\Program Files\Microsoft\Exchange\Web Services\2.1\.

I have used your code but I get the following error:-
Unable to find type [Microsoft.Exchange.WebServices.Data.WellKnownFolderName]: make sure that the assembly containing this type is loaded.
At C:\Users\Waynes\Desktop\Transfer_Mail.ps1:15 char:132
+ $tfTargetidRoot = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName] <<<< ::MsgFolderRoot,$MailboxName)
+ CategoryInfo : InvalidOperation: (Microsoft.Excha...KnownFolderName:String) [], RuntimeException
+ FullyQualifiedErrorId : TypeNotFound


Can you help please?

Glen Scales said...

Did you modify the path in
$dllpath = "C:\Program Files\Microsoft\Exchange\Web Services\1.0\Microsoft.Exchange.WebServices.dll"

to point to the version you installed

Wayne Shepherd said...

Hi Glen, I am very impressed with this and everything works fine until I change the +IsEqualTo to +Contains. When I do I get this error:-
New-Object : Cannot find type [Microsoft.Exchange.WebServices.Data.SearchFilter+Contains]: make sure the assembly containing this type is loaded.
At U:\eMAIL\2Contains_Subject.ps1:44 char:20
+ $Sfsub = new-object <<<< Microsoft.Exchange.WebServices.Data.SearchFilter+Contains([Microsoft.Exchange.WebServices.Data.ItemSchema]::Subject, $Subject)
+ CategoryInfo : InvalidType: (:) [New-Object], PSArgumentException
+ FullyQualifiedErrorId : TypeNotFound,Microsoft.PowerShell.Commands.NewObjectCommand

Unknown said...

Hi Glen,
Good day. How to pull the outlook web mail attachments directly to local drive by writing the script.