Click here to Skip to main content
15,883,901 members
Articles / Programming Languages / PowerShell
Tip/Trick

Exchange 2010 User Maintenance Power-Shell Script

Rate me:
Please Sign up or sign in to vote.
4.00/5 (2 votes)
7 Dec 2015CPOL3 min read 10K   24   1   1
Exchange 2010 user Maintenance

Introduction

This script helps reduce the time it takes to administrate Exchange users.

Background

I wrote this script to allow other people to Mail enable new accounts so they show up in the GAL. It kept expanding to include other functions too.

Using the Code

The script does the following steps:

  • Enables user and contact to show up in the GAL and be part of distribution lists. (Does Not Create Mailboxes).
  • Remove groups and distribution lists from $DisabledOUDN OU.
  • Disabled mail users for $DisabledOUDN OU.
  • Export and disabled mailbox users for $DisabledOUDN OU.
  • Create and enable "No Longer With" Exchange rule users for $DisabledOUWithEmailRule.
  • Export, disabled, and move Mail Box Users for $DisabledOUWithEmailRule Over $PSTExportTime days.

Dependencies for this script:


Loading Modules for Power-Shell

PowerShell
# Exchange User Maintenance Script
# Version 1.3.0
# Operations:
#    *Enables User and Contact to show up in the GAL and be part of Distribution lists. (Does Not Create Mailboxes)
#    *Remove groups and distribution lists from $DisabledOUDN OU.
#    *Disabled Mail Users for $DisabledOUDN OU
#    *Disabled Mail Box Users for $DisabledOUDN OU
#    *Enable "No Longer With" Users for $DisabledOUWithEmailRule 
#    *Disabled Mail Box Users for $DisabledOUWithEmailRule Over $PSTExportTime Days
#Dependencies for this script:
#    *Active Directory PowerShell Tools Installed
#    *Active Directory Administrative Rights
#    *Exchange impersonation Rights
#    *Exchange Remote Management Shell
#    *Exchange Administrative Rights
#    *Exchange Trusted Subsystem needs to have Modify rights to user Home Drive
#    *EWS Managed API needs to be installed
# Code snippets from Sources:
#	http://gsexdev.blogspot.in/2012/11/creating-sender-domain-auto-reply-rule.html
#	http://poshcode.org/624

##Load Active Directory Module
Write-Host ("Loading Active Directory Plugins") -foregroundcolor "Green"
Import-Module "ActiveDirectory"  -ErrorAction SilentlyContinue

## Load Exchange WebServices API dll  
## Set Exchange Version  
Write-Host ("Loading Exchange WebServices Plugins") -foregroundcolor "Green"

###CHECK FOR EWS MANAGED API, IF PRESENT IMPORT THE HIGHEST VERSION EWS DLL, ELSE EXIT
## Code From http://gsexdev.blogspot.in/2012/11/creating-sender-domain-auto-reply-rule.html
$EWSDLL = (($(Get-ItemProperty -ErrorAction SilentlyContinue 
-Path Registry::$(Get-ChildItem -ErrorAction SilentlyContinue 
-Path 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Exchange\Web Services'
|Sort-Object Name -Descending| Select-Object -First 1 -ExpandProperty Name)).
'Install Directory') + "Microsoft.Exchange.WebServices.dll")
if (Test-Path $EWSDLL) {Import-Module $EWSDLL -ErrorAction SilentlyContinue}
#Import-Module "C:\Program Files\Microsoft\Exchange\Web Services\2.2\
Microsoft.Exchange.WebServices.dll" -ErrorAction SilentlyContinue
## Create Exchange Web Service Object  
$ExchangeVersion = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2010_SP2
$EWSservice = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService($ExchangeVersion)  
$EWSservice.UseDefaultCredentials = $true
## End Code From http://gsexdev.blogspot.in/2012/11/creating-sender-domain-auto-reply-rule.html
# Load All Exchange PSSnapins 
Write-Host ("Loading Exchange Plugins") -foregroundcolor "Green"
If ($([System.Net.Dns]::GetHostByName(($env:computerName))).hostname 
-eq $([System.Net.Dns]::GetHostByName(($ExchangeServer))).hostname) {
	Add-PSSnapin Microsoft.Exchange.Management.PowerShell.E2010 -ErrorAction SilentlyContinue
	. $env:ExchangeInstallPath\bin\RemoteExchange.ps1
	Connect-ExchangeServer -auto -AllowClobber
} else {
	$ERPSession = New-PSSession -ConfigurationName Microsoft.Exchange 
	-ConnectionUri http://$ExchangeServer/PowerShell/ -Authentication Kerberos
	Import-PSSession $ERPSession -AllowClobber
}

