0 votes

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
by (1.6k points)

1 Answer

0 votes
by (216k points)

Update 2020

Starting with Adaxes 2020.1 it is possible to approve/deny requests directly from emails without logging in. For details on how to enable the feature, have a look at the following help article: https://www.adaxes.com/help/EnableApproveViaLink.

Original

Hello,

Thanks for your contribution, it is really very much appreciated! :)

Actually, we have such a feature in our TODO list. In one of the future releases it will be possible to implement such functionality (denying or approving Requests by just replying to the notification email) without any scripts.

0

I'm hoping so! :)

For us, we are only going to inform certain users that this function will be present as I'm not sure that it will be that reliable. It'll mainly be for users with Blackberrys who may have to approve high importance tickets quickly (one of the reasons why we extract the sender emails addresses, as we'll have a filter to only permit certain users to use it).

Getting it as an integral feature would be good for the 'great unwashed'!

PS Feature request - tabs on the approval screen so users can group approvals by the source.

FYI If anyone does play with the script, this is what it'll do:-


Rgds

0

PS - You may have to change the folder delimiter from a '.' to a '/' for Exchange mailboxes - i.e. Inbox/Approved, not Inbox.Approved

0

PS Feature request - tabs on the approval screen so users can group approvals by the source.

Thanks for the suggestion and for the update on the script! We'll think on the suggestion when defining features for new releases.

0

..we've upgraded quite a bit today. I won't post the changes unless anyone asks - but suffice to say, getting a reliable way of accurately identifying the GUID of the approval request (and not the target object), and making sure you accurately identify the 'response' - "Approve' or 'Deny' (taking into account people may try and use terms such as 'Not approved'!), when you have multiple mail clients that all alter the text structure in subtely different ways, is a challenge!

The binding to the Adaxes containers and processing the requests works like a charm though! :D

0

Hello,

By the way, speaking of "tabs on the approval screen so users can group approvals by the source", what do you mean in 'by the source'? Do you mean sorting/grouping by Request Originator or something else?

0

If you're asking... :)

Ideally, flexible - the two main types of grouping I can imagine are:-

'by requestor' - Our main scheduled task is the account review, so the ability to group approvals so that all 'Account Reviews' are held together would be useful (and also so that all requests from 'Bob' can be grouped away from 'Sue' etc.

'by operation' - harder I guess with scripted jobs and multiple actions, but ability to group by 'Group Additions', 'Account Changes' etc.

Rgds

0

Hello,

Thanks for the clarification. We' have a similar feature in our TODO list. We are going to completely redesign Adaxes Web Interface in the nearest future, and in the process of redesigning we will add the possibility to filter Approval Requests in the Web Interface by Operation Initiator, by Operation Type etc. It will function pretty much like how filtering works in Adaxes Administration Console now.

Related questions

0 votes
1 answer

Hello everyone, I've received a task to send a report of pending and denied approval requests of a specific task to an email of one of our managers. Since ... $report = $reportHeader + $reportFooter # Send Mail $Context.SendMail($to, $subject, $NULL, $report)

asked Apr 7, 2020 by rshergh (110 points)
0 votes
1 answer

Hello, is there a way to save powershell variable to axases attribute and send it via "send email notification" in Scheduled task? for example, check if Office 2016 ... .name) installed"} ` then add $customattrib value to Send email notification. Thank you

asked Feb 13, 2020 by vheper (20 points)
0 votes
1 answer

We have 4 om prem servers to setup Adaxes on, we currently have almost everything on one server but have crashed on several occassions when multiple scheduled jobs are ... way to achieve this configuration without having to buy double the licenses. Thanks' Jay

asked Sep 24, 2021 by willy-wally (3.2k points)
0 votes
1 answer

Hi there, i've a custom command with multiple powershell scripts (for clearance reasons). If for example the frist script produces an error i Write an Error but the next ... tried with an simple exit 1; I only Write-Errors on issues. Kind regards, Constantin

asked Jul 23, 2021 by Constey (190 points)
0 votes
1 answer

Hi there. We have a number of business rules that are triggered when a user account is updated. However, we would like the business rules to be processed in a specific ... its dynamic group feature provided a way to achieve it on a schedule) Many thanks -Nick

asked May 26, 2015 by nick.gatt (50 points)
3,326 questions
3,026 answers
7,727 comments
544,678 users