none
Download email content / text from Exchange EWS with Powershell. RRS feed

  • Question

  • I'm attempting to save email body / text from Exchange using Powershell. Would prefer to save as HTML, but to save as a txt file or .eml or .msg is ok too.

    I've adapted a script I used to do the same sort of thing with attachments instead, but it isn't working.

    Getting a "Cannot index into a null array" error at L51:C147 and "Value cannot be null" at L54:C18 and nothing is appearing in my specified folder. I have ensured that emails that meet my $Subjects criteria are in my inbox.

    $MailboxName = "mailbox@domain.com"
    
    $Subjects = @(
                  'Incident',
                  'Problem'
                )
    
    [regex]$SubjectRegex = ‘(?i)(‘ + (($Subjects |foreach {[regex]::escape($_)}) –join “|”) + ‘)’
    
    $downloadDirectory = "C:\temp"
    
     
    Function FindTargetFolder($FolderPath){
    	$tfTargetidRoot = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot,$MailboxName)
    	$tfTargetFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$tfTargetidRoot)
    	
    	for ($lint = 1; $lint -lt $pfArray.Length; $lint++) {
    		$pfArray[$lint]
    		$fvFolderView = new-object Microsoft.Exchange.WebServices.Data.FolderView(1)
    		$SfSearchFilter = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+isEqualTo([Microsoft.Exchange.WebServices.Data.FolderSchema]::DisplayName,$pfArray[$lint])
                    $findFolderResults = $service.FindFolders($tfTargetFolder.Id,$SfSearchFilter,$fvFolderView)
    		if ($findFolderResults.TotalCount -gt 0){
    			foreach($folder in $findFolderResults.Folders){
    				$tfTargetFolder = $folder				
    			}
    		}
    		else{
    			"Error Folder Not Found"
    			$tfTargetFolder = $null
    			break
    		}	
    	}
    	$Global:findFolder = $tfTargetFolder
    }
     
    $dllpath = "C:\Program Files\Microsoft\Exchange\Web Services\2.0\Microsoft.Exchange.WebServices.dll"
    [void][Reflection.Assembly]::LoadFile($dllpath)
    $service = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2007_SP1)
     
     
    $windowsIdentity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
    $sidbind = "LDAP://<SID=" + $windowsIdentity.user.Value.ToString() + ">"
    $aceuser = [ADSI]$sidbind
     
    $uri=[system.URI] "https://webmail.domain.com.au/EWS/Exchange.asmx"
    $service.Url = $uri
    
    FindTargetFolder($ProcessedFolderPath)
     
    $folderid = new-object  Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox,$MailboxName)
    $InboxFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($service,$folderid)
    $Sfsub = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.ItemSchema]::Subject,$Subject[0])
    $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);
    $sfCollection.add($Sfsub)
    $sfCollection.add($Sfha)
    $view = new-object Microsoft.Exchange.WebServices.Data.ItemView(2000)
    $frFolderResult = $InboxFolder.FindItems($sfCollection,$view)
      
      foreach($attach in $miMailItems.Attachments){
    
        foreach ($miMailItems in $frFolderResult.Items){
      $miMailItems.body.text | set-content $downloadDirectory
     }
    

    Tuesday, March 11, 2014 10:57 PM

Answers

  • First you have to load the mail item.  What you have is only the directory entry and not the contents.

    $mailitem.Load()
    $mailitem.Body.Text


    ¯\_(ツ)_/¯

    • Marked as answer by Slingy Monday, March 17, 2014 5:12 AM
    Monday, March 17, 2014 5:08 AM

All replies

  • What line is giving you the error?


    ¯\_(ツ)_/¯

    Tuesday, March 11, 2014 11:54 PM
  • First one is:

    $Sfsub = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.ItemSchema]::Subject,$Subject[0])

    Second is:

    $sfCollection.add($Sfsub)

    Tuesday, March 11, 2014 11:57 PM
  • First the code does not make any sense. There are numerous undefined and ambiguous variables and statements.

    The first line you note has an array "$Subject" - there is no such variable.

    Of course if that line fails the next one will too.


    ¯\_(ツ)_/¯

    Wednesday, March 12, 2014 12:30 AM
  • yeah, I've tried to alter something I had for another purpose, so it's pretty messy. 
    I have now changed $Subject to $Subjects, so I'm not getting the errors now. The script runs, but doesn't really do anything. 
    Wednesday, March 12, 2014 12:34 AM
  • Start by opening a new copy of PowerShell CLI.

    Paste these two lines:

    [Reflection.Assembly]::LoadFile("C:\Program Files\Microsoft\Exchange\Web Services\2.0\Microsoft.Exchange.WebServices.dll")
    $Sfsub=new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.ItemSchema]::Subject,'zippy')
    $sfsub

    Note that it works.  We are only creating a property object to use for finding a subject.  We don't even have to be connected to Exchange.

    Most of what I see tells me that you do not know any PowerShell and are just pasting things you have found together,  Try spending time studying each line until you understand what it does.


    ¯\_(ツ)_/¯

    Wednesday, March 12, 2014 12:39 AM
  • Yep, that finds the subject.

    You are correct that I don't know any Powershell. I'm pretty handy with VBA and VBS, but haven't needed to use Powershell until getting into this email stuff. From my research it seems to be the best way to get things out of Exchange without running Outlook.

    Wednesday, March 12, 2014 12:44 AM
  • Take a little time to review PowerShell. You will find that it is easier than VBA and VBS and more flexible.


    ¯\_(ツ)_/¯

    Wednesday, March 12, 2014 1:08 AM
  • Hi Slingy,

    first of all: Welcome to using EWS in Powershell (oh, and getting started with Powershell too) :)

    As jrv said, you'll really want to learn the basics of the tools you are using. For Powershell you are at the right place (check out the learning section and seek the powershell subsection. In The Gallery you can find many scripts, that not only do things for you, but can be used for looking up how to do a specific task. And if you have a specific question on a scripting problem, that's where we come in.

    For EWS, there are three good places to get started:

    Technet Forum: Exchange Development

    And Glen's Exchange Dev Blog

    EWS Managed API Documentation

    Glen is the premier Dweller of the Exchange Dev Technet Forums and he has a particularly great blog series on HowTos for "Powershell and EWS".

    On another note of your script:

    $view = new-object Microsoft.Exchange.WebServices.Data.ItemView(2000)

    You use this line to create a view, which is part of a search routine. The numbered parameter (2000) is the paging size - the maximum number of items you want to retrieve in a single operation. If you try to retrieve more items at once than the Exchange is configured to allow, the operation will throw an error. The default number configured on an Exchange is usually 1000.

    You may want to try ...

    $view | Get-Member

    to check, what properties it has. Interesting here is "PropertySet" and "Traversal".

    If you have to count on more items than you can retrieve in one Operation, "Offset" will be essential.

    Cheers and good learning,
    Fred


    There's no place like 127.0.0.1

    Wednesday, March 12, 2014 7:59 AM
  • Fred - very good resources.  Thank you.

    Here is the local link for Powershell: http://technet.microsoft.com/en-us/scriptcenter/dd742419.aspx

    The videos are a very quick bootstrap to going from VBS to PowerShell.


    ¯\_(ツ)_/¯

    Wednesday, March 12, 2014 11:39 AM
  • Thanks guys. I've now started learning more about Powershell from scratch and rewriting the script myself, making sure I know what every step is doing.

    So I've worked out that most of the lines are defining variables before kicking off the loop. 

    The actual working part of the script could be done in a small handful of lines if it wasn't wasn't for using the variables to make thing a bit easier to follow.

    What I've got so far (that I think I need to include) is:

    [Reflection.Assembly]::LoadFile("C:\Program Files\Microsoft\Exchange\Web Services\2.0\Microsoft.Exchange.WebServices.dll")
    $MailboxName = "email@domain.com"
    $Subjects = @(
                  'Incident',
                  'Problem'
                )
    [regex]$SubjectRegex = ‘(?i)(‘ + (($Subjects |foreach {[regex]::escape($_)}) –join “|”) + ‘)’
    $downloadDirectory = "C:\temp"
    
    $exchService = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2007_SP1) 
    $exchService.UseDefaultCredentials = $true
    $exchService.AutodiscoverUrl($MailboxName)
    
    $folderid = new-object  Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox,$MailboxName)
    $InboxFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($exchService,$folderid)
    
    $Sfsub = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.ItemSchema]::Subject,$Subjects[0])
    $sfCollection = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+SearchFilterCollection([Microsoft.Exchange.WebServices.Data.LogicalOperator]::And);
    $sfCollection.add($Sfsub)
    
    
    $view = new-object Microsoft.Exchange.WebServices.Data.ItemView(2000)
    $frFolderResult = $InboxFolder.FindItems($sfCollection,$view)

    Wednesday, March 12, 2014 10:14 PM
  • Oops, looks like I replied to my original post rather than the last post...

    Anyway, I've now got a mailbox, credentials, the exchange server URL and subjects defined.

    I think I'm ready to try to create the "foreach" loop to grab the emails I want. So I'm trying to get the subject and body of each email with the defined subject to be saved into an individual file in the c:\ temp folder.

    Having a go this morning, but might need some assistance at some stage.

    Wednesday, March 12, 2014 10:20 PM
  • Hi Slingy,

    good luck with your test run. One thing you will likely notice, is that the return objects do not have all the properties you want. For example, the body will be ... empty.

    That's because EWS only returns what you ask of it and the properties you ask for are stored in the PropertySet-property of your view. Since you didn't specify which you want, well ...
    You get by default the FirstClassProperties.

    You can add additional Properties by using $view.PropertySet.Add([Property]).

    For a full list of properties, check the ItemSchema-Class and the EmailMessage-Schema for reference. That's not an exhaustive list, however it'll probably cover all your needs.

    On another note for efficiency's sake: Many of the standard item operations (like delete, change or request additional data) are available as functions on the ExchangeService object. Those allow for Bulk-Operations, which are a lot faster than doing individual operations on the items.

    Cheers,
    Fred


    There's no place like 127.0.0.1

    Thursday, March 13, 2014 7:55 AM
  • Ok, I reckon I'm legitimately stuck now..

    Here's what I've got..

    [Reflection.Assembly]::LoadFile("C:\Program Files\Microsoft\Exchange\Web Services\2.0\Microsoft.Exchange.WebServices.dll")
    $MailboxName = "email@domain.com"
    $Subjects = @(
                  'Incident',
                  'Problem'
                )
    [regex]$SubjectRegex = ‘(?i)(‘ + (($Subjects |foreach {[regex]::escape($_)}) –join “|”) + ‘)’
    $downloadDirectory = "C:\temp"
    
    $exchService = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2007_SP1) 
    $exchService.UseDefaultCredentials = $true
    $exchService.AutodiscoverUrl($MailboxName)
    
    $folderid = new-object  Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox,$MailboxName)
    $InboxFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($exchService,$folderid)
    
    $Sfsub = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo([Microsoft.Exchange.WebServices.Data.ItemSchema]::Subject,$Subjects[0])
    $sfCollection = new-object Microsoft.Exchange.WebServices.Data.SearchFilter+SearchFilterCollection([Microsoft.Exchange.WebServices.Data.LogicalOperator]::And);
    $sfCollection.add($Sfsub)
    
    
    $view = new-object Microsoft.Exchange.WebServices.Data.ItemView(2000)
    $frFolderResult = $InboxFolder.FindItems($sfCollection,$view)
      
        foreach ($MailItem in $frFolderResult.Items){
            $miMailItems.body.text | set-content $downloadDirectory
     }

    Now it seems to run, I don't get any errors but all that is returned is:
    GAC    Version        Location                                                                                                                                                                                                    
    ---    -------        --------                                                                                                                                                                                                    
    False  v2.0.50727     C:\Program Files\Microsoft\Exchange\Web Services\2.0\Microsoft.Exchange.WebServices.dll

    Which is from the first line.

    So I'm trying to save the email body to the download directory, but I just don't know how to do it.. Assuming it somehow needs me to specify a file type and name? 

    Sunday, March 16, 2014 11:14 PM
  • What is this supposed to be doing?  It doesn't look like it does anything constructive.

    [regex]$SubjectRegex = ‘(?i)(‘ + (($Subjects |foreach {[regex]::escape($_)}) –join “|”) + ‘)’


    ¯\_(ツ)_/¯

    Monday, March 17, 2014 1:04 AM
  • That line is to open up the email subjects being searched for to have wildcards.

    At least I hope that's what it is doing. I'm unsure how to test it.

    Monday, March 17, 2014 1:07 AM
  • That line is to open up the email subjects being searched for to have wildcards.

    At least I hope that's what it is doing. I'm unsure how to test it.

    That line does nothing realistic.

    Start by getting your code to work with a simple single subject. Once you can actually retrieve mail then add the complex filter back.

    You are trying to do too many things that you don't understand all at the same time.

    I can use that code simplified to a single subject and it works fine.


    ¯\_(ツ)_/¯

    Monday, March 17, 2014 1:39 AM
  • What I cannot understand is why you are using two conflicted methods of adding multiple subject searches.  You are adding in a regex and a logical search.  Use one or the other.

    If you want to match a subject then just ask for it.

    $subjecttomatch='[incident|problem]'

    That is all it takes to find all subjects with either incident or problem in the string.


    ¯\_(ツ)_/¯

    Monday, March 17, 2014 1:56 AM
  • ok, now at least it looks like it's trying to do the right thing, but getting an error now.

    Set-Content : Access to the path 'C:\temp' is denied.
    At D:\Users\me\Desktop\ML4.ps1:31 char:45
    +         $miMailItems.body.text | set-content <<<<  $downloadDirectory
        + CategoryInfo          : NotSpecified: (:) [Set-Content], UnauthorizedAccessException
        + FullyQualifiedErrorId : System.UnauthorizedAccessException,Microsoft.PowerShell.Commands.SetContentCommand

    So I'm doing a bit of research to find out why.. 

    Monday, March 17, 2014 2:05 AM
  • you are trying to set the content of a folder.  You cannot do that.

    ¯\_(ツ)_/¯

    Monday, March 17, 2014 2:54 AM
  • So I'm thinking I need to create a new item, set the name as the subject, set the content as the mail item body and the path as the specified path...
    Working on  it now :-)
    Monday, March 17, 2014 3:18 AM
  • I am creating the item with:

    new-item -Path $downloadDirectory -name $SaveFileName -type file -value $MailItem.Body.Text

    It creates a blank text file but the name and location are correct now. 

    I would have thought -value $MailItem.Body.text would be the way to set the content of the new file as that text?

    Monday, March 17, 2014 3:48 AM
  • First you have to load the mail item.  What you have is only the directory entry and not the contents.

    $mailitem.Load()
    $mailitem.Body.Text


    ¯\_(ツ)_/¯

    • Marked as answer by Slingy Monday, March 17, 2014 5:12 AM
    Monday, March 17, 2014 5:08 AM
  • Just got some issues now getting the "ForEach" loop to work..

    Have tried defining an array of subjects i.e

    $Subject = @("incident","problem")

    As well as the method you mentioned earlier:

    $subjecttomatch='[incident|problem]'

    then (assuming this is where it's supposed to go)

    $frFolderResult = $InboxFolder.FindItems($subjecttomatch,$view)

    But the error states that it's only valid for exchange2010 or later..

    Tuesday, March 18, 2014 10:51 PM
  • You need to spend some time reading the documentation for EWS.  EWS does not support wildcards or regular expressions. 

    The code you copied wasa taken from an example on how to use match in a loop.  THe version you copied was changed by someone who did not know what they were doing.  It won't work.  THe code you originally pastd was a combination of two examples which were both at least partly wrong.

    if($mailiten.subject -match '[incident|problem]'){
           # process item
    }

    This will detect either word in the subject line.


    ¯\_(ツ)_/¯


    • Edited by jrv Wednesday, March 19, 2014 12:47 AM
    Wednesday, March 19, 2014 12:46 AM
  • Hmm.. It just doesn't seem to like having more than one subject..

    [Reflection.Assembly]::LoadFile("C:\Program Files\Microsoft\Exchange\Web Services\2.0\Microsoft.Exchange.WebServices.dll")
    
    # Name of the mailbox
    $MailboxName = "mailbox@domain.com"
    
    # Subjects to search for
    $Subjects = '[Problem|Incident]'
                
    # Directory to download to            
    $downloadDirectory = "D:\temp"
    
    # Load the Exchange service
    $exchService = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2007_SP1) 
    
    # Exchange service to use default credentials and find URL for OWA Mailbox
    $exchService.UseDefaultCredentials = $true
    $exchService.AutodiscoverUrl($MailboxName)
    
    # Specify which folder in the mailbox to use
    $folderid = new-object  Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox,$MailboxName)
    $InboxFolder = [Microsoft.Exchange.WebServices.Data.Folder]::Bind($exchService,$folderid)
    
    $MailItems = $InboxFolder.Items
    
    # Check mailbox 
    
    ForEach($Mailitem in $Mailitems){
            
            if($MailItem.subject -match $Subjects){
                    
                    $MailItem.load()
                    
                    $MailBody = $MailItems.Body.Text
                    $SaveFileName = ((Get-Date -Format "yyMMdd") + "_" + $MailItem.Subject + ".html")
                    new-item -Path $downloadDirectory -name $SaveFileName -type file -value $MailBody
                    }}

    I'm just not getting it..

    I've tried this with and without the "ForEach" And it doesn't even seem to work for one subject like this..

    Wednesday, March 19, 2014 11:36 PM
  • Works fine for me.  You need to back up and try to understand what you are doing and why.

    Here is a slightly easier way to do this.

    [Reflection.Assembly]::LoadFile("C:\Program Files\Microsoft\Exchange\Web Services\2.0\Microsoft.Exchange.WebServices.dll")
    
    $MailboxName='mailbox@domain.com'
    $Subjects='[Problem|Incident]'
    $downloadDirectory = "D:\temp"
    
    # Load the Exchange service
    $exchService = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService([Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2007_SP1) 
    # Exchange service to use default credentials and find URL for OWA Mailbox
    $exchService.UseDefaultCredentials = $true
    $exchService.AutodiscoverUrl($MailboxName,{$true})
    
    # Check mailbox 
    $view=New-Object Microsoft.Exchange.WebServices.Data.ItemView(2000)
    $Mailitems=$exchService.FindItems('Inbox',$view)
    $Subjects='test123|test456'
    ForEach($Mailitem in $Mailitems){
        if($MailItem.subject -match $subjects){             
         Write-Host $Mailitem.Subject -fore green
           $MailItem.load()                
            $MailBody = $MailItems.Body.Text
            $SaveFileName='{0}_{1}.html' -f [DateTime]::Today.ToString('yyMMdd'),$MailItem.Subject
        }
    }


    ¯\_(ツ)_/¯

    Thursday, March 20, 2014 12:11 AM
  • Ah, so near and yet so far!

    This basically worked. Just had to adapt it to use a mailbox that wasn't mine and the AutoDiscoverURL didn't work, so I had to set that too.

    Pretty happy now, and I've learnt a lot. Thanks :-)


    • Edited by Slingy Thursday, March 20, 2014 3:27 AM
    Thursday, March 20, 2014 3:27 AM
  • Of course you realize that you will get the inbox of the authenticated user and not the alternate user.  Under default settings you cannot read another users mail even if you are an Admin.


    ¯\_(ツ)_/¯

    Thursday, March 20, 2014 4:04 AM
  • Of course you realize that you will get the inbox of the authenticated user and not the alternate user.  Under default settings you cannot read another users mail even if you are an Admin.


    ¯\_(ツ)_/¯

    Ah yes :-)

    It's another mailbox I have access to. Just had to specify that mailbox and the relevant inbox so that it didn't use my Inbox.

    Thursday, March 20, 2014 4:06 AM