Setting all variables in needed for script. Also, finds the default e-mail domain.

PowerShell
#############################################################################
# User Varibles
#############################################################################

#User Home Drive Share
$HomeDriveShare = "\\File Server FQDN\Share"
$PSTFolder = "Outlook"
$PSTExportTime = 120
$ExchangeServer = "Exchange Server"
$Company = "Company Name"
$DisabledOUDN = "Disabled user Distinguished Name"
$DisabledOU = (Get-ADOrganizationalUnit $DisabledOUDN).Name
$DisabledOUWithEmailRule = "Disabled Users under 6 months"
$EnableEmailUsersOUs = "OU Name to Mail Enable","2nd OU Name to Mail Enable"
$ExchangeGroupsOU = "Exchange E-Mail Groups"
$ADContactOU = "AD Contacts OU Name"

#Set Defaults
$PrimaryEmailDomain = ((get-emailaddresspolicy | Where-Object 
{ $_.Priority -Match "1" } ).EnabledPrimarySMTPAddressTemplate).split('@')[-1]

You need to ignore any SSL errors otherwise, cannot create or enable Exchange rules.

PowerShell
#############################################################################
## Choose to ignore any SSL Warning issues caused by Self Signed Certificates  

## Code From http://poshcode.org/624
## Create a compilation environment
$Provider=New-Object Microsoft.CSharp.CSharpCodeProvider
$Compiler=$Provider.CreateCompiler()
$Params=New-Object System.CodeDom.Compiler.CompilerParameters
$Params.GenerateExecutable=$False
$Params.GenerateInMemory=$True
$Params.IncludeDebugInformation=$False
$Params.ReferencedAssemblies.Add("System.DLL") | Out-Null

$TASource=@'
  namespace Local.ToolkitExtensions.Net.CertificatePolicy{
	public class TrustAll : System.Net.ICertificatePolicy {
	  public TrustAll() { 
	  }
	  public bool CheckValidationResult(System.Net.ServicePoint sp,
		System.Security.Cryptography.X509Certificates.X509Certificate cert, 
		System.Net.WebRequest req, int problem) {
		return true;
	  }
	}
  }
'@ 
$TAResults=$Provider.CompileAssemblyFromSource($Params,$TASource)
$TAAssembly=$TAResults.CompiledAssembly

## We now create an instance of the TrustAll and attach it to the ServicePointManager
$TrustAll=$TAAssembly.CreateInstance("Local.ToolkitExtensions.Net.CertificatePolicy.TrustAll")
[System.Net.ServicePointManager]::CertificatePolicy=$TrustAll

## end code from http://poshcode.org/624

This section is adding AD users to GAL that have e-mail address other than the default email address for your domain.

