1
votes

I need to extract attachments from an Outlook 365 mailbox.

I'm following this example here (Powershell)

https://gallery.technet.microsoft.com/office/Export-Email-Messages-from-1419bbe9

I've got it running fine. This example extracts the entire message and saves to a .EML file.

To get attachments I can run some other script over the .EML file afterwards (I don't really want to do this) or I can use Powershell to extract the attachment directly

So I'm trying to use powershell but the doco on the API that is being used is sparser than what I'm used to

Basically the problem I have is that the sample code saves an email by calling this (reduced code)

# not sure if this is relevant. I think so
$itemPropertySet = New-Object Microsoft.Exchange.WebServices.Data.PropertySet(
[Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties,
[Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::MimeContent)
$emailMsg.Load($itemPropertySet)



# This actually saves it
$emailContent = $emailMsg.MimeContent.Content      
[System.IO.File]::WriteAllBytes("$Path\$fileName.eml",$emailContent)

But when I try the same type of code against an attachment, MimeContent.Content results in NULL

I think it's because I need to call Attachment.Load with the correct parameters to drag the attachment locally? But the documentation is not helping.

So anyway here's some reduced sanitised code.

This code prints out the attachment names but for each attachment I get these errors

Exception calling "Load" with "1" argument(s): "Empty path name is not legal.

Which is because I'm not calling Load correctly

Export-OSCEXOEmailAttachment : Exception calling "WriteAllBytes" with "2" argument(s): "Value cannot be null.

Which is because the variable that's meant to hold the attachment bytes is empty.

Also can anyone tell me - every time I make a code change do I need to Remove-Module and Import-Module to refresh? I suspect there is a way to run the script directly without importing it

Code is below

Function Export-OSCEXOEmailAttachment
{
    [cmdletbinding()]
    Param
    (
        #Define parameters
        [Parameter(Mandatory=$true,Position=1,ValueFromPipeline=$true)]
        [Microsoft.Exchange.WebServices.Data.SearchFolder]$SearchFolder,
        [Parameter(Mandatory=$true,Position=2)]
        [string]$Path,
        [Parameter(Mandatory=$false,Position=3)]
        [int]$PageSize=100,
        [Parameter(Mandatory=$false)]
        [switch]$AllowOverwrite,
        [Parameter(Mandatory=$false)]
        [switch]$KeepSearchFolder
    )
    Begin
    {
        #Verify the existence of exchange service object
        #This bit of code (removed) 
        #validates that variable $exService is initialised


        #Load necessary properties for email messages
        #Not certain what this is for. Does this indicate which particular properties are loaded?
        $itemPropertySet = New-Object Microsoft.Exchange.WebServices.Data.PropertySet(`
                           [Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties,
                           [Microsoft.Exchange.WebServices.Data.EmailMessageSchema]::MimeContent)

        #Load properties for attachments. Do we need to do this to get Mime.Content??
        $attachmentPropertySet = New-Object Microsoft.Exchange.WebServices.Data.PropertySet(`
                           [Microsoft.Exchange.WebServices.Data.BasePropertySet]::FirstClassProperties,
                           [Microsoft.Exchange.WebServices.Data.Attachment]::MimeContent)

    }
    Process
    {
        #Define the view settings in a folder search operation.
        $itemView = New-Object Microsoft.Exchange.WebServices.Data.ItemView($PageSize)

        #Iterate each item in the search folder
        do
        {
            $findResults = $SearchFolder.FindItems($itemView)

            foreach ($findResult in $findResults) {
                #Bind each email with a small set of PropertySet
                $emailMsg = [Microsoft.Exchange.WebServices.Data.EmailMessage]::Bind(`
                            $exService,$findResult.Id)
                $emailMsg.Load($itemPropertySet)

                # Addition to original function: now iterate through attachments
                foreach ($attachment in $emailMsg.Attachments) {
                    $ext = [System.IO.Path]::GetExtension($attachment.Name)

                    if($ext -eq ".xlsx") {
                     $attachment.Load($attachmentPropertySet)
                     $exportPath=$Path + "\" + $attachment.Name
                     Write-Host $exportPath

                     $attachmentContent = $attachment.MimeContent.Content

                     #Export attachment
                     Try
                     {
                        [System.IO.File]::WriteAllBytes($exportPath,$attachmentContent)
                     }
                     Catch
                     {
                        $PSCmdlet.WriteError($_)
                     }
                   }
                }


            }
        } while ($findResults.MoreAvailable)
    }
    End
}

Here's a sample of code saving attachments in C# using the API from here https://msdn.microsoft.com/EN-US/library/office/dn726695(v=exchg.150).aspx

public static void SaveEmailAttachment(ExchangeService service, ItemId itemId)
{
    // Bind to an existing message item and retrieve the attachments collection.
    // This method results in an GetItem call to EWS.
    EmailMessage message = EmailMessage.Bind(service, itemId, new PropertySet(ItemSchema.Attachments));

    foreach (Attachment attachment in message.Attachments)
    {
        if (attachment is ItemAttachment)
        {
            ItemAttachment itemAttachment = attachment as ItemAttachment;
            itemAttachment.Load(ItemSchema.MimeContent);
            string fileName = "C:\\Temp\\" + itemAttachment.Item.Subject + ".eml";

            // Write the bytes of the attachment into a file.
            File.WriteAllBytes(fileName, itemAttachment.Item.MimeContent.Content);

            Console.WriteLine("Email attachment name: "+ itemAttachment.Item.Subject + ".eml");
        }
    }
}

Whats with all these 'schemas'? the problem is there but I don't understand it

1

1 Answers

1
votes

The answer is here:

http://gsexdev.blogspot.com.au/2009/06/downloading-attachments-and-exporting.html

As far as I can tell it has nothing to do with schemas or property sets. I should've used

$attachment.Content

instead of

$attachment.MimeContent.Content

Which might've bee more obvious if Content appeared in any API documentation. Also why does the C# sample use MimeContent.Content? Maybe it's obvious to a seasoned Powershell/API programmer.

Here's an excerpt of the final code, based on the original code at https://gallery.technet.microsoft.com/office/Export-Email-Messages-from-1419bbe9

#Iterate each item in the search folder
do
{
    $findResults = $SearchFolder.FindItems($itemView)
    foreach ($findResult in $findResults) {
        #Bind each email with a small set of PropertySet
        $emailMsg = [Microsoft.Exchange.WebServices.Data.EmailMessage]::Bind($exService,$findResult.Id)
        $emailMsg.Load()

        # Iterate through attachments inside email
        foreach ($attachment in $emailMsg.Attachments) {
            $ext = [System.IO.Path]::GetExtension($attachment.Name)

            if($ext -eq ".xlsx") {
              $attachment.Load()
              $exportPath=$Path + "\" + $attachment.Name
              Write-Host $exportPath

              #Export attachment
              Try
              {
                 $file = New-Object System.IO.FileStream($exportPath,[System.IO.FileMode]::Create)
                 $file.Write($attachment.Content,0,$attachment.Content.Length)
                 $file.Close()
              }
              Catch
              {
                 $PSCmdlet.WriteError($_)
              }
           }
        }


    }

    # Once we've gone through this page of items, go to the next page
    $itemView.Offset += $PageSize
} while ($findResults.MoreAvailable)