The Problem
I have a number of scheduled PowerShell scripts that perform various system health checks. If any of them detect something is amiss they send an alert email to myself and my support group using the send-mailmessage cmdlet.
However if the mail server was inaccessible when attempting to send-mailmessage, the command and resulting alert messages would fail. This was a probable scenario given that the purpose of the error checking scripts is to detect problems including the inaccessibility of network resources.
So I wanted to easily add a level of persistence to the error message emails without having to require the individual health check scripts run continuously.
My solution was to have the checking scrpits place email details into an xml file, and have a batch emailing script pickup and send those messages when it could connect to the mail server.
Constraints
I considered installing a local SMTP mail server such as hMail, and just let it relay through the corporate SMTP server. However;
- I didn’t want to introduce new software to the server, and even though hMail is relatively simple, even it is seems like overkill for the requirement.
- I didn’t think the corporate security guys would appreciate me installing an SMTP server on the network.
- Most importantly, I felt like stretching my brain doing some PowerShell problem solving.
So I thought a PowerShell only solution would be nice.
The Solution
Rather than have all my PowerShell scripts handle emailing I decided to create a simple email handling script I called PixSeeMailR. (The name makes sense to me in the suite of scripts for which it is used. See my article PixSeeLog: Error detection through image pixel inspection.)
The various other monitoring scripts create email messages (PowerShell objects that contain all the relevant message parameters) and deposit them into an “Out-Mail” folder as an XML file.
Messages in the Out-Mail folder are processed by PixSeeMailR periodically and moved to a “Sent-Mail” folder when they have been processed. If the message processing fails for any number of reasons, the messages remain in the Out-Mail folder to be processed in the next period.
This solution design uses a scheduled PowerShell script to check for messages. I also did consider using the System.IO.FileSystemWatcher to trigger the processing of messages as soon as they were deposited into the Out-Mail folder. That would require the script to run persistently however and for this exercise I wanted to rely on scheduled script execution.
The following diagram shows how the process will work.