PowerShell
ForEach ($EMOU in $EnableEmailUsersOUs) {
	Write-Host ("")
	Write-Host ("Searching for Users to Mail Enable in OU: $EMOU"  )
	#Mail Enable All user that have E-Mail Address in an AD OU "UIC Campus Users"
	$enablemailusers = get-user -organizationalUnit $EMOU  | 
	where-object{$_.RecipientType -eq "User" -and $_.WindowsEmailAddress -ne $null}
	$enablemailusers | ForEach-Object { 
		$data = $_.WindowsEmailAddress -split("@")
		if (($data[0] -ne "") -and ($data[1] -ne $PrimaryEmailDomain)) {
			Write-Host ("`tEnable Mail Name: " + $_.Name + 
			" Alias: " + $_.SamAccountName + " 
			Email: " + $_.WindowsEmailAddress) -foregroundcolor "Gray"
			#Remove any Exchange Attributes to reduce errors
			set-aduser -Identity $_.SamAccountName -clear msExchMailboxGuid,
			msexchhomeservername,legacyexchangedn,mailnickname,msexchmailboxsecuritydescriptor,
			msexchpoliciesincluded,msexchrecipientdisplaytype,msexchrecipienttypedetails,
			msexchumdtmfmap,msexchuseraccountcontrol,msexchversion	
			Enable-MailUser -Identity $_.Name -ExternalEmailAddress 
			$_.WindowsEmailAddress -Alias $_.SamAccountName 
		}
	}
}

This section adds AD contacts to the GAL that have e-mail addresses other than your default email addresses for your domain.

PowerShell
Write-Host ("Searching for Contacts to Mail Enable on OU: $ADContactOU")
#Mail Enable All contact that have E-Mail Address in an AD OU "Contacts"
$enablemailusers = Get-Contact -organizationalUnit $ADContactOU| 
where-object { $_.RecipientType -NotLike "*Mail*" -and $_.WindowsEmailAddress -ne $null }
$enablemailusers | ForEach-Object { 
	$data = $_.WindowsEmailAddress -split("@")
	if (($data[0] -ne "") -and ($data[1] -ne $PrimaryEmailDomain)) {
		
		Write-Host ("`tEnable Contact Name: " + $_.Name + 
		" Alias: " + $($data[0]) + " Email: " + 
		$_.WindowsEmailAddress) -foregroundcolor "Gray"

		Enable-MailContact -Identity $_.Name -ExternalEmailAddress $($data[0] + 
		"@" + $data[1]) -Alias $($data[0]) 
	}
}

This section is removing any groups or distribution lists from users in $DisabledOUDN.

PowerShell
Write-Host ("Searching for Users to Mail Disable in DN: $DisabledOUDN")
#Mail Disable All user that have E-Mail Address in an AD OU "Disabled Users"
get-aduser  -SearchBase $DisabledOUDN  -Filter * | ForEach-Object { 
	$UserDN = $_.DistinguishedName
	$userSAM = $_.SamAccountName
	Get-ADGroup -LDAPFilter "(member=$UserDN)" | foreach-object {
		if ($_.name -ne "Domain Users") {
			Write-Host ("`t Removing $userSAM from 
			group $_.name") -foregroundcolor "magenta"
			if ($_.DistinguishedName.tostring().contains("OU=" + $ExchangeGroupsOU)) {
				Remove-DistributionGroupMember -identity $_.name -Member $UserDN -Confirm:$False
			} else {
				remove-adgroupmember -identity $_.name -member $UserDN -Confirm:$False
			}
		} 
	}
}

Now, we are exporting and disabling any mailbox users in the $DisabledOU.

PowerShell
Write-Host ("Searching for Users to Disable in Exchange in OU: $DisabledOU")
#Mail Disable All user that have E-Mail Address in an AD OU "Disabled Users"
$enablemailusers = get-user -organizationalUnit $DisabledOU | 
where-object {$_.RecipientType -ne "User" -and $_.WindowsEmailAddress -ne $null}
ForEach ($EEUser in $enablemailusers) {

	if ($EEUser.WindowsEmailAddress -ne "") {
		If ($EEUser.RecipientType -eq "MailUser" ) {
			Write-Host ("`tDisable Mail Name: " + $EEUser.Name + 
			" Alias: " + $EEUser.SamAccountName + " Email: " + 
			$EEUser.WindowsEmailAddress) -foregroundcolor "magenta"
			Disable-MailUser -Identity $EEUser.SamAccountName -Confirm:$False
		}
		If ($EEUser.RecipientType -eq "UserMailbox" ) {
			#Testing to see if is in queue
			If ((Get-MailboxExportRequest | Where-Object { $_.Identity  
			-contains $EEUser.Identity -And $_.Status -ne "Completed"}) -eq $null) {
				Write-Host ("`tExport Mail Name: " + $EEUser.Name + 
				" Alias: " + $EEUser.SamAccountName + " 
				Email: " + $EEUser.WindowsEmailAddress) -foregroundcolor "Blue"
				#Create New Home Drive
				if (-Not (Test-Path $($HomeDriveShare + "\" + 
				$EEUser.SamAccountName))) 
					{New-Item -ItemType directory -Path ($HomeDriveShare + 
					"\" + $EEUser.SamAccountName)}
				if (-Not (Test-Path $($HomeDriveShare + "\" + 
				$EEUser.SamAccountName + "\" + $PSTFolder + "\"))) 
					{New-Item -ItemType directory -Path ($HomeDriveShare + 
					"\" + $EEUser.SamAccountName + "\" + $PSTFolder + "\")}
				#Export Mailbox to PST
				New-MailboxExportRequest -Mailbox $EEUser.SamAccountName 
				-FilePath $($HomeDriveShare + "\" + $EEUser.SamAccountName  + 
				"\" + $PSTFolder + "\" + $EEUser.SamAccountName + ".pst")

				while ( (Get-MailboxExportRequestStatistics -Identity $($EEUser.SamAccountName + 
				"\MailboxExport")).status -ne "Completed" ) {
					#View Status of Mailbox Export
					Get-MailboxExportRequestStatistics -Identity $($EEUser.SamAccountName + 
					"\MailboxExport") | ft SourceAlias,Status,
					PercentComplete,EstimatedTransferSize,BytesTransferred
					Start-Sleep -Seconds 10
				}

				#Remove mailbox from Exchange
				Disable-Mailbox -Identity $EEUser.SamAccountName -confirm:$false			

			} else {
				Write-Host ("`t`tUser " + $EEUser.Name + " already submitted.")
				while ((Get-MailboxExportRequestStatistics -Identity 
				($EEUser.SamAccountName + "\MailboxExport")).status -ne $("Completed")) {
					#View Status of Mailbox Export
					Get-MailboxExportRequestStatistics -Identity ($EEUser.SamAccountName + 
					"\MailboxExport") | ft SourceAlias,Status,
					PercentComplete,EstimatedTransferSize,BytesTransferred
					Start-Sleep -Seconds 10
				}

				#Remove mailbox from Exchange
				Disable-Mailbox -Identity $EEUser.SamAccountName -confirm:$false								
			}
		}
	}
}

Here is where a lot of things happen. I will break them down in to smaller chunks. All if this is happening to $DisabledOUWithEmailRule.

Getting the list of user from the OU and looping though them.

PowerShell
Write-Host ("Searching for Disable Users in OU: $DisabledOUWithEmailRule")

#Mail Disable All user that have E-Mail Address in an AD OU "Disabled Users"
$enablemailusers = get-user -organizationalUnit $DisabledOUWithEmailRule | 
where-object {$_.RecipientType -ne "User" -and $_.WindowsEmailAddress -ne $null}
ForEach ($CurrentAccount In $enablemailusers) { 
	$CurrentMailBox = $CurrentAccount | Get-Mailbox

When we disabled a user, we put in the AD user description the following (Date of deactivation in YYYYMMDD format) (username of person disabling account) (request/ticket number). Since we know when we disabled the user, we parse that out to know when the user has had the Exchange rule on for $PSTExportTime time.

PowerShell
If ( $($CurrentAccount.WindowsEmailAddress) -ne "" ) {
	#Need to parse out description to get date and then see if it is over 6 months.
	$ADUser = Get-adUser $CurrentAccount.SamAccountName -Properties Description,Manager
	#converts string to date
	$StrTestDate = [datetime]::ParseExact($ADUser.description.substring(0,8),"yyyyMMdd",$null)
	#Find out how old
	$currentdate= GET-DATE
	$TimeSpan = [DateTime]$currentdate - [DateTime]$StrTestDate
	$UsersManager= get-user $CurrentAccount.Manager
	#Look to see if OOA E-Mail is set

Here was the hardest part to get to work. This section uses EWS Managed API Please make sure you have it installed.

We first disable all rules to make sure we do not have any conflicting rules. The mail box rule is created and applied in this section.

PowerShell
$AllRules = Get-InboxRule -Mailbox $CurrentAccount.SamAccountName
if ($AllRules | where-object{ $_.name -eq "Termination Auto Reply"})
{
	#OOA Set
} else {
	#Disable all other rules
	## Code From <a href="http://gsexdev.blogspot.in/2012/11/creating-sender-domain-auto-reply-rule.html">http://gsexdev.blogspot.in/2012/11/creating-sender-domain-auto-reply-rule.html</a>
	ForEach ($Rule in $AllRules) {
		Disable-InboxRule -Identity $Rule.RuleIdentity -Mailbox $CurrentAccount.WindowsEmailAddress
	}
	Write-Host ("`tCreating Email Rule for $CurrentAccount.SamAccountName") 
	-foregroundcolor "Blue"
	$EWSservice.AutodiscoverUrl($CurrentAccount.WindowsEmailAddress,{$true})
	$EWSservice.ImpersonatedUserId = new-object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId
	([Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress, $CurrentAccount.WindowsEmailAddress) 
	Write-Host ("`t Using CAS Server : " + $EWSservice.url)
	
	#Create Message to reply with
	$templateEmail = New-Object Microsoft.Exchange.WebServices.Data.EmailMessage -ArgumentList $EWSservice
	$templateEmail.ItemClass = "IPM.Note.Rules.ReplyTemplate.Microsoft";
	$templateEmail.IsAssociated = $true;
	$templateEmail.Subject = "$($CurrentAccount.FirstName) is no longer with $Company";
	$htmlBodyString = " $($CurrentAccount.FirstName) is no longer with $Company 
	For any business related needs please e-mail $($UsersManager.FirstName) 
	at $($UsersManager.WindowsEmailAddress). ";
	$templateEmail.Body = New-Object Microsoft.Exchange.WebServices.Data.MessageBody($htmlBodyString);
	$PidTagReplyTemplateId = New-Object Microsoft.Exchange.WebServices.Data.ExtendedPropertyDefinition
	(0x65C2, [Microsoft.Exchange.WebServices.Data.MapiPropertyType]::Binary)
	$templateEmail.SetExtendedProperty($PidTagReplyTemplateId, [System.Guid]::NewGuid().ToByteArray());
	$templateEmail.Save([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Inbox);
		 
	#Create Inbox Rule
	$inboxRule = New-Object Microsoft.Exchange.WebServices.Data.Rule
	$inboxRule.DisplayName = "Termination Auto Reply";
	$inboxRule.Actions.ServerReplyWithMessage = $templateEmail.Id;
	$inboxRule.Exceptions.ContainsSubjectStrings.Add("RE:");
	$inboxRule.Exceptions.ContainsSubjectStrings.Add("FW:");			
	$createRule = New-Object Microsoft.Exchange.WebServices.Data.CreateRuleOperation[] 1
	$createRule[0] = $inboxRule
	$EWSservice.UpdateInboxRules($createRule,$true);
			
	## End Code From http://gsexdev.blogspot.in/2012/11/creating-sender-domain-auto-reply-rule.html

Now we are enabling email forwarding to the user's AD Manager and also keep a copy in the user mail box.

PowerShell
	#Enable Mail forwarding to manager.
	Write-Host ("`tForwarding e-mail for $CurrentAccount.SamAccountName to 
	$($UsersManager.Name)") -foregroundcolor "Blue"
	If ($CurrentAccount.ForwardingAddress -eq $null ) {
			If (-Not [string]::IsNullOrEmpty($UsersManager.WindowsEmailAddress.ToString())) {
				$CurrentAccount | Set-Mailbox -DeliverToMailboxAndForward $true 
				-ForwardingSMTPAddress "$($UsersManager.WindowsEmailAddress.ToString())"
				#$CurrentAccount | Set-MailboxAutoReplyConfiguration -AutoReplyState 
				enabled -ExternalAudience "all" -InternalMessage 
				"$CurrentAccount.FirstName is no longer with $Company 
				For any business related needs please e-mail $UsersManager.FirstName 
				at $UsersManager.WindowsEmailAddress." -ExternalMessage 
				"$CurrentAccount.FirstName is no longer with $Company 
				For any business related needs please e-mail $UsersManager.FirstName 
				at $UsersManager.WindowsEmailAddress."
			}	
	}
}

Now we are testing to see if we can export email to PST. If we can, we export email and disable Exchange account. After the exchange account is disabled, we move it to our $DisabledOUDN.

PowerShell
	#Write-Host ("Testing Mail Name: " + $_.Name + " Alias: " + $($data[0]) + 
		"Disable Date: " + $StrTestDate + " Date Age: " + $TimeSpan.TotalDays)
		
	If ($TimeSpan.TotalDays -ge $PSTExportTime) {
		#Testing to see if is in queue
		If ((Get-MailboxExportRequest | Where-Object { $_.Identity  
		-contains $($CurrentAccount.Identity)}) -eq $null) {
			Write-Host ("`tExport Mail Name: " + 
			$CurrentAccount.Name + " Alias: " + $CurrentAccount.SamAccountName + 
			" Email: " + $CurrentAccount.WindowsEmailAddress)  -foregroundcolor "Blue"
			#Create New Home Drive
			if (-Not (Test-Path $HomeDriveShare + "\" + $CurrentAccount.SamAccountName)) 
				{New-Item -ItemType directory -Path ($HomeDriveShare + 
				"\" + $CurrentAccount.SamAccountName)}
			if (-Not (Test-Path $HomeDriveShare + "\" + 
			$CurrentAccount.SamAccountName + "\" + $PSTFolder + "\")) 
				{New-Item -ItemType directory -Path ($HomeDriveShare + "\" + 
				$CurrentAccount.SamAccountName + "\" + $PSTFolder + "\")}
			#Export Mailbox to PST
			New-MailboxExportRequest -Mailbox $_.SamAccountName -FilePath 
			$($HomeDriveShare + 
			"\" + $CurrentAccount.SamAccountName  + "\" + $PSTFolder + 
			"\" + $CurrentAccount.SamAccountName + ".pst")

			while ( (Get-MailboxExportRequestStatistics -Identity 
				$($CurrentAccount.SamAccountName + 
			"\MailboxExport")).status -ne "Completed" ) {
				#View Status of Mailbox Export
				Get-MailboxExportRequestStatistics -Identity 
				$($CurrentAccount.SamAccountName + 
				"\MailboxExport") | ft SourceAlias,Status,
				PercentComplete,EstimatedTransferSize,BytesTransferred
				Start-Sleep -Seconds 10
			}

			#Remove mailbox from Exchange
			Disable-Mailbox -Identity $CurrentAccount.SamAccountName -confirm:$false
				
			#Move User to Disabled Outlook
			Move-ADObject -Identity $ADUser -TargetPath $DisabledOUDN
		} else {
			Write-Host ("`t`tUser " + $CurrentAccount.Name + " already submitted.")
			while ((Get-MailboxExportRequestStatistics -Identity 
			($CurrentAccount.SamAccountName + "\MailboxExport")).status -ne $("Completed")) {
				#View Status of Mailbox Export
				Get-MailboxExportRequestStatistics -Identity 
				($CurrentAccount.SamAccountName + 
				"\MailboxExport") | ft SourceAlias,Status,
				PercentComplete,EstimatedTransferSize,BytesTransferred
				Start-Sleep -Seconds 10
			}

			#Remove mailbox from Exchange
			Disable-Mailbox -Identity $CurrentAccount.SamAccountName -confirm:$false
				
			#Move User to Disabled Outlook
			Move-ADObject -Identity $ADUser -TargetPath $DisabledOUDN				
		}
	}
    }
}

Points of Interest

I would like to thank the following sites that gave examples of code for this script:

History

Version 1.3.0 First Script Post

  • Mail Enables User and Contacts
  • Mail User Disabled $DisabledOUDN
  • Mail Box User Disables $DisabledOUDN
  • Remove groups and distribution lists from $DisabledOUDN OU
  • Enable "No Longer With" Exchange reply for $DisabledOUWithEmailRule
  • Disable Mail box users $PSTExportTime Days old (Date from Description YYYYMMDD) and Export Mail to PST. Then the users are disabled in Exchange and moved to the $DisabledOUDN

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Systems / Hardware Administrator
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionUsing Office 365 Pin
kiquenet.com6-Nov-20 11:17
professionalkiquenet.com6-Nov-20 11:17 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.