Downloading Attachment from Gmail - Humdinger!

Hi All,

I have an email that comes in daily to a gmail hosted email. Normally I have no problem downloading through a script, however, this time the filename coming as an attachment has colons (:slight_smile: in it. The subject of the email is always the same, so its easy to identify the email and find the attachment, but given my current code, it says the path is not supported and I am at a loss. My current code is:

# !=========== DownloadReports (Date; dd-MMM-yyyy) ==============!
# This function downloads the reports that are attachments on emails sent from the system
Function DownloadReports-All ($date) {
	# Load IMAPX module
	$dll = "$dir\ImapX.dll"
	[Reflection.Assembly]::LoadFile($dll)

	# Setup Credentials
	$pwdgmail = ConvertTo-SecureString "gmailpassword" -AsPlainText -Force
	$bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($pwdgmail)
	$Username = "emailaddress"
	$Password = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr)


	# Initialize the IMAP client
	$client = New-Object ImapX.ImapClient

	# Set Fetching Mode to retrieve the part of message needed (less is better)
	$client.Behavior.MessageFetchMode = "Full"
	$client.Host = "imap.gmail.com"
	$client.Port = 993
	$client.UseSsl = $true
	$client.Connect()
	$client.Login($Username,$Password)

	# Get folder/label object containing emails you want to read from
	$res = $client.folders.subfolders | where { $_.path -eq '[Gmail]/All Mail'}

	# Create search query
	$searchSinceDate = $date
	$searchQuery = 'SUBJECT "Subject I am searching for" SINCE ' + $searchSinceDate + ''
	Write-Host "Searching Emails for: $searchQuery"


	# Search email threads inside the subfolder
	$numberOfMessagesLimit = 200
	$messages = $res.search($searchQuery, $client.Behavior.MessageFetchMode, $numberOfMessagesLimit)

	# Create Attachment Folder
	New-Item $dir\Attachments -ItemType Directory -Force

	# Set Folder Path for Attachments
	$folderPath = Resolve-Path -Path "$dir\Attachments\"


	# for each message, download attachment if it exists
	# download into its own folder
	$counter = 0
$messageCount = 0

foreach ($message in $messages) {
	    Write-Host "Processing the next message: $message"
	    foreach ($i in $message.Attachments) {
		Write-Host "Processing the next attachment: $i"
		# Reset SubFolder location
		$strCounter = [string]$counter
		$subfolderPath = $dir + "\Attachments\Attachment_" + $strCounter

		# Reset SubFolder location
		$strCounter = [string]$counter
		$subfolderPath = $dir + "\Attachments\Attachment_" + $strCounter

		# Create new sub-folder
		New-Item $subfolderPath -ItemType Directory -Force
		$subfolderPath = Resolve-Path -Path $subfolderPath	

		# Fetch emails
		$i.Download();
        foreach($i in $messages.Attachments) {
    if($i -ne $null) {
        $i.Download()
$filename = $attachment.FileName
$illegalChars = [System.IO.Path]::GetInvalidFileNameChars()
foreach ($char in $illegalChars) {
   
if ($filename -ne $null) {
    $filename = $filename.Replace($char, '_')
}

		# Download attachments to location
		$i.Save($subfolderPath)

		# Uptick counter
		$counter++
		Write-Host "Finished with this attachment: $i"
		Write-Host "Counter is now $counter"
	    }
	    Write-Host "Finished with this message: $message"
	}


	# Display the messages in a formatted table
	$messages | ft *

	$client.Disconnect()
}
}}}






# ========== Initialization ============
$dir = "$PSScriptRoot" 
$inputFileName = "sampleCSV.csv"
$outputFileName = "SampleHTML_built-in2.html" 
$title = "Sample Output HTML from CSV"

# Get current date
#$currentDate = Get-Date -Format "dd-MMM-yyyy"
$currentDate = (Get-Date).AddDays(0)
$currentDate = $currentDate.ToString("dd-MMM-yyyy")


# - Path initialization
$inputFilePath = "$dir\$inputFileName"
$outputFilePath = "$dir\$outputFileName"

# ============ Main Program ===============

# ----- Clean Up Last Day's Run-----
Remove-Item -Recurse -Force "$dir\Attachments\"

# ----- Retrieve Attachments for current day ------
DownloadReports-All($currentDate)

# ----- Get List of Files to Process (Should only be one) -----
$files = Get-ChildItem $dir\Attachments -Filter *.* -Recurse


The error I am getting is:

