AutomaShell

  • TwitterTwitter
  • LinkedInLinkedIn
  • RSS FeedRSS Feed
  • Home
  • About
  • Contact
  • Troy
  • June 6, 2013
  • 1

User Termination (Part 2)

In this 2 part series I will show you how to build an automated and audit-friendly user termination process. In Part 1, I showed you how to get the users into a centralized location and execute basic user termination actions. For Part 2, we will fully automate the process of the user being deleted from the organization, while backing up their email, home directories, and providing email logs.

You can find Part 1 here.

Disclaimer: This script could have a very negative impact on your organization. As such, I am not responsible for any damage that may come from using this script. You should take great care when implementing anything that will be destroying/altering data without interaction.

Addons / Things to know

  • buy genuine Seroquel online !Important: You will need DotNetZip to use this script properly. You can find the dll that we’ll use here: Ionic.Zip.dll
  • An Exchange account will need to be created for mailflow purposes.
  • Create a second OU for termed users who have encountered an error. If errors occur, a person will need to correct them.

Scenario

HR has submitted the termination request for the user Test User. In Part 1 we:

  • Removed the user from any groups
  • Updated the description field with a note (for us, we will use TERMED: mm/dd/yyyy)
  • Moved the user to a termed user OU
  • Hid the user from the GAL

In Part 2, our script (not us!) will do the following:

  • Move the Mailbox to a terminated user Mailbox Database
  • Export the active Mailbox and Personal Archive PST
  • Zip up and move the user’s Home Directory
  • Check our work, and mark users who are ready for removal
  • Remove the users from Active Directory
  • Clean up previous processes

We will use the following functions:

  • Zip
  • ConvertToDate
  • MoveMailbox
  • ExportMailbox
  • ArchiveHomeDirectory
  • PendingRemoval
  • RemoveObject
  • LoadModules
  • CleanUp

And we’re off.

Variables, Modules, and Parameters

Before we can get into the meat and potatoes, we need to define some variables and load some modules.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#Import Modules
param ([string]$function)
[void][System.Reflection.Assembly]::LoadFrom("C:\\scripts\\ScheduledTasks\\bin\\Ionic.Zip.dll")
[Reflection.Assembly]::LoadWithPartialName(”System.Web”) | out-null
 
# Modifiable Variables
$global:terminated_days = "30"
$global:from_email = "HelpDesk@domain.local"
$global:to_email = "system.administrators@domain.local"
$global:smtp_server = "relayserver.domain.local"
$global:zip_password = "ZipPassword!"
$global:termed_ou = "All Termed Users"
$global:domain = "domain.local"
$global:termed_mailbox_db = "TERMED"
$global:blockmailsender = "blockmailuser"
$global:archivelocation = "\\share\archive\"
 
# Static Variables (do not change)
$global:purge_date = (get-date).AddDays(-$terminated_days)
$global:date = (get-date).ToString("MM/dd/yyy")
$split = $domain.Split('.')
$global:shortdom = $split[0]
$global:shortdom2 = $split[1]

To begin, our only parameter is $function. This allows us to run the script and define a specific function. We then load our Ionic.Zip.dll (DotNetZip) and System.Web (we’re going to use it to generate a hashtag). The DLL that you downloaded earlier should be placed in C:\\scripts\\ScheduledTasks\\bin\\.

Variables

  • Customiziable variables. Change these to reflect your organization.
    • $terminated_days – The amount of days that must pass since this user was terminated before we can being our process
    • $from_email – Who the email will come from
    • $to_email – Who the email will be sent to
    • $smtp_server – The relay server. You can DNS or IP
    • $zip_password – The password we’ll use to “secure” our zip files
    • $termed_ou – The OU where terminated users are placed
    • $domain – Your internal domain name
    • $termed_mailbox_db – Termed user mailbox database
    • $blockmailsender – This user will be used to stop mail flow to a specific user once their PST has been exported
    • $archivelocation – Share location where PST’s and homedirectories will be placed
  • Static Variables. Do not change these variables.
    • $purge_date – Subtract the $terminated_date from today’s date
    • $date – Today’s date
    • $split – Split $domain into an array
    • $shortdom – The NetBIOS name of your domain (hopefully)
    • $shortdom2 – Top level domain (local)

Function: Zip [$Path, $OutputPath, $user]

This function will zip up our HomeDirectories.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#### Desc: Zip directories ####
function Zip($Path,$OutputPath,$user) {
$hashinfo = $null;
for($a  = 1; $a -ne 10) {
$hashinfo = "$hashinfo`n$([System.Web.Security.Membership]::GeneratePassword(128,0))"
$a++
}
 
$directoryToZip = "$Path";
$zipfile =  new-object Ionic.Zip.ZipFile;
$e= $zipfile.AddEntry("README.txt", "$user archived on $date")
$e= $zipfile.AddEntry("hash.txt", "$hashinfo")
$e= $zipfile.AddDirectory($directoryToZip, "$user")
$zipfile.Encryption = [Ionic.Zip.EncryptionAlgorithm]::WinZipAes256
    $zipfile.Password = "$zip_password"
$zipfile.UseZip64WhenSaving = [Ionic.Zip.Zip64Option]::Always
$zipfile.Save("$OutputPath\$user.zip")
$zipfile.Dispose();
}

