Converting between timeZones

Further to my post in enumerating the time zones using

[System.TimeZoneInfo]::GetSystemTimeZones() |
select standardName,daylightName,DisplayName

I wrote two functions to be able to determine the timezone of a user based on the city and country locations ( Get-TimeZone )  and one to convert a time between any two timezones.  As I’m UK based if I omit the fromTimeZone parameter my function defaults to GMT Standard Time.

Get-TimeZone needs a city, e.g. London and a Country Abbreviation, e.g. GB or US to return the timezone name that can then be used by the Convert_timeZone function.

The function listing also uses a technique I’m sure I’ve blogged about too that limits how many emails a function will send out.  It can be embarassing for a script to go wrong and send 1000’s of emails so you should set some expected limits that will trigger mechanisms to prevent yet more spam in your environment.

So that this function is stand alone I use script scope variables and just in case I forget to instantiate these before calling the function I use test-path to check if the variable already exists and create it if it doesn’t.

if ( ! ( Test-Path variable:script:maxEmailLimit ) ) {
 $script:maxEmailLimit = 10
}
  • maxEmailLimit is the maximum number of emails I’ll let my function send
  • emailSent tracks how many emails I sent and I compare it with my limit
  • MissingCity and Missing Country allow my main program to report these later

I use an embedded function Send-email to handle sending email alerts to myself so that I capture any missing City and Country locations. m The function increments the emailSent counter and also checks that my limit hasn’t been exceeded. If it has then I send a final warning email and then stop sending any more.

function Send-email {
 param ( $bodyText,$subject)
 $script:emailsSent ++
 if ( $script:emailsSent -gt $($script:maxEmailLimit+1) ) {
  return
 }
 elseif ( $script:emailsSent -eq $($script:maxEmailLimit+1) ) {
  Send-MailMessage `
   -To $scriptOwner `
   -From $scriptOwner `
   -SmtpServer "FQDNofMYSMTPServer" `
   -Body "TOO MANY EMAILS SENT please investigate`r`n $bodyText" `
   -Subject "TOO MANY EMAILS SENT- $subject"
 }
 else {
  Send-MailMessage `
   -To $scriptOwner `
   -From $scriptOwner `
   -SmtpServer "FQDNofMYSMTPServer" `
   -Body $bodyText `
   -Subject $subject
 }
}

To add a location you need to look up the timezone information using google and work out which timezone string to use

[System.TimeZoneInfo]::GetSystemTimeZones() |
select standardName,daylightName,DisplayName

Here’s the full listing of Get-TimeZone. It’s just a simple switch statement really.

function Get-TimeZoneID {
 param ( $country,$city )
 if ( ! ( Test-Path variable:script:maxEmailLimit ) ) {
  $script:maxEmailLimit = 10
 }
 if ( ! ( Test-Path variable:script:emailsSent ) ) {
  $script:emailsSent = 0
 }
 if ( ! ( Test-Path variable:script:MissingCity ) ) {
  $script:MissingCity = @()
 }
 if ( ! ( Test-Path variable:script:MissingCountry ) ) {
  $script:MissingCountry = @()
 }
 function Send-email {
  param ( $bodyText,$subject)
  $script:emailsSent ++
  if ( $script:emailsSent -gt $($script:maxEmailLimit+1) ) {
   return
  }
  elseif ( $script:emailsSent -eq $($script:maxEmailLimit+1) ) {
   Send-MailMessage -To $scriptOwner -From $scriptOwner -SmtpServer "FQDNofMYSMTPServer" -Body "TOO MANY EMAILS SENT please investigate`r`n $bodyText" -Subject "TOO MANY EMAILS SENT- $subject"
  }
  else {
   Send-MailMessage -To $scriptOwner -From $scriptOwner -SmtpServer "FQDNofMYSMTPServer" -Body $bodyText -Subject $subject
  }
 }
 switch ( $country ) {
  "AU"                   { $timeZoneID = "AUS Eastern Standard Time"      ; break } # Australia
  "CA"                   { $timeZoneID = "Eastern Standard Time"          ; break } # Canada
  "DE"                   { $timeZoneID = "Central European Standard Time" ; break } # Germany
  "GB"                   { $timeZoneID = "GMT Standard Time"              ; break } # United Kingdom
  "US" {
   switch ( $city ) {
    "Chicago"       { $timeZoneID = "Central Standard Time"    ; break }
    "Los Angeles"   { $timeZoneID = "Pacific Standard Time"    ; break }
    "New York"      { $timeZoneID = "US Eastern Standard Time" ; break }
    "Off-site"      { $timeZoneID = "US Eastern Standard Time" ; break } # Off Site at a specific location
    default         {
     $timeZoneID = "US Eastern Standard Time"
     if ( $script:MissingCity -notcontains $city ) {
      $script:missingCity += $city
      Send-email -bodyText  "Missing TZ for US CITY:'$city'" -subject "Missing TZ in $scriptName ARS Script Version: $($scriptVersion)"
     }
    }
   }
   break
  }
  default                {
   $timeZoneID = "GMT Standard Time"
   if ( $script:MissingCcountry -notcontains $country ) {
    $script:missingCountry += $country
    Send-email -bodyText  "Missing TZ for CITY:'$city' in COUNTRY:'$country'" -subject  "Missing TZ in $scriptName ARS Script Version: $($scriptVersion)"
   }
  }
 }
 return $timeZoneID
}