Counter is now 35
Exception calling “Save” with “1” argument(s): “The given path’s format is not supported.”
At D:\autoloader\Plains_TT\PlainsTTEmailRetrieval.ps1:81 char:3

  •     $i.Save($subfolderPath)
    
  •     ~~~~~~~~~~~~~~~~~~~~~~~
    
    • CategoryInfo : NotSpecified: (:slight_smile: , MethodInvocationException
    • FullyQualifiedErrorId : NotSupportedException

Thank you in advance for any help you are able to provide! I do not have PowerShellGmail module, Gmail API or any other third party process.

Melissa

If you check the array returned by GetInvalidFileNameChars() you’ll see it doesn’t contain the : character - at least it doesn’t on my system. Checking the docs:

The array returned from this method is not guaranteed to contain the complete set of characters that are invalid in file and directory names.

Add another replace() or -replace to replace the : with a replacement character.

2 Likes

Makes sense! I did do a replacement with -replace however I still get the same error listed below the following amended script.

# !=========== DownloadReports (Date; dd-MMM-yyyy) ==============!
# This function downloads the reports that are attachments on emails sent from the system
Function DownloadReports-All ($date) {
	# Load IMAPX module
	$dll = "$dir\ImapX.dll"
	[Reflection.Assembly]::LoadFile($dll)

	# Setup Credentials
	$pwdgmail = ConvertTo-SecureString "automation7878" -AsPlainText -Force
	$bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($pwdgmail)
	$Username = "dsautomation@datascavenger.com"
	$Password = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr)


	# Initialize the IMAP client
	$client = New-Object ImapX.ImapClient

	# Set Fetching Mode to retrieve the part of message needed (less is better)
	$client.Behavior.MessageFetchMode = "Full"
	$client.Host = "imap.gmail.com"
	$client.Port = 993
	$client.UseSsl = $true
	$client.Connect()
	$client.Login($Username,$Password)

	# Get folder/label object containing emails you want to read from
	$res = $client.folders.subfolders | where { $_.path -eq '[Gmail]/All Mail'}

	# Create search query
	$searchSinceDate = $date
	$searchQuery = 'SUBJECT "#44 Plains Midstream Customer Report - 100/04-13-039-09W5 Orlen" SINCE ' + $searchSinceDate + ''
	Write-Host "Searching Emails for: $searchQuery"


	# Search email threads inside the subfolder
	$numberOfMessagesLimit = 200
	$messages = $res.search($searchQuery, $client.Behavior.MessageFetchMode, $numberOfMessagesLimit)

	# Create Attachment Folder
	New-Item $dir\Attachments -ItemType Directory -Force

	# Set Folder Path for Attachments
	$folderPath = Resolve-Path -Path "$dir\Attachments\"


	# for each message, download attachment if it exists
	# download into its own folder
	$counter = 0
$messageCount = 0

foreach ($message in $messages) {
	    Write-Host "Processing the next message: $message"
	    foreach ($i in $message.Attachments) {
		Write-Host "Processing the next attachment: $i"
		# Reset SubFolder location
		$strCounter = [string]$counter
		$subfolderPath = $dir + "\Attachments\Attachment_" + $strCounter

		# Reset SubFolder location
		$strCounter = [string]$counter
		$subfolderPath = $dir + "\Attachments\Attachment_" + $strCounter

		# Create new sub-folder
		New-Item $subfolderPath -ItemType Directory -Force
		$subfolderPath = Resolve-Path -Path $subfolderPath	

		# Fetch emails
		$i.Download();
        foreach($i in $messages.Attachments) {
    if($i -ne $null) {
        $i.Download()
$filename = $attachment.FileName
$filename.Replace(':','')

		# Download attachments to location
		$i.Save($subfolderPath)

		# Uptick counter
		$counter++
		Write-Host "Finished with this attachment: $i"
		Write-Host "Counter is now $counter"
	    }
	    Write-Host "Finished with this message: $message"
	}


	# Display the messages in a formatted table
	$messages | ft *

	$client.Disconnect()
}
}}