First we empty out $hashinfo, then with a foreach loop we create a unique hash for this zip. We then add README.txt with the info $user archived on $date. This allows us to know when this user’s HomeDirectory was specifically archived. Add in hash.txt so that our zip is unique. I implore you to read more about DotNetZip for information about what else is happening within this function. DotNetZip is a good tool to have in your arsenal of powershell scripts.

Variables for this function

  • $Path – Path to the file that will be zipped
  • $OutputPath – Where our zip will be dumped once it has been processed
  • $user – The username of the user who is being terminated

Function: ConvertToDate [$STR]

This function will convert the string date that we get from the user’s description field into something we can use. ** Note: I did not write this function, but I cannot find the source. If you did, or know who did create this, please contact me so I can provide proper credit! **

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#### Desc: Convert AD description to a usable date ####
function ConvertToDate($STR) {
switch -regex ($STR) {
"^\d{2}/\d{2}$" {[DateTime]::ParseExact($STR,"MM/yy",$null)}
"^\d{1}/\d{2}$" {[DateTime]::ParseExact($STR,"M/yy",$null)}
"^\d{2}/\d{2}/\d{2}$" {[DateTime]::ParseExact($STR,"MM/dd/yy",$null)}
"^\d{1}/\d{2}/\d{2}$" {[DateTime]::ParseExact($STR,"M/dd/yy",$null)}
"^\d{2}/\d{1}/\d{2}$" {[DateTime]::ParseExact($STR,"MM/d/yy",$null)}
"^\d{1}/\d{1}/\d{2}$" {[DateTime]::ParseExact($STR,"M/d/yy",$null)}
"^\d{2}/\d{2}/\d{4}$" {[DateTime]::ParseExact($STR,"MM/dd/yyyy",$null)}
"^\d{1}/\d{2}/\d{4}$" {[DateTime]::ParseExact($STR,"M/dd/yyyy",$null)}
"^\d{2}/\d{1}/\d{4}$" {[DateTime]::ParseExact($STR,"MM/d/yyyy",$null)}
"^\d{1}/\d{1}/\d{4}$" {[DateTime]::ParseExact($STR,"M/d/yyyy",$null)}
"^\d{2}/\d{4}$" {[DateTime]::ParseExact($STR,"MM/yyyy",$null)}
"^\d{1}/\d{4}$" {[DateTime]::ParseExact($STR,"M/yyyy",$null)}
default { $STR = "N/A"}
}
}

Please read this TechNet article for more information about regex.

Function: MoveMailbox

This function will find any users that are in the Termed Users OU and move them to the Termed users mailbox database.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#### Desc: Move all mailboxes located in "All Termed Users" to the proper termed Mailbox Database ####
function MoveMailbox {
$results = (Get-ADUser -filter * -SearchBase "OU=$termed_ou,DC=$shortdom,DC=$shortdom2" -properties * -ea stop) | Sort Name
$count = 0;
foreach($result in $results) {
$sam = ($result.SamAccountName)
$database = Get-Mailbox $sam -ea stop
if($database.Database -ne "$termed_mailbox_db") {
New-MoveRequest -identity $sam -TargetDatabase "$termed_mailbox_db" -BadItemLimit "50" -ea stop
$count++
$move_mailboxes = "$move_mailboxes$sam<br>"
}
}
 
# Email admin information
if($count -ne 0) {
Send-MailMessage -to "$to_email" -from "$from_email" -subject "User Termination Report - Move Mailboxes" -body "<font face=verdana size=2><b>Mailboxes Moved:</b><br><br>$move_mailboxes</font>" -smtpServer "$smtp_server" -BodyAsHtml -ea stop
}
}

First we need to get a list of users in the Termed Users OU. We do this by using -SearchBase. We will do this for nearly every function from here on out, so I will not cover it beyond this point. We need to set our $count variable to 0 in order to know if we should send an email (was anything done).

Within the foreach loop, we are generating the following info and executing the following commands.

  • $sam – Get the user’s SamAccountName
  • $database – Get the user’s mailbox
  • if $database.Database does not equal “$termed_mailbox_db” (the one defined by you in the variables), then do the following
    • Execute New-MoveRequest for the user $sam with a Target Database of $termed_mailbox_db. Allow up to 50 items to be corrupted.
      • You certainly do not have to allow any bad items; however, in my experience this will only cause issues with your automation. It is acceptable within our organization to lose 1-2 emails for every 10,000 emails.
    • $count++ is saying make $count whatever it was, plus 1.
    • $move_mailboxes is for our logging purposes. If this is the first time the loop has run through, then $move_mailboxes is empty and only the $sam is captured.

Now that our loop is complete, we should send an email to the administrator(s) and let them know who has been moved.

We will reuse many of the same commands / variables (although we will redefine them) for future functions, but I will not continue to explain them.

Function: ExportMailbox

