How to write an Email Processing script - Example : Processing DMARC Report attachments in PowerShell using the Microsoft Graph SDK.
I’ve been kind of late enabling DMARC on my personal email domains (it was introduced as a standard in 2012). In 2024 email providers are stepping up DMARC requirements a little https://www.proofpoint.com/au/blog/email-and-cloud-threats/google-and-yahoo-set-new-email-authentication-requirements . While I’m not a bulk sender and never intend to be if your like me and haven’t checked if you have DMARC setup it’s a good time to start.
So that brings me to the topic of this post once you setup DMARC you have the option of receiving DMARC reports which are Zip attachments on emails with and XML payload telling you about the DMARC actions that took place on the receiving endpoint. During the initial setup phase of DMARC its advisable to do a softfail and then look at these reports to make sure you don’t have any issues that might affect you when you move to the reject phase and then ongoing you would use them as operational reports.
There are a whole bunch of good scripts and services out there for processing these DMARC reports, Dmarcian is one you always here good things about but there are plenty of roll your own and these are great and worthwhile investments especially when your dealing with a large volume of reports. But if you want to do something a little more custom or if like me you just want to easily read a DMARC report from the PowerShell console you can write your own script to do this relatively easily. In the past I’ve written about doing simple email attachment processors before https://gsexdev.blogspot.com/2010/01/writing-simple-scripted-process-to.html and in this post I’ll cover the specifics around doing this in the Microsoft Graph.
Requirements and Authentication
In this example I’ve use the PowerShell Graph SDK this abstracts most of the Request, Response and Authentication plumbing into the module. Connect-MgGraph has all the authentication options you might need from Certificate based App authentication to Delegate user based authentication. In my example cmdlet i expect that you have done whatever authentication you need and you justr passing in that Mailbox and parameters you want to process around.
Searching
DMARC reports are just one of the many emails that are going to be found in a mailbox and if you are writing an Email processing script or process you want to do a least some type of search to limit the results you get back from the Microsoft Graph. There are 3 different ways of doing a mail search in the Microsoft Graph.
Do a Filter or Search on a particular Folder or the Messages endpoint (which is essentially a search folder that covers most mail folder in the Mailbox).
Use the Search Endpoint in Graph and target Message as the Entity your searching for.
For this process a folder based search (“Mailfolder/FolderId/Messages”) hits more the sweet spot and because I want to search on attachment name (or attachment types) a Search is better then a filter as Search uses the Exchange Content indexes so that means my search will be faster and less complex. So for this type of search I need to build a KQL querystring that covers all the conditions I want to limit my search to.
Search Conditions
For my process I want the following Search conditions
Received Time of the Message, each time the cmdlet is run there is a mandatory parameter that asks you to enter in the number of hours the script should go back and return messages from. Exchange stores Messages in UTC time so that means you need to first convert the local time to UTC and then format that in KQL datetime format and then you can use the following KQL to filter based on the received datetime of a message. Some PowerShell code to do this looks like
$received = (Get-Date).AddHours(-$HoursToLookBack).ToUniversalTime().ToString("yyyy-MM-ddThh:mm:ss")
$Messages = Get-MgUserMailFolderMessage -Userid $MailboxName -All -MailFolderId $FolderId -Search "(received>=$received)”
Subject of the Message, In the DMARC RFC it states the Subject of the Message “Should” contain “Report-ID:”. While its not a MUST it means the majority of the DMARC reports you get should have this text in the Subject. This means for this process I can add this as a condition in my Query string eg (subject:Report-Id) means it will match against any message where the subject contains this string (note its not an equal condition which requires quotes).
Attachment Names In DMARC reports messages the RFC states it should be gzip compressed. However what I see is both gzip and zip attachments (if your interested in the technical differences between the two have a read of this ). In KQL you can filter down messages that contain these type of attachments using ((attachmentnames:.gz) OR ( attachmentnames:.zip)) which means I’m just targeting attachments that have these words in their filename.
Putting it all together in one query string means Bracketing (or using parenthesis ()
to group multiple property restrictions). So my query string looks like this"(received>=$received) AND (subject:Report-Id) AND ((attachmentnames:.gz) OR (attachmentnames:.zip))"
When you think about how there server will solve for this just think about how you would do this in a Math equation.
Message Retrieval
Message retrieval is handled by the Get-MgUserMailFolderMessage cmdlet in the Graph PowerShell SDK eg $Messages = Get-MgUserMailFolderMessage -Userid $MailboxName -All -MailFolderId $FolderId -Search "(received>=$received) AND (subject:Report-Id) AND ((attachmentnames:.gz) OR ( attachmentnames:.zip))" -ExpandProperty "Attachments" -Select Subject,ReceivedDateTime,Attachments,Sender
So in the above request its first limiting my search to a particular folder in the script I allow you to pass a folderpath parameter in the case where you have a Inbox rule to move these report message to a particular folder. Otherwise the default is to look only in the Inbox. It then uses the -ExpandProperty so it will will expand the attachments and return the content in one request. And finally I used Select to reduce the number of properties returned to make the request a little more efficient.
So after all that hopefully you end up with a collection of DMARC report messages where the content of the attachments have been downloaded.
Processing Attachments
In my script I’ve downloaded the Attachments in memory, so I have basically a Base64Encoded String in memory which has the content of the attachment. In my instance this is compressed .
$atContent = [System.Convert]::FromBase64String($attachment.AdditionalProperties.contentBytes)
$compressedStream = new-object System.IO.MemoryStream(, $atContent)
To convert that to a String that i can use to read the xml associated with the DMRAC report I need to first covert the base64 to a ByteArray which I can then a create Stream from. I can then decompress that stream using the appropriate class from System.IO.Compression and read that stream with a StreamReader to get the associated XMLString. eg
$gzipStream = new-object System.IO.Compression.GZipStream($compressedStream, [System.IO.Compression.CompressionMode]::Decompress)
$reader = [System.IO.StreamReader]::new($gzipStream)
[XML]$dracResult = $reader.ReadToEnd()
Processing the DMARC payload
DMARC being a relative mature standard the payload for the report is XML (which is kind of frowned on today). For this I wanted to turn the result into something i could quickly scan at the cmdline and I also wanted to do a GeoIp lookup on the sourceIP address so I could get a feel for the source location. For this i let powershell do the XML parsing and create a custom PowerShell object and wrote that out as a result so it can be processed further if necessary. So the result look something like this
The script is available on GitHub https://github.com/gscales/Powershell-Scripts/blob/master/Graph101/GraphSDK/dmarcDlnandUncompress.ps1
To use this script just Import the module likeimport-module .\dmarcDlnandUncompress.ps1 -Force
Make sure you have connected to the graph using Connect-MgGraph
Then use something like the following to process the last 24 hours of Message with DMARC Reports
Invoke-DownloadAndProcessDmarc -MailboxName user@domain.com -HoursToLookBack 24
To specify a folder other then the inbox you can useInvoke-DownloadAndProcessDmarc -MailboxName user@domain.com -HoursToLookBack 24 -folderPath \Inbox\DmarcReports