Convert-TimeZone is a little more complex and I plagiarized most of this from google.

To convert to another timezone from GMT use this command line.

$expiryTime = get-date $(Convert-TimeZone -time $user.PasswordExpires -fromTimeZoneID “GMT Standard Time” -toTimeZoneID $(Get-TimeZoneID -city $user.l -country $user.c))

Where $user is an AD user with attributes l = city and c = country abbr

function Convert-TimeZone {
 [CmdletBinding()]
 param(
  [Parameter(Mandatory=$false)]
   $time,
   [Parameter(Mandatory=$false)]
   $fromTimeZoneID,
   [Parameter(Mandatory=$false)]
   $toTimeZoneID
 )
 # if no from timezone provided use the local timezone
 if ( ! ( $fromTimeZoneID ) ) {
  #$fromTimeZoneID = (([System.TimeZoneInfo]::Local).Id).ToString()
  $fromTimeZoneID = "GMT Standard Time"
 }
 if ( $fromTimeZoneID -eq $toTimeZoneID ) {
  # nothing to do other than convert the date to a string and return
  return  $(Get-Date($time) -format "dddd, MMMM dd, yyyy h:mm:ss tt")
 }
 # if no time given as a parameter use current date and time using get-date
 if (!($time))   {
  $time = [DateTime]::SpecifyKind((Get-Date), [DateTimeKind]::Unspecified)
 }
 else {
  try { 
   $time = [DateTime]::SpecifyKind((Get-Date([dateTime]$time)), 
           [DateTimeKind]::Unspecified) }
  catch {
   $breakPoint = 0
  }
 }
 $Error.Clear()
 try { 
  $UTC = [System.TimeZoneInfo]::ConvertTimeToUtc($time,
         [System.TimeZoneInfo]::FindSystemTimeZoneById($fromTimeZoneID)) 
 }
 catch {
  $Error
 }
 if ($toTimeZoneID) {
  try { $toTimeZoneTime = [System.TimeZoneInfo]::ConvertTime($UTC, 
        [System.TimeZoneInfo]::FindSystemTimeZoneById($toTimeZoneID)) 
  }
  catch {
   $Error
  }
  Write-Verbose ("{0}: {1}" -f $fromTimeZoneID, $time)
  Write-Verbose ("{0}: {1}" -f $toTimeZoneID, $toTimeZoneTime)
 } 
 else {
  Write-Verbose ("{0}: {1}" -f $fromTimeZoneID, $time)
  $times = @()
  foreach ($timeZone in ([system.timezoneinfo]::GetSystemTimeZones())) {
   $times += (New-Object psobject -Property @{'Name' = $timeZone.DisplayName;
   'ID' = $timeZone.id; 'Time' = 
   [System.TimeZoneInfo]::ConvertTime($UTC,`
   System.TimeZoneInfo]::FindSystemTimeZoneById($timeZone.id)); `
   'DST' = $timeZone.SupportsDaylightSavingTime})
  }
  $times | Sort-Object Time | Format-Table -Property * -AutoSize
 }
 try {
  $toTimeZoneTime = $(Get-Date($toTimeZoneTime) `
                         -format "dddd, MMMM dd, yyyy h:mm:ss tt")
 }
 catch {
  $toTimeZoneTime = $null
 }
  return $toTimeZoneTime
}

How to get your mugshot photo into AD

My previous posts on this topic explained how to convert the format to a  jpeg  then resize it to fit into the AD size limit and then finally how to upload it into AD in the thumbnailAttribute.

This worked fine for years but recently we had some complaints that the pictures were not consistent. AD , SharePoint and Outlook were not all showing the same photograph.

The long and the short of this is that exchange creates it’s own copy on the exchange server and if you don’t use the exchange commandlet ‘Set-UserPhoto’ to upload the photo then this cache may not be updated.  You can force it to be updated by deleting the cached photo but it’s better to just use the new commandlet like this:

Set-UserPhoto -Identity $guid -PictureData ([System.IO.File]::ReadAllBytes($jpegFile)) -Confirm:$false

Automating ARS Scheduled Task Parameters

The blog title might seem a little confusing and I suspect that not many people are going to be searching for this.  

I thought it was a clever idea and I’ve been picking away at it for some time but I only completely solved it today.  As it turns out some of my earlier posts on how to write a function to get the ARS scheduled tasks had bugs in and I hadn’t even realised it.

I have quite a lot of ARS scheduled tasks in my environment.  I could write these to be monolithic with all the variables embedded but this means if I want to reuse the script for another scheduled task I can’t and if I want to change one of the variables at a later date I have to edit the script. So two problems,

1. the variables are not visible without reading the script and
2. I need to debug the script to change the variables.

Let’s quickly cover off how to get the scheduled task parameters. If you are running the script inside ARS, i.e. its actually the scheduled task running then you can use the intrinsic $Task.DirObject like this:

$Task.DirObj.GetInfo() 
$Task.DirObj.GetInfoEx(@('edsaParameters'),0)  
#  if no parameters are specified this command will trigger the catch clause
$strParameters         = $Task.DirObj.Get('edsaParameters')

if this is being run in an editor externally then you get the task parameters like this:

$Task = Get-QADObject -Identity $script:TaskDN  `
          -IncludedProperties edsaParameters,edsaModule -Proxy
$strParameters = $Task.edsaParameters

in both case you now need to convert the $strParameters variable to an [xml] object like this:

$strParameters = '<parameters>' + $strParameters + '</parameters>'
# and then return this variable using  
Return $strParameters
# and then convert to XML
$strParameters = Get-TaskParameters 
$xmlParameters = [xml]$strParameters

Now you can set your parameters to the scheduled task parameters like this

$emailTo       = [string]$xmlParameters.parameters.emailTo 

In my script function Get-TaskParameters I wrap these commands inside a try / catch clause. The script first tries to get the intrinsic object and falls back, using the catch clause to using the second method. As long as the $script:TaskDN is valid the function will successfully return the task parameters.

Some of my script use a lot of variables which I expose as ARS scheduled task parameters.  I always add defaults into the script so I can debug the script when writing it. I also set a flag so I know if any of the parameters were defaults rather than values taken from the scheduled task itself.  

$defaultUsed = $false # flag used to detect if all parameters were defaults
if ( ($emailTo -eq $null) -or ($emailTo.Length -le  1) ) { 
 $emailTo = "lee.andrews@myDomain.com" ; $defaultUsed = $true 
} 

In theory I don’t need to specify any parameters in the scheduled task unless I subsequently want to change the script behavior by using different task parameters. However I now have to remember which parameters I can add to the task to change it’s behavior, which in a years time may not be so obvious to me. That’s probably more like a few hours before I forget 😦  

A better way would be to copy each parameter from the script to the scheduled task but this is error prone and time consuming and as ‘scripters’ aren’t we all inherently lazy? OK maybe that’s not the best way of putting that, we like to speed things up with automation!

What if the script could update it’s own parameters on the first run or any run for that matter, how cool ( to us nerds ) would that be?

This idea had originally come to me because I had a task where one of the parameters was a group name and I thought, “what if someone changes the group name”.  I then thought “if I use the GUID of the group that couldn’t change!” Adding the group GUID as a parameter would be a nightmare so the solution was, in my mind at least, to get the script on each run to read the group name and then update the GUID in the task parameters if it needed to. I Thought I’d already posted this but it seems I can’t find it so I’ll add that as my next post.

Here is a modified version of my Get-TaskParameters function. If the scheduled task has no parameters then  
$strParameters = $Task.DirObj.Get(‘edsaParameters’) triggers the catch clause and then it uses the $script:taskDN variable to use the second method to get the parameters.

The $script:taskDN variable is used when debugging but it will fail if you reorganise your scheduled tasks. My ideal is for variable to only be required if we are debugging otherwise I want the task to dynamically figure out where it is using the intrinsic object. I’m writing more robust code if it can handle renames and container moves without modification.

If when my script runs it detects that some or all of the parameters were defaults, i.e. the task does not contain the parameter then my script creates a hash table of parameters and calls ‘Set-TaskParameters’.

Here’s all the code to do this including the new function Set-TaskParameters

function Get-TaskParameters {
 try {
  $Task.DirObj.GetInfo() 
  #  if no parameters are specified this will trigger the catch clause
  # we set the $Script:taskDNDefault dynamically in case we invoke catch
  $Script:taskDN         = $($Task.DirObj.Get('distinguishedName')) 
  $Task.DirObj.GetInfoEx(@('edsaParameters'),0)  
  $strParameters         = $Task.DirObj.Get('edsaParameters')
 }
 catch {
  try {
   $Task = Get-QADObject -Identity $script:TaskDN  `
                         -IncludedProperties edsaParameters,edsaModule `
                         -Proxy 
   $strParameters        = $Task.edsaParameters
  }
  catch {
   return 'Unable to get task parameters'
  }
 }
 $strParameters = '<parameters>' + $strParameters + '</parameters>' 
 return $strParameters
}

function Set-TaskParameters {
 param (
  $htParameters,
  $taskDN,
  $connection 
 )
 $htParameterList = @()
 foreach ( $htParameter in $htParameters.GetEnumerator() ) {
  $htParameterList += ` $('<'+$htParameter.key+'>'+$htParameter.value+'</'+$htParameter.key+'>')
 }
 Set-QADObject taskDN -ObjectAttributes @{edsaParameters=$htParameterList} `
                      -Connection $connection | Out-Null 
}

#here is example calling code 
$strParameters = Get-TaskParameters 
if ( $strParameters -eq 'Unable to get task parameters' ) {
 $subject = "$hostName $scriptName Script ERROR - Unable to get PARAMETERS"
 $emailParameters.Add('body',"$hostName  Line  $($error[0].InvocationInfo.ScriptLineNumber) - FATAL ERROR: Failed to get  script parameters")
 $emailParameters.Add('Subject',$subject)
 Send-MailMessage @emailParameters -Encoding ([System.Text.Encoding]::UTF8)
 throw $subject
}

$xmlParameters   = [xml]$strParameters
$emailTo         = [string]$xmlParameters.parameters.emailTo     
$emailFrom       = [string]$xmlParameters.parameters.emailFrom     
$operationReason = [string]$xmlParameters.parameters.operationReason
#Region Set Parameter Defaults
$defaultUsed = $false 
if ( ($emailTo -eq $null) -or ($emailTo.Length -le  1) ) { 
 $emailTo = "lee.andrews@MyDOmain.com" ; $defaultUsed = $true } 
if ( ($emailFrom -eq $null ) -or ($emailFrom.Length -le  1) ) { 
 $emailFrom = $scriptOwner ; $defaultUsed = $true } 
if ( ($operationReason -eq $null) -or ($operationReason.Length -le  1) ) {   $operationReason = $script:scriptName ; $defaultUsed = $true } 
# now check if we need to update the task parameters
if ( $defaultUsed ) {
 $taskParameters = @{
  'emailTo'         = $emailTo;
  'emailFrom'       = $emailFrom;
  'operationReason' = $operationReason;
 }
 Set-TaskParameters -htParameters $taskParameters `
                    -taskDN $script:taskDN `
                    -connection $proxy  
}

 
So your first run will automatically set all the parameters to the default values.

Is that neat or what? OK, OK I know I need to get out more 🙂
 

 

Listing the system timezones

I’ve just not had any time to post recently but I needed to look up the timezones and I knew I’d done this before but could I find it on my blog…. No.  So here it is a nice simple 1 liner.

[System.TimeZoneInfo]::GetSystemTimeZones() | select standardName,daylightName,DisplayName

and you’ll get this out….

timezones

I have some more blogs to follow when I have time as I was recently working on an ARS policy which extends my idea of dynamic ARS scheduled task parameters.  So for example where you configure a group name in your scheduled task and then rename the group.  I came up with a way of dynamically fixing this, so hopefully I can post this soon.

Date and Time Formatting

I’ve made a few posts about the hassles of dates and time – I just wish we could all agree on a format and stick to it.

If you are dealing with dates in Powershell in a production script where the script and the updates it makes are viewed globally then you have my sympathy.

Hopefully this post will help you solve your problems.

Dealing with dates is awkward because everybody likes to have their own system for writing down dates.  Our cousins in the US use MM/DD/YYYY and we use DD/MM/YYYY.  Other countries use different separators like the period ( Germany ) or a hyphen (Canada).

This is before we get into the different ways to display a date, try this command at a PoSH prompt:

(Get-Culture).DateTimeFormat

This lists all the ways that a date can be displayed.

dateformats

This usually isn’t a problem when getting dates out of an AD attribute as the data is stored as UTC and is in a standard format.  When you extract the information and display it the system does the conversion for you and displays it using the ‘culture’ of your host operating system.

What your OS is cultured?

You can find out what date and time formats your PC is currently using at a PoSH prompt

A cultured PoSH prompt?

$(Get-Culture).Name

In the UK this will return en-GB for English – Great Britain.

Anyway the problem is when you start extracting date strings from CSV files.  Now you need to know if the string 12/02/2016 is the 12th of February or the 2nd of December.

There is no fix for this by the way you just have to know!  You could parse the whole file and just check your assumption that the dates are in UK or US format but there is no guarantee that there are any date strings that violate either assumption.  Any date string that does not have a number above 12 will pass both tests.  Only days 13 and above will reveal the formatting.

Right so lets assume we know that the file has dates in US format.  For our US cousins they can now stop reading as there is nothing for you to do, unless of course the date stings in your file are UK format, then you have exactly the same issue as your UK counterpart.

Excel gets it wrong too so don’t feel bad for your powershell code

Did you know Excel also makes assumptions based on your regional settings and you know what happens when you assume something.     Yup excel will make a complete mess of your data!  Try it if you don;t believe me!  Anyway back to PowerShell.

Converting dates

I’m only going to cover, for now at least the conversion between UK and US dates.  The same principles apply for any conversion.  You need to know the format in the file and the ‘culture’ of the system processing it.  Why do I need the ‘culture’?  Because this can actually be different for the user of any system so rather than assume what it is use Get-Culture and this way your script should in theory work on any system using any date ‘culture’.

These are not the droids you are looking for

In converting my date strings to match my system date I have 3 possible states.

  1. The date string in the file matches my system ‘culture’
  2. The date string is in US and my system is UK – convert from US to UK
  3. The date string is in UK and my system is in US – convert from UK to US

Once we know which action we need to do you just use a substring statement to rearrange the date string.

Oh one other thing I’m assuming we are using a 4 digit year in this example and every day  and month is using 2 characters.

if ( ( ( $dateFormat -eq “US” ) -and ( $(Get-Culture).Name -eq “en-US” ) ) -or ( ( $dateFormat -eq “GB” ) -and ( $(Get-Culture).Name -eq “en-GB” ) ) )

# leave as is

{$Users = Import-Csv $File -Encoding “UTF7” | Select-Object ‘ID’,’Date’}

elseif ( ( $dateFormat -eq “US” ) -and ( $(Get-Culture).Name -eq “en-GB” ) )

{$Users = Import-Csv $File -Encoding “UTF7” |
Select-Object ‘ID’,@{Name=’Date’;Expression={“$($_.’Date’.substring(3,2))/$($_.’Date’.substring(0,2))/$($_.’Date’.substring(6,4))”}}}

elseif ( ( $dateFormat -eq “GB” ) -and ( $(Get-Culture).Name -eq “en-US” ) )
{$Users = Import-Csv $File -Encoding “UTF7”  |
Select-Object ‘ID’,@{Name=’Date’;Expression={“$($_.’Date’.substring(0,2))/$($_.’Date’.substring(3,2))/$($_.’Date’.substring(6,4))”}}}

I think I got that right 🙂  anyway all you need to do is to split up that date sting to suit your culture.  The code above shows you how so now you should be able to write your own conversions.

 

Cool feature in Out-GridView

I came across this post the other day and was wowed by the new feature in powershell 3 – I say new, new to me as the post was in 2013.  It just goes to show when a new version of powershell comes out you should always search for posts on new features they added.

https://mcpmag.com/articles/2013/01/08/pshell-gridview.aspx

The post discusses the new out-gridview switches and specifically you can now pass information back to the calling command which means you can select stuff from the list and then do stuff with the returned rows.  This could save you literally hours of coding and might even make you look like a god when you write such cool code in seconds.  OK I may be going over the top a little there 🙂

If you have never used Out-GridView then now is a good time to check it out and run some of the examples in the post above.

 

 

Updating users attributes when adding a user to a group

I had a request the other day to update the attributes for a group of users.  The fax solution used required some user attributes to be updated for fax to be enabled.  Now you’d think that there would be a console to do this but apparently I’m told this isn’t the case.  The messaging team manually update the attributes to enable fax which is a bit of a nightmare, especially if you are using AD Users and computers.  Did you know the attribute editor is only available if you walk the OU structure and actually click ont eh user object!  If you search for the user then the attribute editor is not there which is crazy.  I think it’s one of those Microsoft “Features”.

Our AD schema was extended when the fax software was installed, this this seems to scare a lot of people but it actually, if done correctly makes a lot of sense but I’m getting away from the point of this blog post so lets get back to it.

In this scenario I want to set some user attributes when a user is added to a group.

ARS can do this easily using a script policy.  In short all I need to is this:

  • Identify that the Target group has been modified
  • Check that the modification was to the group membership
  • if a user was added then set the attributes on that user
  • if the user was deleted then clear the attributes.

The script will use the usual best practice function to detect if the member attribute has changed and then we can iterate through the properties of the $request object until we get to the member attribute.

for ($i = 0; $i -lt $Request.PropertyCount; $i++) {
$item = $Request.Item($i)
$Name = $item.Name
if ($Name -eq “member”) {

So now I’ve figured out that the target group has been modified ( as the policy is only linked to the one group or the group is in an OU that’s in scope of the ARS script policy and I’ve also confiremd that the member attribute was modified.

I can use the control code to determine if the user was added or deleted using the two constands shown below – these are all in the SDK by the way but I didn’t find them there this is just taken from an example script I came across years ago.

if ( ($item.ControlCode -eq $Constants.ADS_PROPERTY_APPEND ) -or  ($item.ControlCode -eq $Constants.ADS_PROPERTY_DELETE ) ) {

Because I want to be able to change the attributes that are updated or possibly apply the same logic to another group but for another reason, i.e. a different set of attributes to update I’ve used a multivalued attribute as a parameter.  After messing around for a while I cam up with this little snippet of code to create a hashtable variable called $attributes and add the attributes set in the script parameter to it.

$command = ‘$Attributes’+” = @{$AttributeList}”
Invoke-Expression $command

At this oint I already have the DN of the object as that’s whats stored in the member attribute but I only want to update the attributes on a user object so I need to check this and the only way of doing that is to bind to the object and check it’s type.

$newMember = Get-QADObject -Proxy -Identity $v

if it’s a user and we are adding the user to the group then set the attributes

Set-QADUser -identity $v -objectAttributes $Attributes -Proxy

if we are clearing the attributes then first we need to clear all the attribute values in our hashtable:

foreach($key in $($Attributes.keys)){ $Attributes[$key] = “” }

Here’s the full script including all the helper library functions:

##################################################################################################
#
# Group-UpdateUserAttributes
# The script will update the attributes, adding or removing them when you add or remove a users from a targeted group
# Version 1.0
#
# Version 1.0 - Initial Script Version
##################################################################################################
#region Helper Functions
function onInit($Context) {
$par01 = $context.AddParameter("debugging")
$par01.MultiValued = $false
$par01.PossibleValues = "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"
$par01.DefaultValue = "1"
$par01.Description = "Debugging EventLog Level where: 0 is no debugging; 9 is the most verbose.  NOTE: The script will email logs to the 'scriptOwner' if 'sendDiagEmails' is set to 'Yes'"
$par01.Required = $false
#
$par02 = $context.AddParameter("scriptOwner")
$par02.MultiValued = $false
$par02.DefaultValue = "lee.andrews@myDomain.com"
$par02.Description = "Debugging ERROR Events are sent to the 'scriptOwner' - if 'sendDiagEmails' is set to 'Yes' all events are emailed to the 'scriptOwner'"
$par02.Required = $false
#
$par03 = $context.AddParameter("SMTPServer")
$par03.MultiValued = $false
$par03.DefaultValue = "smtp.mydomain.com"
$par03.Description = "SMTP Server Address"
$par03.Required = $false
#
$par04 = $context.AddParameter("operationReason")
$par04.MultiValued = $false
$par04.DefaultValue = "Script Policy Group-UpdateUserFaxAttributes"
$par04.Description = "ARS Operation Reason - This is a message added to the change history"
$par04.Required = $false
#
$par05 = $context.AddParameter("sendDiagEmails")
$par05.MultiValued = $false
$par05.PossibleValues = "Yes","No"
$par05.DefaultValue = "No"
$par05.Description = "If set to 'Yes' the script will email any messages sent to the 'OutputDebugString function' to the 'scriptOwner'"
$par05.Required = $false
#
$par06 = $context.AddParameter("AttributeList")
$par06.MultiValued = $True
$par06.Description = "If set to 'Yes' the script will email any messages sent to the 'OutputDebugString function' to the 'scriptOwner'"
$par06.Required = $false
#
}
function Get-CurrentLineNumber {
$MyInvocation.ScriptLineNumber
}
function IsAttributeModified ($strAttributeName, $Request) {
# function used to detect if a specific attribute was modified as part of the object update
# $strAttributeName = the LDAP attribute name to check
# $Request = the object modified
$objEntry = $Request.GetPropertyItem($strAttributeName, $Constants.ADSTYPE_CASE_IGNORE_STRING)
if ($objEntry -eq $null) { return $false}
$nControlCode = $objEntry.ControlCode
if ($nControlCode -eq 0) { return $false }
return $true
} # end function
function OutputDebugString {
param (
  [int]$verbosity,
  [string]$str,
  [string]$lineNumber
 )
#region Intialise Script Variables
#endregion Intialise Script Variables
 # outputs debug info to the EDM event log
if ( [string]$PolicyEntry.Parameter("debugging") -ne '0' ) {
  $strDebuggingSwitch = [string]$PolicyEntry.Parameter("debugging")
  $sendDiagEmails = [string]$PolicyEntry.Parameter("sendDiagEmails")
  $str = "Line Number: $lineNumber - $str"
  if ( $verbosity -le [int]$strDebuggingSwitch ) {
   $EventLog.ReportEvent(2,$str)
   if ( $sendDiagEmails -eq "Yes" ) {
    $scriptOwner     = [string]$PolicyEntry.Parameter("scriptOwner")
    $SMTPServer      = [string]$PolicyEntry.Parameter("SMTPServer")
    Send-MailMessage -to $scriptOwner -From $scriptOwner -Subject "Line Number: $lineNumber : Server $($env:COMPUTERNAME)" -Body $str -SmtpServer $SMTPServer
   }
  }
 }
} # end function
function Get-Value($obj, $attr) {
trap {  continue  }
return $obj.Get($attr)
return $null
} # End Function Get-Value
#endregion Helper Functions
#region Event Handlers
function onPostModify($Request) {
OutputDebugString -verbosity 8 -str "Group-UpdateUserAttributes CALLED : $($Request.name)" -linenumber $(Get-CurrentLineNumber)
 if ($Request.class -ne "group") { return } # if it's not a group we don't care
OutputDebugString -verbosity 7 -str "Group-UpdateUserAttributes Group Modified : $($Request.name)" -linenumber $(Get-CurrentLineNumber)
# check that a member was added - or removed
if ( ! ( $(IsAttributeModified -strAttributeName 'member' -Request $Request) ) ) { return } # if the group members were not updated we don't care
OutputDebugString -verbosity 6 -str "Group-UpdateUserAttributes Group Member Modified : $($Request.name)" -linenumber $(Get-CurrentLineNumber)
 #region Intialise Script Variables
$scriptOwner     = [string]$PolicyEntry.Parameter("scriptOwner")     # used to email debug messages
$SMTPServer      = [string]$PolicyEntry.Parameter("SMTPServer")      # the SMTP Server
 $operationReason = [string]$PolicyEntry.Parameter("operationReason") # Operation reason will be used to update the ARS History
 $AttributeList   = [string]$PolicyEntry.Parameter("AttributeList")   # THe list of attributes to update
#endregion Intialise Script Variables
 for ($i = 0; $i -lt $Request.PropertyCount; $i++) {
  $item = $Request.Item($i)
  $Name = $item.Name
  # enumerate the objects attributes until we locate the "member" attribute
  if ($Name -eq "member") {
   # check that the member attribute was modified
   if ( ($item.ControlCode -eq $Constants.ADS_PROPERTY_APPEND ) -or  ($item.ControlCode -eq $Constants.ADS_PROPERTY_DELETE ) ) {
    $command = '$Attributes'+" = @{$AttributeList}"
    Invoke-Expression $command
    $action = "APPEND"
    if  ($item.ControlCode -eq $Constants.ADS_PROPERTY_DELETE ) {
     $action = "DELETE"
     # clear the attribute values
     foreach($key in $($Attributes.keys)){
      $Attributes[$key] = ""
     }
    }
    # iterate through the group members added or removed
    foreach ($v in $item.Values) {
     try { Remove-Variable -Name newMember -ErrorAction SilentlyContinue -WarningAction SilentlyContinue } catch {}
     $failedGet = $false
     try { $newMember = Get-QADObject -Proxy -Identity $v  } catch { $failedGet=$true }
     if ( ( $newMember -eq $null ) -or ( $newMember.type -ne "user" ) ) { Return } # nothing to to this script only updates users
     try { Set-QADUser -identity $v -objectAttributes $Attributes -Proxy }
     catch {
      # add error code here
      Send-MailMessage -to $scriptOwner -from $scriptOwner -subject "TEST" -Body "TEST value of v =  $v `n action = $action " -SmtpServer $SMTPServer
     }
    } # foreach ($v in $item.Values) {
   } # if ( ($item.ControlCode -eq $Constants.ADS_PROPERTY_APPEND ) -or  ($item.ControlCode -eq $Constants.ADS_PROPERTY_DELETE ) ) {
  } # if ($Name -eq "member") {
 } # for ($i = 0; $i -lt $Request.PropertyCount; $i++) {
}

If you are not sure how to link this into ARS then my next post will show you how.