This function will export a user’s active mailbox and personal archive PST.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#### Desc: Export Mailbox and Archive to PST ####
function ExportMailbox {
$results = (Get-ADUser -filter * -SearchBase "OU=$termed_ou,DC=$shortdom,DC=$shortdom2" -properties *) | Sort Name
$count = 0
$blockmail = (Get-ADUser $blockmailuser -properties *).CanonicalName
foreach($result in $results) {
if($result.Description) {
$termed = ConvertToDate ($result.Description).Replace("TERMED: ", "")
if($termed -le $purge_date) {
$sam = ($result.SamAccountName)
$database = Get-Mailbox $sam
if($database.Database -eq "$termed_mailbox_db") {
Set-Mailbox -AcceptMessagesOnlyFromSendersOrMembers "$blockmail" -RequireSenderAuthenticationEnabled $true -Identity $sam
New-MailboxExportRequest -Mailbox $sam -FilePath $archivelocation\$sam.pst -BadItemLimit 50
if((Get-Mailbox -identity $sam).ArchiveDatabase -ne $null) {
New-MailboxExportRequest -Mailbox $sam -FilePath $archivelocation\$sam.archive.pst -IsArchive -BadItemLimit 50
$archive_mailboxes = "$archive_mailboxes$sam<br>"
}
$count++
$regular_mailboxes = "$regular_mailboxes$sam<br>"
}
}
}
}
 
# Email admin information
if($count -ne 0) {
Send-MailMessage -to "$to_email" -from "$from_email" -subject "User Termination Report - Export PST's" -body "<font face=verdana size=2><b>Mailboxes Exported:</b><br><br>$regular_mailboxes<br><b>Archives Exported:</b><br><br>$archive_mailboxes</font>" -smtpServer "$smtp_server" -BodyAsHtml
}
}

The $blockmail variable is collecting the CanonicalName for the user $blockmailuser. This is needed to change the -AcceptMessagesOnlyFromSendersOrMembers parameter within Set-Mailbox.

This foreach loop is completing the following tasks:

  • $termed is executing the ConvertToDate function on the user’s Description field. We must do this so we can compare today’s date with the users “TERMED” date.
  • If $termed is less than or equal to $purge_date
    • $sam – Collect the user’s SamAccountName
    • $database – Get $sam user’s mailbox
    • If $database.Database equals our $termed_mailbox_db
      • Prevent the user from receiving any new email
      • Create a New-MailboxExportRequest which will export the PST to $archivelocation\$sam.pst
      • If our mailbox has a personal archive
        • Create another New-MailboxExportRequest to export this user’s personal archive to $archivelocation\$sam.archive.pst. We use .archive.pst to differentiate their active mailbox and their archived mailbox. The -IsArchive switch tells PowerShell that we are interested in exporting this user’s Personal Archive.
        • $archive_mailboxes is used for our email logs

Once we’re done with our loop, we fire off the email.

Function: ArchiveHomeDirectory

This function will zip up and move the user’s HomeDirectory. The Home Directory is a property within Active Directory. Common Home Directories are U:\ or \\Share\Users\tuser.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#### Desc: Move all data located in a user's HomeDirectory to a encrypted zip ####
function ArchiveHomeDirectory {
$results = (Get-ADUser -filter * -SearchBase "OU=$termed_ou,DC=$shortdom,DC=$shortdom2" -properties *) | Sort Name
$count = 0
foreach($result in $results) {
if($result.Description) {
$termed = ConvertToDate ($result.Description).Replace("TERMED: ", "")
if($termed -le $purge_date) {
$sam = ($result.SamAccountName)
if($result.HomeDirectory) {
$hd = $result.HomeDirectory
if(test-path $hd) {
zip "$hd" "$archivelocation\UserShares" "$sam"
$archived_HD = "$archived_HD$sam<br>"
$count++;
} elseif(!(test-path "$hd" -ea silentlycontinue) -and !(Get-ACL $hd -ea silentlycontinue)) {
$aclerror = "$aclerror Cannot access $hd<br>"
Move-ADObject -identity $result.distinguishedname -TargetPath "OU=$termed_ou (errors),DC=$shortdom,DC=$shortdom2"
$count++;
}
}
}
}
}
 
# Email admin information
if($count -ne 0) {
Send-MailMessage -to "$to_email" -from "$from_email" -subject "User Termination Report - Archive Home Directories" -body "<font face=verdana size=2><b>Home Directories Archived:</b><br><br>$archived_HD<br><b>Errors:</b><br><br>$aclerror</font>" -smtpServer "$smtp_server" -BodyAsHtml
}
}

The similarities between this function and ExportMailbox are very similar; however, here we are working with NTFS files and not mailboxes. Instead of using New-MailboxExportRequest, we’re going to use our own function Zip. We use Test-Path to test if the HomeDirectory listed actually exists, and Get-ACL to test for access. If the folder doesn’t exist, or we do not have permissions, we’ll create an error to send to the admin. The zip files are placed within the share you defined earlier.

Halfway there…

At this point we are halfway done. The script has moved the user’s mailboxes, exported their production mailbox and personal archive to PST, and zipped up and moved their home directory. The following functions will check all of our work and make sure a user is ready for removal, and if everything checks out, remove the AD object and HomeDirectory from our file share. Then all we need is a little cleanup and it’s rinse and repeat.

*Deep breath* Okay, continue when ready.

Function: PendingRemoval