Step 1 – Create Message
The first part of the solution is to create messages.
The following code creates message files by storing all the email message parameters into a PowerShell object and exporting that object to an XML file with Export-Clixml.
I use a message naming convention that guarantees (probably) unique message names.
This Create Message code can be added to any PowerShell script and activated according to whatever criteria is relevant.
$messageObject = @{
MailTo = "chris@emaildomain.com", "somebody@emaildomain.com"
MailFrom = "Testing Script <service_account@maildomain.com>"
MailSubject = "Error Detected"
MailPriority = "High"
MailBody="Hello this is a test message"
MailAttachments=""
MailAttempt=1
MailCreateTime = (Get-Date -Format "yyyy-MM-dd HH:mm:ss")
MessageID= (Get-Date -Format "yyyy_MM_dd_HH_mm_ss_fff")+"_servicename_" + $env:COMPUTERNAME +".xml"
}
Export-Clixml ("D:\PixSeeMailR\Out\"+$messageObject.MessageID) -InputObject $messageObject
Step 2 – Check For and Send Messages
The following code is designed to be run as a scheduled task. I run it every 60 seconds, but you could run it at whatever frequency you like.
- The code performs the following steps:
- Check if there are any emails to send
- Check if the mail server is accessible
- Attempt to send each email
- Move sent emails to the ‘Sent’ folder
- Any messages that were unable to send successfully remain in the ‘Out’ folder and have their Attempts attribute incremented.
<#--
PixSeeMailR by Chris
October 2016
--#>
# == Start Diagnostic Timer
$global:currentLog = ""
$SWScriptTime = [Diagnostics.Stopwatch]::StartNew()
#======================== FUNCTIONS
# ====================== Format Date Function
function GetDateFormattedForPaths(){
Return (Get-Date -Format "yyyy-MM-dd HHmmss")
}
function GetDateFormatted(){
Return (Get-Date -Format "yyyy-MM-dd HH:mm:ss")
}
function GetDateOnlyFormatted(){
Return (Get-Date -Format "yyyy-MM-dd")
}
# ====================== Logging Function
function LogThis($logthisstring){
$global:currentLog = $global:currentLog + "`r`n"+ ($logthisstring )
$logEntry = (GetDateFormatted) + " ~ " + $logthisstring
$logEntry | Out-File $logOutputPath -Append
$logEntry | Out-host
}
# ====================== Log Cleaning Function
function CleanLogs(){
$logCleanLimit = -90
$limit = (Get-Date).AddDays($logCleanLimit)
$path = $logPath
logthis("Cleaning Log Files older than " + $logCleanLimit + " Days")
# Delete files older than the $limit.
Get-ChildItem -Path $path -Recurse -Force | Where-Object { !$_.PSIsContainer -and $_.CreationTime -lt $limit } | Remove-Item -Force
}
# ====================== Exit Block
$ExitNow = {
# find me a line Morpheus
CleanLogs
$SWScriptTime.Stop()
logthis("EXIT NOW! Script complete in " + $SWScriptTime.Elapsed + " ... bravely bravely run away ...")
$global:currentLog | Out-File $currentLogPath -Force
exit
}
#======================== VARIABLES
$mailserver = "smpt.mail.com" #use your own SMTP server
$mailoutpath = "D:\PixSeeMailR\out\"
$mailsentpath = "D:\PixSeeMailR\sent\"
$logdate = getdateonlyformatted
$logPath = "D:\PixSeeMailR\logs\"
$logOutputPath = (("D:\PixSeeMailR\logs\") + $logdate + ("-MessageRelayLog.txt"))
$currentLogPath = ("D:\PixSeeMailR\logs\MessageRelay_current.txt")
#======================== BEGIN
#Populate mails object
$mails = Get-ChildItem $mailoutpath -filter "*.xml"
# check if there are any emails to send
logthis("Checking for messages at; " + $mailoutpath )
if($mails.count -eq 0){
# There are no messages to send.
logthis(" No Mail to Send - Ending")
&$ExitNow
}
# There are messages - proceed!
logthis(" " + $mails.Count + " Mails exist.")
# Check if mail server is available
logthis("Checking mail SMTP Relay server at: " + $mailserver)
if((Test-NetConnection $mailserver -port 25).tcptestsucceeded -eq $true){
# Mail Server is Available
logthis(" Mail Server is available at: " + $mailserver)
# Process messages
$Message = 1
foreach($mail in $mails){
Logthis(" Processing Message " + $Message + " " + $mail.fullname)
try{
$messageObject = Import-Clixml $mail.FullName
("@" + $messageobject.MailAttempt + ": " + $messageobject.MailSubject)
# $messageobject.MailTo
# $messageobject.MailFrom
# $messageobject.MailSubject
# $messageobject.MailBody
if ($messageObject.MailAttachments -eq ""){
logthis (" process mail without attachments")
send-mailmessage -to $messageobject.MailTo -from $messageobject.MailFrom -subject ("@" + $messageobject.MailAttempt + ": " + $messageobject.MailSubject) -body $messageobject.MailBody -priority $messageobject.MailPriority -SmtpServer $mailServer -erroraction Stop #-Credential $cred
}
else{
logthis (" mail has attachments")
send-mailmessage -to $messageobject.MailTo -from $messageobject.MailFrom -subject ("@" + $messageobject.MailAttempt + ": " + $messageobject.MailSubject) -body $messageobject.MailBody -priority $messageobject.MailPriority -Attachments $messageObject.MailAttachments -SmtpServer $mailServer -erroraction Stop #-Credential $cred
}
logthis(" Moving message from: " + $mail.FullName)
logthis(" Moving message to: " + ($mailsentpath + $mail.Name))
move-item $mail.FullName ($mailsentpath + $mail.Name)
Logthis(" ------------------------------------ end")
}
catch{
logthis ("ERROR - an error has occurred sending message: " + $mail.FullName)
$errorcount = $error.count -1
#logthis ($error.item($errorcount))
logthis (" Incrementing send attempt to " + (1+($messageobject.MailAttempt)))
$messageobject.mailattempt = 1+($messageobject.MailAttempt)
Export-Clixml $mail.FullName -InputObject $messageObject
}
}
}
else{
# else mail server is not available - log and exit
logthis("ERROR - Mail server is unavailable at: " + $mailserver + " - Exiting!")
}
&$ExitNow
Limitations
The solution presented here relies on two success testing methods.
1 – A simple TestNet-Connection against the target SMTP server. If the server is not available the messages don’t attempt to send.
2 – The success or failure of the Send-MailMessage cmdlet. If the cmdlet returns any error, the message is flagged for retry.
This approach doesn’t guarantee that the messages will reach the intended recipient, only that the SMTP server accepted the messages without error.
Future Upgrade Ideas
Next, I would like to implement the following upgrades;
- Turn the PixSeeMailR into a Windows service.
- Consider using System.IO.FileSystemWatcher to trigger mail sends immediately.
Addendum
PixSeeMailR has been running on several of my servers for the past few days and seems to be working well. I have scheduled a daily test message drop for 10 am every day and so far they have been processed as expected.
The average run time when there are no messages is 00.025 seconds.