Exception calling “Save” with “1” argument(s): “The given path’s format is not supported.”
At D:\autoloader\Plains_TT\PlainsTTEmailRetrieval.ps1:76 char:3

  •     $i.Save($subfolderPath)
    
  •     ~~~~~~~~~~~~~~~~~~~~~~~
    
    • CategoryInfo : NotSpecified: (:slight_smile: , MethodInvocationException
    • FullyQualifiedErrorId : NotSupportedException

Can you stick a Write-Output $subfolderPath before the save to see what value it has?

Edit: you don’t seem to be using the filename in the save path. Should it not be something like this:

if ($null -ne $i) {
    $i.Download()
    $filename = $i.filename
    $filename.Replace(':','')

		# Download attachments to location
		$i.Save("$subfolderPath\$filename")
...

You bet! The results were:

customerReport150009.2914184.xlsx

Drive : D
Provider : Microsoft.PowerShell.Core\FileSystem
ProviderPath : D:\autoloader\Plains_TT\Attachments\Attachment_0
Path : D:\autoloader\Plains_TT\Attachments\Attachment_0

Also, changed the $i.Save(“$subfolderPath”) to the one you listed above, (“$subfolderPath$filename”) and it has the same error.

OK, that looks like a PathInfo object. Ignore my previous edit for now.

It looks like this
$subfolderPath = $dir + "\Attachments\Attachment_" + $strCounter
which should be a string, is being overwritten by this:
$subfolderPath = Resolve-Path -Path $subfolderPath
which returns a PathInfo object.

And this
$i.Save($subfolderPath)
can’t pull the Path property from the PathInfo object.

Try
$i.Save($subfolderPath.Path)

Sorry for the delay. No bueno:

Exception calling “Save” with “1” argument(s): “The given path’s format is not supported.”
At D:\autoloader\Plains_TT\PlainsTTEmailRetrieval.ps1:76 char:3

  •     $i.Save($subfolderPath.Path)
    
  •     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    
    • CategoryInfo : NotSpecified: (:slight_smile: , MethodInvocationException
    • FullyQualifiedErrorId : NotSupportedException

Where do you define $dir ??

I’m pretty sure $dir is an alias for the existing directory that the script is running from which is my designated starting point for the attachments folders. Everything else works up until the actual save of the file to the subfolderpath. All write-output’s show a valid filename and path however it still says file path is not valid.

I also just tried $i.Save(“$subfolderPath.Path$attachmentName”) with and without the .Path after the $subfolderPath and it gave the same error both times. Is it potentially defaulting back to the original file name with the special characters in it because of $attachmentName. Is there a way of defining the filename after the special characters have been removed and using that in the save path?

Thanks!
Melissa

Yes, I think it’s not updating the filename - it may be a read only property, or it might be you’re not reassigning it.
Unfortunately, I cannot test it because you can’t override Google’s security settings to use PowerShell to connect to mailbox unless you’re using Workspaces.

Is filename a property of $i?

Can you confirm that the script works OK, as originally provided, when the attachment does not contain colons.

Confirmed, when I use the below code without the part about special characters it works as expected.

# !=========== DownloadReports (Date; dd-MMM-yyyy) ==============!
# This function downloads the reports that are attachments on emails sent from the system
Function DownloadReports-All ($date) {
	# Load IMAPX module
	$dll = "$dir\ImapX.dll"
	[Reflection.Assembly]::LoadFile($dll)

	# Setup Credentials
	$pwdgmail = ConvertTo-SecureString "XXXXXXXX" -AsPlainText -Force
	$bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($pwdgmail)
	$Username = "XXXXXXXXXXXXX"
	$Password = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr)


	# Initialize the IMAP client
	$client = New-Object ImapX.ImapClient

	# Set Fetching Mode to retrieve the part of message needed (less is better)
	$client.Behavior.MessageFetchMode = "Full"
	$client.Host = "imap.gmail.com"
	$client.Port = 993
	$client.UseSsl = $true
	$client.Connect()
	$client.Login($Username,$Password)

	# Get folder/label object containing emails you want to read from
	$res = $client.folders.subfolders | where { $_.path -eq '[Gmail]/All Mail'}

	# Create search query
	$searchSinceDate = $date
	$searchQuery = 'SUBJECT "THIS IS A TEST" SINCE ' + $searchSinceDate + ''
	Write-Host "Searching Emails for: $searchQuery"


	# Search email threads inside the subfolder
	$numberOfMessagesLimit = 200
	$messages = $res.search($searchQuery, $client.Behavior.MessageFetchMode, $numberOfMessagesLimit)

	# Create Attachment Folder
	New-Item $dir\Attachments -ItemType Directory -Force

	# Set Folder Path for Attachments
	$folderPath = Resolve-Path -Path "$dir\Attachments\"


	# for each message, download attachment if it exists
	# download into its own folder
	$counter = 0
$messageCount = 0

foreach ($message in $messages) {
	    Write-Host "Processing the next message: $message"
	    foreach ($i in $message.Attachments) {
		Write-Host "Processing the next attachment: $i"
		# Reset SubFolder location
		$strCounter = [string]$counter
		$subfolderPath = $dir + "\Attachments\Attachment_" + $strCounter

		# Reset SubFolder location
		$strCounter = [string]$counter
		$subfolderPath = $dir + "\Attachments\Attachment_" + $strCounter

		# Create new sub-folder
		New-Item $subfolderPath -ItemType Directory -Force
		$subfolderPath = Resolve-Path -Path $subfolderPath	

		# Fetch emails
		$i.Download();
        foreach($i in $messages.Attachments) {
    if($i -ne $null) {
        $i.Download()

		# Download attachments to location
		$i.Save($subfolderPath)

		# Uptick counter
		$counter++
		Write-Host "Finished with this attachment: $i"
		Write-Host "Counter is now $counter"
	    }
	    Write-Host "Finished with this message: $message"
	}


	# Display the messages in a formatted table
	$messages | ft *

	$client.Disconnect()
}
}}



# ========== Initialization ============
$dir = "$PSScriptRoot" 
$inputFileName = "sampleCSV.csv"
$outputFileName = "SampleHTML_built-in2.html" 
$title = "Sample Output HTML from CSV"

# Get current date
#$currentDate = Get-Date -Format "dd-MMM-yyyy"
$currentDate = (Get-Date).AddDays(0)
$currentDate = $currentDate.ToString("dd-MMM-yyyy")


# - Path initialization
$inputFilePath = "$dir\$inputFileName"
$outputFilePath = "$dir\$outputFileName"

# ============ Main Program ===============

# ----- Clean Up Last Day's Run-----
Remove-Item -Recurse -Force "$dir\Attachments\"

# ----- Retrieve Attachments for current day ------
DownloadReports-All($currentDate)

# ----- Get List of Files to Process (Should only be one) -----
$files = Get-ChildItem $dir\Attachments -Filter *.* -Recurse


Please put $i | Get-Member after $i.Download() and post the output.

On the original script, the $i | Get-Memberafter$i.Download() returns:

TypeName : ImapX.Attachment
Name : Download
MemberType : Method
Definition : void Download()

TypeName : ImapX.Attachment
Name : Equals
MemberType : Method
Definition : bool Equals(System.Object obj)

TypeName : ImapX.Attachment
Name : GetHashCode
MemberType : Method
Definition : int GetHashCode()

TypeName : ImapX.Attachment
Name : GetTextData
MemberType : Method
Definition : string GetTextData()

TypeName : ImapX.Attachment
Name : GetType
MemberType : Method
Definition : type GetType()

TypeName : ImapX.Attachment
Name : Save
MemberType : Method
Definition : void Save(string folder, string fileName)

TypeName : ImapX.Attachment
Name : ToString
MemberType : Method
Definition : string ToString()

TypeName : ImapX.Attachment
Name : ContentId
MemberType : Property
Definition : string ContentId {get;}

TypeName : ImapX.Attachment
Name : ContentTransferEncoding
MemberType : Property
Definition : ImapX.Enums.ContentTransferEncoding ContentTransferEncoding {get;}

TypeName : ImapX.Attachment
Name : ContentType
MemberType : Property
Definition : System.Net.Mime.ContentType ContentType {get;}

TypeName : ImapX.Attachment
Name : Downloaded
MemberType : Property
Definition : bool Downloaded {get;}

TypeName : ImapX.Attachment
Name : FileData
MemberType : Property
Definition : byte FileData {get;}

TypeName : ImapX.Attachment
Name : FileName
MemberType : Property
Definition : string FileName {get;}

TypeName : ImapX.Attachment
Name : FileSize
MemberType : Property
Definition : long FileSize {get;}

That looks promising.
It looks like filename is immutable - it doesn’t support a set method.

TypeName : ImapX.Attachment
Name : FileName
MemberType : Property
Definition : string FileName {get;}

However, the Save() method lets you set a filename:

TypeName : ImapX.Attachment
Name : Save
MemberType : Method
Definition : void Save(string folder, string fileName)

Try this:

$fileName = $i.FileName -replace ':','_'
$i.Save($subfolderPath,$fileName)
1 Like

You, my friend, are a GENIUS!!! That worked!

I would like to let you know that you are smarter than AI! I tried troubleshooting it with ChatGPT, but it didn’t work. Went through it for 2 days straight with no resolution before coming here. I am so glad I came here! THANK YOU!!!

2 Likes

I agree 100%, Matt is a genius!

3 Likes

:blush: you’re both too kind, just happy to help where I can.

1 Like