This function is going to check all of our work.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#### Desc: Remove Object & cleanup folders ####
function PendingRemoval {
$results = (Get-ADUser -filter * -SearchBase "OU=$termed_ou,DC=$shortdom,DC=$shortdom2" -properties *) | Sort Name
$count = 0
foreach($result in $results) {
if($result.Description -like "TERMED*") {
$termed = ConvertToDate ($result.Description).Replace("TERMED: ", "")
if($termed -le $purge_date) {
$sam = ($result.SamAccountName)
$hd = $result.HomeDirectory
 
#Has a PST been created?
if((test-path "$archivelocation\$sam.pst")) {
if((Get-Item "$archivelocation\$sam.pst").length/1KB -gt 264) {
$pst = $true
#$sam PST exists.
} else {
$pst = $false
$pendingremoval_error = "PST does not exist (size)"
}
} else {
$pst = $false
$pendingremoval_error = "PST does not exist"
}
 
#Has the HomeDirectory been backed up? Does this user even have a HomeDirectory?
if((test-path "$archivelocation\UserShares\$sam.zip")) {
if((Get-Item "$archivelocation\UserShares\$sam.zip").length/1KB -gt 1) {
$share = $true
}
$share = $true
} elseif(!($result.HomeDirectory)) {
$pendingremoval_error = "$pendingremoval_error - No Home Directory"
$share = $true
} else {
if(!(test-path "$hd" -erroraction silentlycontinue) -and !(Get-ACL $hd -erroraction silentlycontinue)) {
$pendingremoval_error = "$pendingremoval_error - Cannot access $hd"
$share = $false
}
$share = $false
$pendingremoval_error = "$pendingremoval_error - Home Directory exists, but no archive has been created"
}
 
#Parse results from PST and HomeDirectory. If success, update AD with "REMOVAL PENDING" desc. If failure, write to eventlog what has failed.
if($pst -eq $true -and $share -eq $true) {
Set-ADUser -identity $sam -description "REMOVAL PENDING" -enabled $false
$set_success = "$set_success$sam<br>"
} else {
$set_failure = "$set_failure$sam - $pendingremoval_error<br>"
}
 
$count++
}
}
}
 
# Email admin information
if($count -ne 0) {
Send-MailMessage -to "$to_email" -from "$from_email" -subject "User Termination Report - Pending Removal" -body "<font face=verdana size=2><b>User's ready for removal:</b><br><br>$set_success<br><b>Errors:</b><br><br>$set_failure</font>" -smtpServer "$smtp_server" -BodyAsHtml
}
}

While this function may appear quite long and may be daunting, it’s all very basic logic. Has a PST been created? Has the HomeDirectory been backed up? If those two things are true, then set the user as ready for deletion.

Function: RemoveObject

Alright… 213 lines of PowerShell just to get to this moment… The moment we delete the user!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#### Desc: Remove Object & cleanup folders ####
function RemoveObject {
$results = (Get-ADUser -filter * -SearchBase "OU=$termed_ou,DC=$shortdom,DC=$shortdom2" -properties *) | Sort Name
$count = 0
foreach($result in $results) {
if($result.Description -eq "REMOVAL PENDING") {
if(PendingRemoval) {
$sam = ($result.SamAccountName)
$hd = $result.HomeDirectory
Remove-Item -Recurse -Force $hd -confirm:$false
Remove-Mailbox -identity $sam -Permanent $true -confirm:$false
 
$count++
$removed = "$removed$sam<br>"
}
}
}
 
# Email admin information
if($count -ne 0) {
Send-MailMessage -to "$to_email" -from "$from_email" -subject "User Termination Report - Remove Object" -body "<font face=verdana size=2><b>User's removed:</b><br><br>$removed</font>" -smtpServer "$smtp_server" -BodyAsHtml
}
}

What we’re checking for here is if the users (those in the All Termed Users OU) have “REMOVAL PENDING” as their description. If they do, we’re going to check the users again (functions!) and, if everything pans out, delete the user’s Active Directory account and HomeDirectory from the file share.

Function: LoadModules

This script imports the ActiveDirectory module and adds the Exchange Management Shell snap-in.

1
2
3
4
5
#Load modules
function LoadModules {
Import-Module ActiveDirectory
Add-PSSnapin Microsoft.Exchange.Management.PowerShell.E2010
}

If you have read any of my other articles, you have noticed that I always use PowerShell sessions rather than loading modules directly. This is because I want my scripts to work anywhere on the network and have no dependencies on the local machine. However, because we are already having to use a local DLL for this script to function properly, it doesn’t make much since to use remote sessions. Now you’re probably wondering why I have this wrapped up in a function… The reason is that our production script performs basic file moving tasks that don’t require these modules. Because of this, I don’t want the script to waste time loading modules it doesn’t need for certain tasks.

Function: CleanUp

