Bit of giving something back..., especially as it includes snippets of code that I've been given advice on...!
We've piloted some code to allow users to respond to Approval Requests by replying to the email with a one word response - 'Approve' or 'Deny' (they're actually allowed approve, approved, deny and denied, but you get the drift!).
The code is by no means finished, and it's still got lots of tweaks, consolidation and simplification etc, but in it's current state it may be useful for anyone who wants to do the same thing as it is quite "chatty" and logical to follow. Basically:-
1. Read all mails in mail Inbox
2. Strips and moves to 'garbage' anything that isn't a direct reply to an approval request (based on subject line string)
3. For each mail that is a response, finds the ticket GUID (parses the content for the HTML link in the original email)
4. Tries to Approve or Deny the ticket by calling the Adaxes API to pass a Approve() or Deny() to the relevant pending approval
5. Shifts the approved, denyed, or unknown (couldn't find a 'decision' in the right place) mails to archive folders
Important thing to note is that it used the "NetCmdets" (http://www.netcmdlets.com/) powershell module to implement IMAP based mailbox management functions. Non-commercial (test) version is free to download, so you can play and it only a few hundred $ to purchase. IMAP is a bit of a vague protocol so if you're anything like us you may find that some servers won't delete 'moved' mails, so a bit of the code is to output before\after counts to make sure everything is where it should be.
# Import required PS modules, you may have to tweak path etc for different locations
$env:PSModulePath = $env:PSModulePath + ";C:\Windows\System32\Modules"
Import-Module NetCmdlets
Import-Module Adaxes
# Load current variables into a variable
$startupVariables=""
# Create a custom function to clear any variables, other than $startupVariables, when called
new-variable -force -name startupVariables -value ( Get-Variable | % { $_.Name } )
function Cleanup-Variables
{
Get-Variable | Where-Object { $startupVariables -notcontains $_.Name } | % { Remove-Variable -Name "$($_.Name)" -Force -Scope "global" }
}
# Create a custom function to strip non-printing characters, used when parsing email content to create a 'raw text' block
function RemoveLineCarriage($object)
{
$result = [System.String] $object;
$result = $result -replace "`t","";
$result = $result -replace "`n","";
$result = $result -replace "`r","";
$result = $result -replace [Environment]::NewLine, "";
$result;
}
$mailserver = "localhost" # Sets the mail server with the mailbox holding approval replies
$msuser = "adaxes@test.net" # Sets the mail server username
$mspass = "password" # Sets the mail server password
$adaxesserver = "adaxes.test.net" # Sets the Adaxes instance
# Connect to mailserver, quit if fails
Try
{
$imapstr = Connect-IMAP -server $mailserver -user $msuser -password $mspass # -LogFile c:\temp\imap-debug.log
}
Catch
{
Write-Host
Write-Host "Can't do a damn thing if I can't connect to the mail server"
Cleanup-Variables
Continue
}
# Use NetCmdLet function to pull all new mails in Inbox into an array
# Load folder structure into arrays, quit if any fail
Try
{
$newmailarray = @(Get-IMAP -connection $imapstr -folder INBOX)
$approvedMail = @(Get-IMAP -connection $imapstr -folder INBOX.Approved)
$deniedMail = @(Get-IMAP -connection $imapstr -folder INBOX.Denied)
$unknownMail = @(Get-IMAP -connection $imapstr -folder INBOX.Unknown)
$garbageMail = @(Get-IMAP -connection $imapstr -folder INBOX.Garbage)
$errorMail = @(Get-IMAP -connection $imapstr -folder INBOX.Reprocess)
}
Catch
{
Write-Host
Write-Host "Required folder structure not present, make sure 'Approved', 'Denied', 'Unknown', 'Garbage' and 'Reprocess' folders have been created"
Cleanup-Variables
Continue
}
Write-Host
# Check for new mails, if none, close early
If ($newmailarray.count -ne "0")
{
Write-Host "Number of mails in inbox :" $newmailarray.count
}
Else
{
Write-Host "Number of mails in inbox : 0 - quiting early to go for a smoke"
$imapStrShut = Disconnect-IMAP -connection $imapstr
Write-Host
Write-Host "Closed Server Connection" $imapStrShut
Cleanup-Variables
Continue
}
# Start processing new mails
Write-Host
Write-Host "Parsing new mails"
Write-Host "-----------------"
Write-Host
If ($newmailarray.count -ne $NULL)
{
$potential = @()
$garbage = @()
ForEach ($newmail in $newmailarray)
{
If ($newmail.subject -ne "RE: Active Directory - Approval Request") ## IMPORTANT: Change this if the Adaxes approval ticket subject line is different ##
{
# Ignore emails that aren't replies to the approval request tickets
$garbage += $newmail.ID
Write-Host "** Mail ID" $newmail.ID "Ignored **"
Write-Host "Subj: " $newmail.subject
Write-Host "From: " $newmail.fromemail
$gmovestatus = Move-IMAP -connection $imapstr -folder Inbox -Message $newmail.ID.toString() -Destination Inbox.Garbage
Write-Host "Moved message" $newmail.ID "to folder" $gmovestatus.Destination
set-imap -connection $imapstr -folder Inbox -message $newmail.ID.toString() -expunge # Have to issue a expunge after moving, not sure why but stops phantom copies being left behind
Clear-Variable -name gmovestatus
Write-Host
}
Else
{
# Identify mails to process
# Could call processing logic here to be cleaner, but instead feeds into a secondary process as it made development of the script easier
$potential += $newmail.ID
Write-Host "** Mail ID" $newmail.ID "Tagged For Processing **"
Write-Host "Subj: " $newmail.subject
Write-Host "From: " $newmail.fromemail
Write-Host
}
}
}
# Writes a summary of the mails found and what decision was made
Write-Host "Summary Results"
Write-Host "----------------------------"
Write-Host "Number of mails ignored :" $garbage.count
Write-Host "Number of mails to process :" $potential.count
Write-Host "Mail ID's ignored :" $garbage
Write-Host "Mail ID's to be processed :" $potential
# If we've got mails that need processing, get into the meat of the script
If ($potential -ne $NULL)
{
# Bind to the Approval Pending container for the Adaxes stuff
$trash = [Reflection.Assembly]::LoadWithPartialName("Softerra.Adaxes.Adsi")
$admNS = New-Object("Softerra.Adaxes.Adsi.AdmNamespace")
$admService = $admNS.GetServiceDirectly($adaxesserver)
$containerPath = $admService.Backend.GetConfigurationContainerPath("ApprovalRequests")
$container = $admService.OpenObject($containerPath.ToString(), $NULL, $NULL, 0)
$requests = $container.GetApprovalRequests("ADM_APPROVALSTATE_PENDING")
Write-Host
Write-Host "Approval Tickets"
Write-Host "----------------"
# Cycle through the emails tagged as being valid replies to approval tickets
Foreach ($approvalReply in $potential)
{
# Open the full email, this includes the textblock so we can look for the GUID's and other elements we need
$emailobject = Get-IMAP -connection $imapstr -view $approvalReply
Write-Host
Write-Host "Mail ID :" $approvalReply
Write-Host "From :" $emailobject.FromEmail # This will be used in script v2 when we also confirm that the replying user is a valid approver for the ticket as a form of authentication
# Write-Host "Date :" $emailobject.Date
$emailObjectRaw = RemoveLineCarriage($emailobject.Text) # Call our custom function to strip any special characters in the textblock
# Find the approval ticket GUID. This assumes the indexOf string as set will uniquely lock on to the ticket GUID, may need to tweak based on URL length etc.
$TGPo = $emailObjectRaw.indexOf("Click here to approve or deny the request<http://adaxes.svr.testsvr.net/testingSelfService/ViewObject.aspx?guid=")
$ticketGUID = $emailobjectRaw.SubString($TGPo + 112,36)
# Find the target user GUID - not currently used, but may do in future. This bit assumes the URL is 71 characters long, and the first hit of the indexOf string is the target user GUID, may need to tweak.
$UGPo = $emailObjectRaw.indexOf("<http://adaxes.svr.testsvr.net/testingSelfService/ViewObject.aspx?guid=")
$userGUID = $emailobjectRaw.SubString($UGPo + 71,36)
#Find the approval decision. May need to tweak based on mail server, mail clients etc, the string below always directly preceeds the reply text for us, but may need altering.
$replyPos = $emailObjectRaw.indexOf("Content-Transfer-Encoding: 7bit")
# Grab the first 8 characters of the 'reply' - enough to cover all accepted variants - Approve, Approved, Deny and Denied. Doesn't need to be case-sensitive.
$replyText = $emailobjectRaw.SubString($replyPos + 31,8).toLower()
$decision = "UNDETERMINED"
If ($replyText -like "*approve*")
{
$decision = "APPROVED"
}
If ($replyText -like "*denied*" -or
$replyText -like "*deny*")
{
$decision = "DENIED"
}
Write-Host "Ticket GUID : {$ticketGUID}"
Write-Host "Reply Text :" $replyText
Write-Host "Approval :" $decision
# Based on the derived 'decision', try to 'Approve' or 'Deny' the associated Adaxes ticket, or assign to an 'Unknown' folder for manual checks etc.
Switch ($decision)
{
"APPROVED" {
$ticketFound = $FALSE
ForEach ($requestID in $requests)
{
$guid = New-Object "System.Guid" (,$requestID)
$guid = $guid.ToString("B")
# Skip approval tickets that do not match the parsed ticket from the email
If ($guid -ne "{$ticketGUID}")
{
Continue
}
# If there's a valid pending approval, approve it
Else
{
$ticketFound = $TRUE
$requestPath = "Adaxes://<GUID=$guid>"
$request = $admService.OpenObject($requestPath, $NULL, $NULL, 0)
$request.Approve()
Write-Host "GUID Match :" $guid
Write-Host "State : Processed"
Break
}
}
# After approving the request ticket, move the mail to an archive folder
If ($ticketFound -eq $TRUE)
{
$processedItem = Move-IMAP -connection $imapstr -folder Inbox -Message $approvalreply -Destination Inbox.Approved
}
# Logically, if this 'switch' process was called and $ticketFound is still false, then this must mean the approval was no longer pending, so assume this is a duplicate response.
If ($ticketFound -eq $FALSE)
{
Write-Host "GUID Match : None - No pending approval"
$processedItem = Move-IMAP -connection $imapstr -folder Inbox -Message $approvalreply -Destination Inbox.Reprocess
}
# Expunge the original mail - still not sure why this is needed, something to do with the way IMAP works, but maybe our mail server, so check if the moved copy is being deleted.
set-imap -connection $imapstr -folder Inbox -message $newmail.ID.toString() -expunge
}
"DENIED" {
$ticketFound = $FALSE
ForEach ($requestID in $requests)
{
$guid = New-Object "System.Guid" (,$requestID)
$guid = $guid.ToString("B")
If ($guid -ne "{$ticketGUID}")
{
Continue
}
Else
{
$ticketFound = $TRUE
$requestPath = "Adaxes://<GUID=$guid>"
$request = $admService.OpenObject($requestPath, $NULL, $NULL, 0)
$request.Deny("Denied via email integration")
Write-Host "GUID Match :" $guid
Write-Host "State : Processed"
Break
}
}
If ($ticketFound -eq $TRUE)
{
$processedItem = Move-IMAP -connection $imapstr -folder Inbox -Message $approvalreply -Destination Inbox.Denied
}
If ($ticketFound -eq $FALSE)
{
Write-Host "State : Reprocessing Error"
$processedItem = Move-IMAP -connection $imapstr -folder Inbox -Message $approvalreply -Destination Inbox.Reprocess
}
set-imap -connection $imapstr -folder Inbox -message $newmail.ID.toString() -expunge
}
"UNDETERMINED" {
$processedItem = Move-IMAP -connection $imapstr -folder Inbox -Message $approvalreply -Destination Inbox.Unknown
set-imap -connection $imapstr -folder Inbox -message $newmail.ID.toString() -expunge
}
}
Write-Host "New Folder :" $processedItem.Destination
# Write-Host $emailObjectRaw # Uncomment for debugging - useful if you need to play with the indexOf and subString strings etc
# Write-Host $emailObject.Text # Uncomment for debugging - useful if you need to play with the indexOf and subString strings etc
}
}
Else
{
Write-Host
Write-Host "No new approvals to process, clearing up"
}
# Write all folder counts to screen, this was used to ensure that duplicates weren't being made during moving etc
Write-Host
Write-Host "Mail totals before processing"
Write-Host "-----------------------------"
Write-Host "Inbox Mails : " $newmailarray.count
Write-Host "Approved Mails: " $approvedMail.count
Write-Host "Denied Mails : " $deniedMail.count
Write-Host "Unknown Mails : " $unknownMail.count
Write-Host "Reprocessed : " $errorMail.count
Write-Host "Garbage Mails : " $garbageMail.count
$pretotal = $newmailarray.count + $approvedMail.count + $deniedMail.count + $unknownMail.count + $garbageMail.count + $errorMail.count
Write-Host "-----------------------------"
Write-Host "Total : " $pretotal
Write-Host "-----------------------------"
# Disconnect and reconnect - find counts again, we discovered that we had to reconnect to get a true figure as IMAP kinda cache's moved items as a Delete and Create sequence.
Disconnect-IMAP -connection $imapstr
$imapstr2 = Connect-IMAP -server $mailserver -user $msuser -password $mspass
$newInbox = @(Get-IMAP -connection $imapstr2 -folder INBOX)
$approvedMail = @(Get-IMAP -connection $imapstr2 -folder INBOX.Approved)
$deniedMail = @(Get-IMAP -connection $imapstr2 -folder INBOX.Denied)
$unknownMail = @(Get-IMAP -connection $imapstr2 -folder INBOX.Unknown)
$garbageMail = @(Get-IMAP -connection $imapstr2 -folder INBOX.Garbage)
$errorMail = @(Get-IMAP -connection $imapstr2 -folder INBOX.Reprocess)
Write-Host
Write-Host "Count Verification"
Write-Host "----------------------------"
Write-Host "Inbox Mails : " $newInbox.count
Write-Host "Approved Mails: " $approvedMail.count
Write-Host "Denied Mails : " $deniedMail.count
Write-Host "Unknown Mails : " $unknownMail.count
Write-Host "Reprocessed : " $errorMail.count
Write-Host "Garbage Mails : " $garbageMail.count
$posttotal = $newInbox.count + $approvedMail.count + $deniedMail.count + $unknownMail.count + $garbageMail.count + $errorMail.count
Write-Host "-----------------------------"
Write-Host "Total : " $posttotal
Write-Host "-----------------------------"
Write-Host
# Check that the pre-processing and post-processing totals are the same. As we never delete anything, only move (and expunge!) we should have the same for both.
# If your mail server handles IMAP differently and you find these figures are diverging etc, you may have to disable the expunging and/or tweak the IMAP commands.
If ($pretotal -eq $posttotal)
{
Write-Host "Diff: " ($posttotal - $pretotal) " Verification PASSED"
}
Else
{
Write-Host "Diff: " ($posttotal - $pretotal) " Verification FAILED"
}
Write-Host
Write-Host "Closed Server Connection" $imapStrShut
Cleanup-Variables