This function will run  prolately before each of our functions to ensure we keep a nice and clean Exchange environment.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#### Desc: Cleanup previous processes ####
function CleanUp {
if(Get-MoveRequest) {
$mr_failure = Get-MoveRequest -MoveStatus Failed
if($mr_failure) {
Send-MailMessage -to "$to_email" -from "$from_email" -subject "User Termination Report - MoveStatus Failures" -body "<font face=verdana size=2><b>Failures</b><br><br>$mr_failure</font>" -smtpServer "$smtp_server" -BodyAsHtml
}
Get-MoveRequest -MoveStatus Failed | Remove-MoveRequest -confirm:$false
Get-MoveRequest -MoveStatus Completed | Remove-MoveRequest -confirm:$false
}
 
if(Get-MailboxExportRequest) {
$mer_failures = Get-MailboxExportRequest | where {$_.status -like "Failed"}
if($mer_failures) {
foreach($mer_failure in $mer_failures) {
$path = $mer_failure.mailbox
Move-ADObject -identity $path.distinguishedname -TargetPath "OU=$termed_ou (errors),DC=$shortdom,DC=$shortdom2"
$write_mer_failure = "$write_mer_failure<br>$mer_failure"
}
Send-MailMessage -to "$to_email" -from "$from_email" -subject "User Termination Report - ExportRequest Failures" -body "<font face=verdana size=2><b>Failures</b><br><br>$write_mer_failure</font>" -smtpServer "$smtp_server" -BodyAsHtml
}
Get-MailboxExportRequest | where {$_.status -like "Failed"} | Remove-MailboxExportRequest -confirm:$false
Get-MailboxExportRequest | where {$_.status -like "Completed*"} | Remove-MailboxExportRequest -confirm:$false
}
}

As you can see, we’re heavy on the Exchange commands here. All we’re really trying to do is report any failures that have happened previously. If there is an issue, the user is moved to our All Termed Users (errors) OU. Here an admin can manually audit these users and see if they are ready to be deleted.

Execute the proper function

Now we can tell the script which function to execute.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#### Desc: Execute proper function ####
$errormessage = $null
try {
if($function -eq "MoveMailbox") {
LoadModules
CleanUp
MoveMailbox
} elseif($function -eq "ExportMailbox") {
LoadModules
CleanUp
ExportMailbox
} elseif($function -eq "ArchiveHomeDirectory") {
LoadModules
CleanUp
ArchiveHomeDirectory
} elseif($function -eq "PendingRemoval") {
LoadModules
CleanUp
PendingRemoval
} elseif($function -eq "RemoveObject") {
LoadModules
CleanUp
RemoveObject
} elseif($function -eq "List") {
LoadModules
List
} else {
$errormessage = "No function was defined."
}
} catch [system.exception] {
$errormessage = $($_.Exception.Message)
} finally {
if($errormessage) {
Send-MailMessage -to "$to_email" -from "$from_email" -subject "User Termination Report - $function" -body "<font face=verdana size=2><b>Error:</b><br><br>$errormessage</font>" -smtpServer "$smtp_server" -BodyAsHtml
}
}

The $function variable is defined by the user when they execute the script (e.g. .\UserTermination.ps1 -function MoveMailbox). We’ve wrapped this within try {} catch {} finally {} so we can catch any errors. And that’s it for the script!

Now we need to set up some scheduled tasks so this truly requires no interaction.

The Scheduled Tasks

These are the scheduled tasks that you’ll need to set up to really benefit from this script.

Program: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe

Mailbox-Export

  • Runs at 7:00 AM every Saturday of every week
  • Add Arguments: -noninteractive -nologo C:\scripts\ScheduledTasks\UserTermination.ps1 “ExportMailbox”

Mailbox-HomeDirectory

  • Runs at 6:00 PM every Friday of every week
  • Add Arguments: -noninteractive -nologo C:\scripts\ScheduledTasks\UserTermination.ps1 “ArchiveHomeDirectory”

Mailbox-Move

  • Runs at 8:00 AM every Wednesday of every week
  • Add Arguments: -noninteractive -nologo C:\scripts\ScheduledTasks\UserTermination.ps1 “MoveMailbox”

Mailbox-PendingRemoval

  • Runs at 8:00 AM every Monday of every week
  • Add Arguments: -noninteractive -nologo C:\scripts\ScheduledTasks\UserTermination.ps1 “PendingRemoval”

Mailbox-RemoveObject

  • Runs at 5:00 PM every Tuesday of every week
  • Add Arguments: -noninteractive -nologo C:\scripts\ScheduledTasks\UserTermination.ps1 “RemoveObject”

All of these must be set to run with the highest privileges, and set to run whether a user is logged on or not.

Structure

Here are some images of what your Active Directory and Scheduled Tasks should look like.

Active Directory

UserTerminationAD

Scheduled Tasks

Capture

Conclusion

Assuming you made it all the way down here after reading that mess above, congratulations! If you’re curious about the viability of this script, it has been working in our production environment for just short of a year now. It has processed north of 600 accounts and we have not yet (knock on wood) had a piece of information we were unable to retrieve.

Since the introduction of this script, we have freed up a little over 1TB in Mailbox database space and file share disk space. Those may not be huge returns, but if you’re working with limited resources, every bit helps.

You will probably have some issues along the way. This script was specifically tailored for our environment, but it should work with minimal modification by you.

I hope this is useful to you, and thank you for reading.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
#####################################
# UserTermination.ps1
# Version 1.1
# Troy Ward
# @AutomaShell / www.AutomaShell.com
#####################################
 
#Import Modules
param ([string]$function)
[void][System.Reflection.Assembly]::LoadFrom("C:\\scripts\\ScheduledTasks\\bin\\Ionic.Zip.dll")
[Reflection.Assembly]::LoadWithPartialName(”System.Web”) | out-null
 
# Modifiable Variables
$global:terminated_days = "30"
$global:from_email = "HelpDesk@domain.local"
$global:to_email = "system.administrators@domain.local"
$global:smtp_server = "relayserver.domain.local"
$global:zip_password = "ZipPassword!"
$global:termed_ou = "All Termed Users"
$global:domain = "domain.local"
$global:termed_mailbox_db = "TERMED"
$global:blockmailsender = "blockmailuser"
$global:archivelocation = "\\share\archive\"
 
# Static Variables (do not change)
$global:purge_date = (get-date).AddDays(-$terminated_days)
$global:date = (get-date).ToString("MM/dd/yyy")
$split = $domain.Split('.')
$global:shortdom = $split[0]
$global:shortdom2 = $split[1]
 
#### Desc: Zip directories ####
function Zip($Path,$OutputPath,$user) {
$hashinfo = $null;
for($a  = 1; $a -ne 10) {
$hashinfo = "$hashinfo`n$([System.Web.Security.Membership]::GeneratePassword(128,0))"
$a++
}
 
$directoryToZip = "$Path";
$zipfile =  new-object Ionic.Zip.ZipFile;
$e= $zipfile.AddEntry("README.txt", "$user archived on $date")
$e= $zipfile.AddEntry("hash.txt", "$hashinfo")
$e= $zipfile.AddDirectory($directoryToZip, "$user")
$zipfile.Encryption = [Ionic.Zip.EncryptionAlgorithm]::WinZipAes256
    $zipfile.Password = "$zip_password"
$zipfile.UseZip64WhenSaving = [Ionic.Zip.Zip64Option]::Always
$zipfile.Save("$OutputPath\$user.zip")
$zipfile.Dispose();
}
 
#### Desc: Convert AD description to a usable date ####
function ConvertToDate($STR) {
switch -regex ($STR) {
"^\d{2}/\d{2}$" {[DateTime]::ParseExact($STR,"MM/yy",$null)}
"^\d{1}/\d{2}$" {[DateTime]::ParseExact($STR,"M/yy",$null)}
"^\d{2}/\d{2}/\d{2}$" {[DateTime]::ParseExact($STR,"MM/dd/yy",$null)}
"^\d{1}/\d{2}/\d{2}$" {[DateTime]::ParseExact($STR,"M/dd/yy",$null)}
"^\d{2}/\d{1}/\d{2}$" {[DateTime]::ParseExact($STR,"MM/d/yy",$null)}
"^\d{1}/\d{1}/\d{2}$" {[DateTime]::ParseExact($STR,"M/d/yy",$null)}
"^\d{2}/\d{2}/\d{4}$" {[DateTime]::ParseExact($STR,"MM/dd/yyyy",$null)}
"^\d{1}/\d{2}/\d{4}$" {[DateTime]::ParseExact($STR,"M/dd/yyyy",$null)}
"^\d{2}/\d{1}/\d{4}$" {[DateTime]::ParseExact($STR,"MM/d/yyyy",$null)}
"^\d{1}/\d{1}/\d{4}$" {[DateTime]::ParseExact($STR,"M/d/yyyy",$null)}
"^\d{2}/\d{4}$" {[DateTime]::ParseExact($STR,"MM/yyyy",$null)}
"^\d{1}/\d{4}$" {[DateTime]::ParseExact($STR,"M/yyyy",$null)}
default { $STR = "N/A"}
}
}
 
#### Desc: Move all mailboxes located in "All Termed Users" to the proper termed Mailbox Database ####
function MoveMailbox {
$results = (Get-ADUser -filter * -SearchBase "OU=$termed_ou,DC=$shortdom,DC=$shortdom2" -properties * -ea stop) | Sort Name
$count = 0;
foreach($result in $results) {
$sam = ($result.SamAccountName)
$database = Get-Mailbox $sam -ea stop
if($database.Database -ne "$termed_mailbox_db") {
New-MoveRequest -identity $sam -TargetDatabase "$termed_mailbox_db" -BadItemLimit "50" -ea stop
$count++
$move_mailboxes = "$move_mailboxes$sam<br>"
}
}
 
# Email admin information
if($count -ne 0) {
Send-MailMessage -to "$to_email" -from "$from_email" -subject "User Termination Report - Move Mailboxes" -body "<font face=verdana size=2><b>Mailboxes Moved:</b><br><br>$move_mailboxes</font>" -smtpServer "$smtp_server" -BodyAsHtml -ea stop
}
}
 
#### Desc: Export Mailbox and Archive to PST ####
function ExportMailbox {
$results = (Get-ADUser -filter * -SearchBase "OU=$termed_ou,DC=$shortdom,DC=$shortdom2" -properties *) | Sort Name
$count = 0
$blockmail = (Get-ADUser $blockmailuser -properties *).CanonicalName
foreach($result in $results) {
if($result.Description) {
$termed = ConvertToDate ($result.Description).Replace("TERMED: ", "")
if($termed -le $purge_date) {
$sam = ($result.SamAccountName)
$database = Get-Mailbox $sam
if($database.Database -eq "$termed_mailbox_db") {
Set-Mailbox -AcceptMessagesOnlyFromSendersOrMembers "$blockmail" -RequireSenderAuthenticationEnabled $true -Identity $sam
New-MailboxExportRequest -Mailbox $sam -FilePath $archivelocation\$sam.pst -BadItemLimit 50
if((Get-Mailbox -identity $sam).ArchiveDatabase -ne $null) {
New-MailboxExportRequest -Mailbox $sam -FilePath $archivelocation\$sam.archive.pst -IsArchive -BadItemLimit 50
$archive_mailboxes = "$archive_mailboxes$sam<br>"
}
$count++
$regular_mailboxes = "$regular_mailboxes$sam<br>"
}
}
}
}
 
# Email admin information
if($count -ne 0) {
Send-MailMessage -to "$to_email" -from "$from_email" -subject "User Termination Report - Export PST's" -body "<font face=verdana size=2><b>Mailboxes Exported:</b><br><br>$regular_mailboxes<br><b>Archives Exported:</b><br><br>$archive_mailboxes</font>" -smtpServer "$smtp_server" -BodyAsHtml
}
}
 
#### Desc: Move all data located in a user's HomeDirectory to a encrypted zip ####
function ArchiveHomeDirectory {
$results = (Get-ADUser -filter * -SearchBase "OU=$termed_ou,DC=$shortdom,DC=$shortdom2" -properties *) | Sort Name
$count = 0
foreach($result in $results) {
if($result.Description) {
$termed = ConvertToDate ($result.Description).Replace("TERMED: ", "")
if($termed -le $purge_date) {
$sam = ($result.SamAccountName)
if($result.HomeDirectory) {
$hd = $result.HomeDirectory
if(test-path $hd) {
zip "$hd" "$archivelocation\UserShares" "$sam"
$archived_HD = "$archived_HD$sam<br>"
$count++;
} elseif(!(test-path "$hd" -ea silentlycontinue) -and !(Get-ACL $hd -ea silentlycontinue)) {
$aclerror = "$aclerror Cannot access $hd<br>"
Move-ADObject -identity $result.distinguishedname -TargetPath "OU=$termed_ou (errors),DC=$shortdom,DC=$shortdom2"
$count++;
}
}
}
}
}
 
# Email admin information
if($count -ne 0) {
Send-MailMessage -to "$to_email" -from "$from_email" -subject "User Termination Report - Archive Home Directories" -body "<font face=verdana size=2><b>Home Directories Archived:</b><br><br>$archived_HD<br><b>Errors:</b><br><br>$aclerror</font>" -smtpServer "$smtp_server" -BodyAsHtml
}
}
 
#### Desc: Remove Object & cleanup folders ####
function PendingRemoval {
$results = (Get-ADUser -filter * -SearchBase "OU=$termed_ou,DC=$shortdom,DC=$shortdom2" -properties *) | Sort Name
$count = 0
foreach($result in $results) {
if($result.Description -like "TERMED*") {
$termed = ConvertToDate ($result.Description).Replace("TERMED: ", "")
if($termed -le $purge_date) {
$sam = ($result.SamAccountName)
$hd = $result.HomeDirectory
 
#Has a PST been created?
if((test-path "$archivelocation\$sam.pst")) {
if((Get-Item "$archivelocation\$sam.pst").length/1KB -gt 264) {
$pst = $true
#$sam PST exists.
} else {
$pst = $false
$pendingremoval_error = "PST does not exist (size)"
}
} else {
$pst = $false
$pendingremoval_error = "PST does not exist"
}
 
#Has the HomeDirectory been backed up? Does this user even have a HomeDirectory?
if((test-path "$archivelocation\UserShares\$sam.zip")) {
if((Get-Item "$archivelocation\UserShares\$sam.zip").length/1KB -gt 1) {
$share = $true
}
$share = $true
} elseif(!($result.HomeDirectory)) {
$pendingremoval_error = "$pendingremoval_error - No Home Directory"
$share = $true
} else {
if(!(test-path "$hd" -erroraction silentlycontinue) -and !(Get-ACL $hd -erroraction silentlycontinue)) {
$pendingremoval_error = "$pendingremoval_error - Cannot access $hd"
$share = $false
}
$share = $false
$pendingremoval_error = "$pendingremoval_error - Home Directory exists, but no archive has been created"
}
 
#Parse results from PST and HomeDirectory. If success, update AD with "REMOVAL PENDING" desc. If failure, write to eventlog what has failed.
if($pst -eq $true -and $share -eq $true) {
Set-ADUser -identity $sam -description "REMOVAL PENDING" -enabled $false
$set_success = "$set_success$sam<br>"
} else {
$set_failure = "$set_failure$sam - $pendingremoval_error<br>"
}
 
$count++
}
}
}
 
# Email admin information
if($count -ne 0) {
Send-MailMessage -to "$to_email" -from "$from_email" -subject "User Termination Report - Pending Removal" -body "<font face=verdana size=2><b>User's ready for removal:</b><br><br>$set_success<br><b>Errors:</b><br><br>$set_failure</font>" -smtpServer "$smtp_server" -BodyAsHtml
}
}
 
#### Desc: Remove Object & cleanup folders ####
function RemoveObject {
$results = (Get-ADUser -filter * -SearchBase "OU=$termed_ou,DC=$shortdom,DC=$shortdom2" -properties *) | Sort Name
$count = 0
foreach($result in $results) {
if($result.Description -eq "REMOVAL PENDING") {
if(PendingRemoval) {
$sam = ($result.SamAccountName)
$hd = $result.HomeDirectory
Remove-Item -Recurse -Force $hd -confirm:$false
Remove-Mailbox -identity $sam -Permanent $true -confirm:$false
 
$count++
$removed = "$removed$sam<br>"
}
}
}
 
# Email admin information
if($count -ne 0) {
Send-MailMessage -to "$to_email" -from "$from_email" -subject "User Termination Report - Remove Object" -body "<font face=verdana size=2><b>User's removed:</b><br><br>$removed</font>" -smtpServer "$smtp_server" -BodyAsHtml
}
}
 
#### Desc: Manual function to list next batch of accounts to be termed ####
function List {
$results = (Get-ADUser -filter * -SearchBase "OU=$termed_ou,DC=$shortdom,DC=$shortdom2" -properties *) | Sort Name
$count = 1
foreach($result in $results) {
if($result.Description) {
$termed = ConvertToDate ($result.Description).Replace("TERMED: ", "")
if($termed -le $purge_date) {
$sam = ($result.SamAccountName)
write-host "$count) $sam"
$count++;
}
}
}
}
 
#Load modules
function LoadModules {
Import-Module ActiveDirectory
Add-PSSnapin Microsoft.Exchange.Management.PowerShell.E2010
}
 
#### Desc: Cleanup previous processes ####
function CleanUp {
if(Get-MoveRequest) {
$mr_failure = Get-MoveRequest -MoveStatus Failed
if($mr_failure) {
Send-MailMessage -to "$to_email" -from "$from_email" -subject "User Termination Report - MoveStatus Failures" -body "<font face=verdana size=2><b>Failures</b><br><br>$mr_failure</font>" -smtpServer "$smtp_server" -BodyAsHtml
}
Get-MoveRequest -MoveStatus Failed | Remove-MoveRequest -confirm:$false
Get-MoveRequest -MoveStatus Completed | Remove-MoveRequest -confirm:$false
}
 
if(Get-MailboxExportRequest) {
$mer_failures = Get-MailboxExportRequest | where {$_.status -like "Failed"}
if($mer_failures) {
foreach($mer_failure in $mer_failures) {
$path = $mer_failure.mailbox
Move-ADObject -identity $path.distinguishedname -TargetPath "OU=$termed_ou (errors),DC=$shortdom,DC=$shortdom2"
$write_mer_failure = "$write_mer_failure<br>$mer_failure"
}
Send-MailMessage -to "$to_email" -from "$from_email" -subject "User Termination Report - ExportRequest Failures" -body "<font face=verdana size=2><b>Failures</b><br><br>$write_mer_failure</font>" -smtpServer "$smtp_server" -BodyAsHtml
}
Get-MailboxExportRequest | where {$_.status -like "Failed"} | Remove-MailboxExportRequest -confirm:$false
Get-MailboxExportRequest | where {$_.status -like "Completed*"} | Remove-MailboxExportRequest -confirm:$false
}
}
 
#### Desc: Execute proper function ####
$errormessage = $null
try {
if($function -eq "MoveMailbox") {
LoadModules
CleanUp
MoveMailbox
} elseif($function -eq "ExportMailbox") {
LoadModules
CleanUp
ExportMailbox
} elseif($function -eq "ArchiveHomeDirectory") {
LoadModules
CleanUp
ArchiveHomeDirectory
} elseif($function -eq "PendingRemoval") {
LoadModules
CleanUp
PendingRemoval
} elseif($function -eq "RemoveObject") {
LoadModules
CleanUp
RemoveObject
} elseif($function -eq "List") {
LoadModules
List
} else {
$errormessage = "No function was defined."
}
} catch [system.exception] {
$errormessage = $($_.Exception.Message)
} finally {
if($errormessage) {
Send-MailMessage -to "$to_email" -from "$from_email" -subject "User Termination Report - $function" -body "<font face=verdana size=2><b>Error:</b><br><br>$errormessage</font>" -smtpServer "$smtp_server" -BodyAsHtml
}
}

 

Exchange Server · PowerShell · Windows Server

© Copyright 2012-2018 AutomaShell. Contact me if you have any questions.

  • TwitterTwitter
  • LinkedInLinkedIn
  • RSS FeedRSS Feed
  • Tabs

    • Recent Posts
    • Most Popular
    • Comments
    • Nutanix AOS 5.10 & Prism Central 5.10 releasedNovember 26, 2018
    • Nest VMware ESXi on Nutanix AHVSeptember 18, 2018
    • Nutanix Calm – AWS Setup & Example App DeploymentAugust 15, 2018
    • Nutanix: Add Unprotected VMs to Protection DomainJune 7, 2018
    • Deploy VMware VM’s with PowerCLIOctober 23, 2012
    • Enable Exchange 2010 Online Archive in a mixed environment (Part 1: By OU)December 4, 2012
    • User Termination (Part 2)June 6, 2013
    • Search Active Directory with PowerShell (LDAP)April 29, 2013
    • Rob Duff on:Search Active Directory with PowerShell (LDAP)
    • sean on:Deploy VMware VM’s with PowerCLI
    • Troy on:Deploy VMware VM’s with PowerCLI
    • Niklas Ilves on:Deploy VMware VM’s with PowerCLI
  • Categories

    • AWS
    • Calm
    • Exchange Server
    • General
    • Nutanix
    • PowerCLI
    • PowerShell
    • Uncategorized
    • VMware
    • Windows Server