In September of 2005, I wrote a blog post named "Sending an e-mail to users whose password is about to expire". Written in VBScript, it was one of my most popular blog posts of all time. I still have clients of mine that use it and I get occaisional email questions regarding it.
However, it is certainly showing its age!
There are other solutions available now, for free. However, the other solutions don't meet all of my needs. (As always, I encourage you to choose the solution that best meets your needs.)
In my case, I need to be able to support:
-
Fast and efficient searching of Active Directory
-
Support for Fine Grained Password Policies (FGPPs, also known Password Settings Objects or PSOs)
-
Authenticated SMTP
-
SSL/TLS SMTP
-
Report to Administrative User only (via SMTP)
-
Report to Administrative User only (via console)
-
Report to end-user (via SMTP)
The script in this blog post meets all of those needs. And, it is now written in PowerShell instead of VBScript.
I also chose to use the .Net Framework (System.DirectoryServices) for access to Active Directory (as well as some ADSI), instead of using the Active Directory PowerShell cmdlets. This makes it possible to execute the script on pretty-much any domain-joined computer, instead of one that requires RSAT-ADDS to be installed. It also avoids some weirdness around certain values returned by the AD cmdlets not matching older cmdlets or the AD itself.
This script is designed to work with PowerShell v2.0. The only PowerShell v2.0 feature is use of the Send-MailMessage cmdlet and using splatting to call Send-MailMessage. If you need this on PowerShell v1.0, you must just write a replacement for Send-MailMessage (use System.Net.Mail - it's not a big deal).
The script will work on any domain functional level (DFL). The DFL is relevant to whether Fine-Grained Password Policies (FGPP, also known as Password Settings Objects - PSOs) are in use or not. FGPPs can be used when the domain level is at Windows 2008 or higher.
Coming in at 767 lines, this is the longest single PowerShell script I believe I've posted. But it's well documented and hopefully self-descriptive. There are some fairly advanced capabilities demonstrated in this script, so you may find it worthwhile to study it a bit. If you have questions, let me know.
###
### Send-MailToUsersWithExpiringPasswords
###
### The top third of the script is data acquisition (and well documented).
### The bottom two-thirds is simple email-sending and report writing.
###
### This is PowerShell v1 compatible EXCEPT for using Send-MailMessage. You can
### easily replace that using System.Net.Mail if you wish.
###
### Parameter information:
### daysForEmail - how many days before a password expires should a user receive warning emails
### adminEmail - the administrator's email address
### adminEmailOnly - do not send email to users, only report to the administrator
### SMTPfrom - the From address for the SMTP message(s)
### SMTPserver - the server to be used for sending the SMTP message(s)
### SMTPuser - if credentials are required, the user for authenticating to the SMTP server
### SMTPpassword - if credentials are required, the password for the SMTPuser
### anr - instead of searching all users, only search for users matching the specified ANR string
### SMTPuseSSL - use an SSL/TLS connection, not a clear-text SMTP connection
### Quiet - if NOT set, a copy of the admin report is dumped to the pipeline as text
### DontSendEmail - Email is never sent to either users or admin
###
Param(
[int]$daysForEmail = 14,
[string]$adminEmail = "michael@smithcons.com",
[switch]$adminEmailOnly,
[string]$SMTPfrom,
[string]$SMTPserver,
[string]$SMTPuser,
[string]$SMTPpassword,
[string]$anr,
[switch]$SMTPuseSSL,
[switch]$Quiet,
[switch]$DontSendEmail
)
### Using Set-StrictMode helps protect against wonky errors that get caught by the
### compiler in compiled languages. Specifically (from the helpfile for the cmdlet):
### -- Prohibits references to uninitialized variables (including uninitialized
### variables in strings).
### -- Prohibits references to non-existent properties of an object.
### -- Prohibits function calls that use the syntax for calling methods.
### -- Prohibits a variable without a name (${}).
###
### However, using strict mode means that extra care has to be taken when using
### hashtables and property value collections. You see that in this script every
### time you see the Item() accessor method being used.
Set-StrictMode -Version 2.0
### For information about ANR, see "Ambiguous Name Resolution" in
### http://technet.microsoft.com/en-us/library/cc978014.aspx
$domainObject = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
$domainName = $domainObject.Name
$domainRoot = "LDAP://" + $domainName
$domainADSI = [ADSI]$domainRoot
$domainMode = $domainADSI.'msDS-Behavior-Version' ## Windows2008Domain is 3.
### domainMode is a PITA. System.DirectoryServices.ActiveDirectory.Domain.DomainMode and
### Microsoft.ActiveDirectory.Management.ADDomainMode have different values for the same
### enums!
###
### The first type is returned by System.DirectoryServices, the second type by the
### Get-ADDomain PowerShell cmdlet.
###
### That is why I ignore both of those potential access methods and use ADSI to access the
### value directly from the domain object. For specific information about those values, see:
### http://msdn.microsoft.com/en-us/library/cc223742(v=prot.10).aspx
[System.Int64]$Script:MaxPasswordAge = 0
function GetMaximumPasswordAge
{
###
### GetMaximumPasswordAge
###
### Retrieve the maximum password age that is set on the domain object. This is
### normally set by the "Default Domain Policy".
###
if( $Script:MaxPasswordAge )
{
### Cache the value so that it only has to be retrieved once, converted
### to an int64 once, and converted to days once. Win-win-win.
return $Script:MaxPasswordAge
}
### Dealing with ADSI unfortunately also means dealing with COM objects.
### Using ConvertLargeIntegerToInt64 takes the COM object and converts
### it into a native .Net type.
[System.Int64]$Script:MaxPasswordAge = $domainADSI.ConvertLargeIntegerToInt64( $domainADSI.maxPwdAge.Value )
### Convert to days
### there are 86,400 seconds per day (24 * 60 * 60)
### there are 10,000,000 nanoseconds per second
[System.Int64]$Script:MaxPasswordAge = ( -$Script:MaxPasswordAge / ( 86400 * 10000000 ) )
return [System.Int64]$Script:MaxPasswordAge
}
function newSecurePassword( [string]$password )
{
###
### newSecurePassword
###
### Take the normal string password provided and turn it into a
### secure string that can be used to set credentials.
###
### In PowerShell v2.0, this can be done with ConvertTo-SecureString.
### That cmdlet isn't available in v1.0 though.
###
$secure = New-Object System.Security.SecureString
$password.ToCharArray() |% { $secure.AppendChar( $_ ) }
return $secure
}
function newPSCredential( [string]$username, [string]$password )
{
###
### newPSCredential
###
### Create a new PSCredential object containing the provided
### username and plain-text password.
###
$pass = newSecurePassword $password
$cred = New-Object System.Management.Automation.PSCredential( $username, $pass )
$pass = $null
return $cred
}
###
### We need to find all those user's who:
### Are normal users 0x00000200 ADS_UF_NORMAL_ACCOUNT
### Are not disabled 0x00000002 ADS_UF_ACCOUNTDISABLE
### Do not have "password never expires" 0x00010000 ADS_UF_DONT_EXPIRE_PASSWD
### Do not have "no password required" 0x00000020 ADS_UF_PASSWD_NOTREQD
###
### Once we have the users, determine whether the user has a PSO
### by examining msDS-PSOApplied (if the domain mode of the executing domain
### is at Windows2008Domain or higher).
###
### If the user has a PSO, load the PSO (and store it to a hashtable)
### and evaluate the user's password against the PSO.
###
### If the user does not have a PSO, evalute the user's password
### against the Default Domain Policy Maximum Password Age (which
### is found by the function GetMaximumPasswordAge above).
###
### The most efficient way to find users is to do an LDAP search. But building the
### proper search isn't that easy. We need a number of filters:
###
### objectCategory=Person
### (userAccountControl & ADS_UF_NORMAL_ACCOUNT) <> 0
### (userAccountControl & ADS_UF_ACCOUNTDISABLE) == 0
### (userAccountControl & ADS_UF_DONT_EXPIRE_PASSWD) == 0
### (userAccountControl & ADS_UF_PASSWD_NOTREQD) == 0
###
### Since userAccountControl is a BIT-FLAG attribute (meaning that individuals bits
### of the value control these options) then we need to be able to do a bit-wise
### LDAP search. It's important to realize that with a bit-wise search, the result
### of a particular filter is either not-one (!1, which is false) or not-zero (!0,
### which is true).
###
### So the next step in building our query is to use the LDAP bit-wise AND filter:
###
### 1.2.840.113556.1.4.803
###
### This is used to do a bit-wise AND of the attribute on the left to the value
### on the right. For example:
###
### attribute:1.2.840.113556.1.4.803:=1024
###
### This means a bit-wise AND is done of the value of the attribute to the value
### 1024 (which is 0x400 in hexadecimal). If the result of that bit-wise AND is
### zero, then the value of the filter is false. If the result is non-zero, then
### the value of the filter is true. Putting a "!" in front of a false result
### makes the result true. Putting a "!" in front of a true result, makes it false.
###
### A combination of these two techniques makes it possible to scan for zero and
### non-zero bits (that is, those which are set to one and those which are set to
### zero).
###
### LDAP also supports a bit-wise OR filter, using the special value of:
###
### 1.2.840.113556.1.4.804
###
### Given the presence of AND and OR filters, it is possible to build very complex
### combination filters.
###
### A combination filter is built up of individual filters combined with either a
### logical OR ("|") or a logical AND ("&") and surrounded by parentheses.
###
### (&
### (objectCategory=Person)
### (userAccountControl:1.2.840.113556.1.4.803:=512)
### (!userAccountControl:1.2.840.113556.1.4.803:=2)
### (!userAccountControl:1.2.840.113556.1.4.803:=65536)
### (!userAccountControl:1.2.840.113556.1.4.803:=32)
### )
###
### So, in pseudo-C code this is:
###
### if( ( objectCategory == Person ) AND
### ( ( userAccountControl & ADS_UF_NORMAL_ACCOUNT ) != 0 ) AND
### ( ( userAccountControl & ADS_UF_ACCOUNTDISABLE ) == 0 ) AND
### ( ( userAccountControl & ADS_UF_DONT_EXPIRE_PASSWD ) == 0 ) AND
### ( ( userAccountControl & ADS_UF_PASSWD_NOTREQD ) == 0 ) )
### {
### ### we've got a matching user!
### }
###
$ldapFilter = "(&" +
"(objectCategory=Person)" +
"(userAccountControl:1.2.840.113556.1.4.803:=512)" +
"(!userAccountControl:1.2.840.113556.1.4.803:=2)" +
"(!userAccountControl:1.2.840.113556.1.4.803:=65536)" +
"(!userAccountControl:1.2.840.113556.1.4.803:=32)"
if( $anr )
{
###
### using an ANR subquery allows us to reduce the result set from the LDAP query
###
$ldapFilter += "(anr=$anr)"
}
$ldapFilter += ")"
###
### build the LDAP search
###
$directorySearcher = New-Object System.DirectoryServices.DirectorySearcher
$directorySearcher.PageSize = 1000
$directorySearcher.SearchRoot = $domainRoot
$directorySearcher.SearchScope = "subtree"
$directorySearcher.Filter = $ldapFilter
###
### load the properties we want
###
$directorySearcher.PropertiesToLoad.Add( "displayName" ) | Out-Null
$directorySearcher.PropertiesToLoad.Add( "mail" ) | Out-Null
$directorySearcher.PropertiesToLoad.Add( "pwdLastSet" ) | Out-Null
$directorySearcher.PropertiesToLoad.Add( "sAMAccountName" ) | Out-Null
$directorySearcher.PropertiesToLoad.Add( "userAccountControl" ) | Out-Null
if( $domainMode -ge 3 )
{
### this attribute is only valid on Windows2008Domain and above
$directorySearcher.PropertiesToLoad.Add( "msDS-PSOApplied" ) | Out-Null
}
$users = $directorySearcher.FindAll()
###
### All the data is in $users (or will be paged into $users).
### Build the necessary reports and emails.
###
$crnl = "`r`n"
$script:adminReport = ""
function line
{
foreach( $arg in $args )
{
$script:adminReport += $arg + $crnl
}
}
$now = Get-Date
$maximumPasswordAge = GetMaximumPasswordAge
line ( "Admin Report - Send Mail to Users with Expiring Passwords - Run Date/Time: " + $now.ToString() )
line " "
line "Parameters:"
line " Days warning for sending email: $daysForEmail"
line " Administrator email: $adminEmail"
if( $DontSendEmail )
{
line " Email will not be sent to either the administrator email or to user's email"
}
elseif( $adminEmailOnly )
{
line " Only the administrator will be sent email"
}
if( ![System.String]::IsNullOrEmpty( $SMTPfrom ) )
{
line " SMTP From address: $SMTPfrom"
}
if( ![System.String]::IsNullOrEmpty( $SMTPserver ) )
{
line " SMTP server: $SMTPserver"
}
if( ![System.String]::IsNullOrEmpty( $SMTPuser ) )
{
line " SMTP user: $SMTPuser"
}
if( ![System.String]::IsNullOrEmpty( $SMTPpassword ) )
{
line " SMTP password: $SMTPpassword"
}
if( $SMTPuseSSL )
{
line " UseSSL with SMTP is set"
}
line " Maximum Password Age in Default Domain Policy = $maximumPasswordAge"
line " Domain Mode $($domainObject.DomainMode.ToString())"
line ( " User count = " + $users.Count.ToString() )
if( [System.String]::IsNullOrEmpty( $SMTPserver ) -and [System.String]::IsNullOrEmpty( $PSEmailServer ) -and !$DontSendEmail )
{
Write-Error "No email server was specified via the SMTPserver parameter or the `$PSEmailServer environment variable."
exit
}
if( ![System.String]::IsNullOrEmpty( $SMTPuser ) -and [System.String]::IsNullOrEmpty( $SMTPpassword ) )
{
Write-Error "No SMTPpassword was specified."
exit
}
if( ![System.String]::IsNullOrEmpty( $SMTPpassword ) -and [System.String]::IsNullOrEmpty( $SMTPuser ) )
{
Write-Error "No SMTPuser was specified."
exit
}
if( [System.String]::IsNullOrEmpty( $SMTPfrom ) )
{
$SMTPfrom = "Password Administrator "
}
###
### some of the bitflags attached to the userAccountControl attribute
###
$ADS_UF_NORMAL_ACCOUNT = 0x00200
$ADS_UF_ACCOUNTDISABLE = 0x00002
$ADS_UF_DONT_EXPIRE_PASSWD = 0x10000
$ADS_UF_PASSWD_NOTREQD = 0x00020
$psoCache = @{}
foreach( $user in $users )
{
###
### we spend some time being pretty careful dealing with the properties. this
### allows us to verify we get the attributes we want, and for those that were
### not present, we can establish reasonable defaults.
###
line " "
$propertyBag = $user.properties
if( !$propertybag )
{
line "error! null propertybag!"
continue
}
$mail = ""
$disp = ""
$sam = ""
$pls = 0
$pso = 0
$uac = 0
$pwdX = $null
$dispObj = $propertyBag.Item( 'displayname' )
if( $dispObj -and ( $dispObj.Count -gt 0 ) )
{
$disp = $dispObj.Item( 0 )
if( !$disp ) { $disp = "" }
}
$dispObj = $null
### line "displayname = $disp"
$samObj = $propertyBag.Item( 'samaccountname' )
if( $samObj -and ( $samObj.Count -gt 0 ) )
{
$sam = $samObj.Item( 0 )
if( !$sam ) { $sam = "" }
}
else
{
$sam = ""
}
$samObj = $null
### line "sam = $sam"
$uacObj = $propertyBag.Item( 'useraccountcontrol' )
if( $uacObj -and ( $uacObj.Count -gt 0 ) )
{
$uac = $uacObj.Item( 0 )
}
else
{
line "no uac for $sam, assumed 0x200"
$uac = $ADS_UF_NORMAL_ACCOUNT
}
$uacObj = $null
### line "uac = $uac"
$plsObj = $propertyBag.Item( 'pwdlastset' )
if( $plsObj -and ( $plsObj.Count -gt 0 ) )
{
$pls = $plsObj.Item( 0 )
}
else
{
### this can be a normal occurence if the password has never been set
$pls = 0
}
$plsObj = $null
### line "pls = $pls"
line ( $sam.PadRight( 21 ) + $pls.ToString().PadRight( 21 ) + $uac.ToString().PadRight( 8 ) + '0x' + $uac.ToString('x').PadRight( 8 ) )
if( $pls -eq 0 )
{
line ( " " * 8 + "The password has never been set for this user, skipped" )
continue
}
[System.Int64]$localMaxAge = $maximumPasswordAge
if( $domainMode -ge 3 )
{
###
### PSOs can be created on Server 2003, but they don't work properly until
### the domain mode is Windows2008Domain or higher.
###
### So if we find a PSO and the domain mode is Windows2008Domain or higher,
### we first determine whether we've seen this PSO before. If we have seen
### the PSO before, then we've stored the msDS-MaximumPasswordAge value for
### the PSO into a hash table for quick retrieval. If we have not seen the
### PSO before, then we use ADSI to load the PSO and retrieve the value for
### msDS-MaximumPasswordAge and then cache the value for future access. By
### using a cache, we only have to access Active Directory to obtain values
### once per PSO, leading to a significant performance improvement compared
### to using the native cmdlets or S.DS.
###
$psoObj = $propertyBag.Item( 'msds-psoapplied' )
if( $psoObj -and ( $psoObj.Count -gt 0 ) )
{
$pso = $psoObj.Item( 0 )
### line ( "PSO object/name" + $pso )
if( $psoCache.Item( $pso ) )
{
[System.Int64]$localMaxAge = $psoCache.Item( $pso )
### line ( " " * 8 + "Accessed PSO from cache = " + $pso )
}
else
{
$psoADSI = [ADSI]( "LDAP://" + $pso )
$ageOBJ = $psoADSI.'msDS-MaximumPasswordAge'
[System.Int64]$localMaxAge = $psoADSI.ConvertLargeIntegerToInt64( $ageOBJ.Value )
### Convert to days
### there are 86,400 seconds per day (24 * 60 * 60)
### there are 10,000,000 nanoseconds per second
$localMaxAge = ( -$localMaxAge / ( 86400 * 10000000 ) )
$psoCache.$pso = $localMaxAge
### line ( " " * 8 + "Stored PSO to cache = " + $pso )
$ageOBJ = $null
$psoADSI = $null
### line "localMaxAge = $localMaxAge"
}
}
else
{
### completely normal to not have a PSO, in that case, use the maxPwdAge
### from the default domain policy.
$pso = $null
### line "pso is null"
}
$psoObj = $null
}
### In an Exchange environment, the 'mail' attribute contains the primary SMTP
### address for a user. In a non-Exchange environment, that should also be true,
### but no system process validates it. We do presume that the mail address is
### valid, if present.
$mailObj = $propertyBag.Item( 'mail' )
if( $mailObj -and ( $mailObj.Count -gt 0 ) )
{
$mail = $mailObj.Item( 0 )
}
else
{
$mail = ''
}
if( 0 )
{
### The conditions reported on below cannot occur based on our LDAP filter
### But they helped me develop and test the LDAP filter. :-)
if( $uac -band $ADS_UF_NORMAL_ACCOUNT )
{
"`t`tNormal account"
}
else
{
"`t`tNot a normal account"
}
if( $uac -band $ADS_UF_ACCOUNTDISABLE )
{
"`t`tAccount disabled"
}
else
{
"`t`tAccount enabled"
}
if( $uac -band $ADS_UF_DONT_EXPIRE_PASSWD )
{
"`t`tPassword doesn't expire"
}
else
{
"`t`tPassword expires"
}
if( $uac -band $ADS_UF_PASSWD_NOTREQD )
{
"`t`tPassword not required"
}
else
{
"`t`tPassword required"
}
}
### If we get here, $pls is non-zero.
###
### $pls comes to us in FileTime format (the number of 100 nansecond ticks
### since Jan 1, 1601). So it must be converted to DateTime and adjusted
### for the normal clock in order for us to do our arithmetic on it. For
### more information about FileTime, see:
### msdn.microsoft.com/en-us/library/windows/desktop/ms724290(v=vs.85).aspx
$date = [DateTime]$pls
$passwordLastSet = $date.AddYears( 1600 ).ToLocalTime()
$passwordExpires = $passwordLastSet.AddDays( $localMaxAge )
line ( " " * 8 + "The password was last set on " + $passwordLastSet.ToString() )
if( $now -gt $passwordExpires )
{
line ( " " * 8 + "The password already expired on $($passwordExpires.ToString()). No email will be sent.")
continue
}
else
{
line ( " " * 8 + "The password will expire on " + $passwordExpires.ToString() )
}
$difference = $passwordExpires - $now
$days = $difference.Days.ToString()
$hours = $difference.Hours.ToString()
$minutes = $difference.Minutes.ToString()
line ( " " * 8 + "(This is in $days days, $hours hours, $minutes minutes)" )
if( $difference.Days -le $daysForEmail )
{
if( [System.String]::IsNullOrEmpty( $mail ) -and !$adminEmailOnly )
{
line ( " " * 8 + "Oops - user doesn't have an email address." )
continue
}
if( $DontSendEmail )
{
line ( " " * 8 + "Oops - we aren't supposed to send an email." )
continue
}
line ( " " * 8 + "This user will be sent an email for password change." )
$hash = @{}
$hash.To = $adminEmail
$hash.Priority = 'High'
$hash.From = $SMTPfrom
if( ![System.String]::IsNullOrEmpty( $disp ) )
{
$hash.Subject = "Warning! The network password for $disp ($sam) is about to expire."
}
else
{
$hash.Subject = "Warning! The network password for $sam is about to expire."
}
if( !$adminEmailOnly -and ![System.String]::IsNullOrEmpty( $mail ) )
{
$hash.CC = $mail
}
if( ![System.String]::IsNullOrEmpty( $SMTPserver ) )
{
$hash.SmtpServer = $SMTPserver
}
elseif( ![System.String]::IsNullOrEmpty( $PSEmailServer ) )
{
$hash.SmtpServer = $PSEmailServer
}
###
### Send-MailMessage will default to using $PSEmailServer when no other SMTP server is specified.
### We checked earlier to ensure that at least one of those was specified.
###
if( ![System.String]::IsNullOrEmpty( $SMTPuser ) )
{
###
### If SMTPuser is specified then SMTPpassword is also specified.
### We checked earlier to make certain that if one was specified,
### then both were specified.
###
$hash.Credential = newPSCredential $SMTPuser $SMTPpassword
}
if( $SMTPuseSSL )
{
$hash.UseSSL = $true
}
$bodyHeader = @"
WARNING!
"@
$bodyHeader += "`r`nFor network user id: "
if( ![System.String]::IsNullOrEmpty( $disp ) )
{
$bodyHeader += $disp + " (" + $sam + ")"
}
else
{
$bodyHeader += $sam
}
$hash.Body = @"
$bodyHeader
Your password is about to expire in $days days, $hours hours, $minutes minutes.
Please change it now!
Thank you,
Your System Administrator
"@
###
### This is V2.0 function. I should replace it with something v1.0 compatible.
### (splatting is also V2.0 only, which is why I'm lazy and didn't replace it.)
###
Send-MailMessage @hash
$hash = $null
}
}
###
### Invidual report emails complete.
### Now send summaries to the console and/or to the administrator email address.
###
line " "
if( !$quiet )
{
###
### If $quiet is NOT set, then dump the report to the console, as well
### as sending the email to the administrator.
###
$script:adminReport
}
if( !$DontSendEmail )
{
$hash = @{}
$hash.To = $adminEmail
$hash.Priority = 'High'
$hash.From = $SMTPfrom
$hash.Subject = "Admin Report - Send Mail to Users with Expiring Passwords - Run Date/Time: " + $now.ToString()
$hash.Body = $adminReport
###
### Send-MailMessage will default to using $PSEmailServer when no other SMTP server is specified.
### We checked earlier to ensure that at least one of those was specified.
###
if( ![System.String]::IsNullOrEmpty( $SMTPserver ) )
{
$hash.SmtpServer = $SMTPserver
}
elseif( ![System.String]::IsNullOrEmpty( $PSEmailServer ) )
{
$hash.SmtpServer = $PSEmailServer
}
if( ![System.String]::IsNullOrEmpty( $SMTPuser ) )
{
###
### If SMTPuser is specified then SMTPpassword is also specified.
### We checked earlier to make certain that if one was specified,
### then both were specified.
###
$hash.Credential = newPSCredential $SMTPuser $SMTPpassword
}
if( $SMTPuseSSL )
{
$hash.UseSSL = $true
}
Send-MailMessage @hash
$hash = $null
}
###
### Clean up a bit.
###
$now = $null
$users = $null
$psoCache = $null
$directorySearch = $null
$domainADSI = $null
$domainObject = $null
$script:adminreport = $null
### Done.
Until next time...
If there are things you would like to see written about, please let me know.
Exchange 2010 Service Pack 2 was released-to-web on Monday (December 5, 2011).
Containing literally hundreds of code corrections since service pack 1, plus several significant new features, service pack 2 is an important new release. I will not attempt to cover all of its details, that has been done-to-death on other web sites and blogs, including Microsoft's own. Instead, I'll share one new "gotcha" and give you my opinion on best practices for installing the service pack.
The "gotcha"? First, a little background: Address Book Policies (ABPs) are an important new feature contained within service pack 2. They depend on the Exchange 2010 architecture, where all client communication is routed through the Client Access Server role. However, one of the things happening behind the scenes in the Exchange 2010 architecture is that the CAS role also hosts the "NSPI protocol". NSPI stands for Name Server Provider Interface, and it is the mechanism used by messaging clients to access and manipulate address data stored in Active Directory (paraphrased from Microsoft's NSPI Protocol document, available here). Historically, NSPI was hosted by group catalog servers (i.e., domain controllers), because NSPI information isn't used by only Exchange.
Here is the kicker: when you install Exchange on a domain controller, Exchange will use the NSPI provider from the domain controller and not install it's own as part of the Client Access Server role. This means that Address Book Policies will not work on Exchange Servers that are also domain controllers. This means that ABPs do not work on any SBS servers, or on any other "kitchen-sink" servers that may exist. There is your "gotcha" - to use ABPs, you must run Exchange and domain controllers as separate servers.
Now, on to installing Exchange 2010 Service Pack 2. As always, the role installation order is Client Access, Hub Transport, Unified Messaging, Mailbox. Edge Servers can be done in any order.
In the best of all possible worlds, you simply double-click on setup.exe and you are done, right? In the best of all possible worlds, that may be true. But, few of us live in that special place. :-) So, there are some things that we can do to ensure that our service pack install is "smooth as silk".
My list is:
For CAS, first install the new prequisite required by SP2. To do this:
-
Open an elevated PowerShell session
-
Import-Module ServerManger
-
Add-WindowsFeature Web-WMI
-
Exit
If you are running Database Availability Groups (DAGs) on Windows Server 2008 R2, and you have not already installed it, install the hotfix from KB2550886 - A transient communication failure causes a Windows Server 2008 R2 failover cluster to stop working. For more information about this hotfix, see the Microsoft Exchange Team blog posting: Recommended Windows Hotfix for Database Availability Groups running Windows Server 2008 R2.
Stop all ForeFront services - this includes ForeFront EndPoint Protection (FEP, which is local anti-virus) and ForeFront Protection for Exchange (FPE, which is Exchange anti-virus and anti-spam).
Stop all local anti-virus services.
Stop all System Center agents (this includes services named "MOM" and "System Center Management" and any other agent that may attempt to load PowerShell or the Exchange Management Shell or any Exchange snap-ins or modules).
Stop the "Task Scheduler" service. This prevents any manually scheduled tasks, or those schedule by, for example, System Center Operations Manager, from firing up while you are installing a service pack a screwing it up.
(Optional) Install the schema update manually. To do this:
-
Open an elevated PowerShell session, under an account which is an Enterprise Admin, Domain Admin, and Schema admin
-
setup.com /ps
-
exit
If you are using DAGs on the current server, then you need to mark the current server as "offline - undergoing maintenance". Exchange provides a script for that:
-
Open the Exchange Management Shell (EMS)
-
cd $exscripts
-
.\StartDagServerMaintenance -ServerName ExDag01
-
exit
Note: you must substitute the proper server name for ExDag01
Almost there! Start Task Manager, check the box for "Show processes for all users", and sort by Image Name. Verify that no instances of PowerShell.exe or MMC.exe are running. If there are, figure out why and get them closed.
Finally, apply the service pack.
From a cmd.exe prompt:
setup.com /m:upgrade
From setup.exe, simply double-click the setup.exe executable and follow the bouncing ball.
Next, I recommend a reboot of the server. Just because I'm old school.
If you are using DAGs on the current server, then you need to mark the current server as "back online - ready for use". Exchange provides a script for that:
-
Open the Exchange Management Shell (EMS)
-
cd $exscripts
-
.\StopDagServerMaintenance -ServerName ExDag01
-
exit
Note: you must substitute the proper server name for ExDag01
That's it!
If you follow this process, you will have a very high likelihood of your service pack installation going smoothly and without challenges. Obviously, for any given customer environment, this process can be automated. Doing so for a generic environment would be quite challenging. If I learn of any other service pack challenges that can be mitigated, I'll either update this post or create a new one and link to this one.
Until next time...
If there are things you would like to see written about, please let me know.
When creating PowerShell cmdlets for any Microsoft technology - WMI, Exchange, Lync, etc. - it is common to need to provide credentials that are different from the default credentials. This can be even more important when you are using PowerShell remoting to connect to a remote computer.
However, using the built-in cmdlet Get-Credential causes a dialog box to be opened on the console! (And it will simply fail in some cases, when the internal PowerShell $host.UI.PromptForCredential interface has not been implemented.) This is certainly not something that you want to happen when your PowerShell script is being called with remote PowerShell or from a service, or in many other scenarios.
The solution is to pass in the full credential, already containing the secure password and the user names and (optionally) the domain or a user principal name. This is a bit challenging, as the constructor for a secure string doesn't provide you an option for passing in an entire password. Therefore, you must build the secure string one character at a time.
The two functions below make the process easy.
Note: the $username parameter to newPSCredential can be in several formats: a plain username, a domain\username, or username@domain.com, or computername\username (for a local user).
Note 2: some functions want a NetworkCredential instead of a PSCredential. Creating one of those is as simple as changing System.Management.Automation.PSCredential to System.Net.NetworkCredential.
Note 3: as a security best practice, after you call the newPSCredential function, you should ensure that the plain text password is no longer available in the calling routine.
Enjoy!
function newSecurePassword( [string]$password )
{
###
### newSecurePassword
###
### Take the normal string password provided and turn it into a
### secure string that can be used to set credentials.
###
$secure = new-object System.Security.SecureString
$password.ToCharArray() |% { $secure.AppendChar( $_ ) }
return $secure
}
function newPSCredential( [string]$username, [string]$password )
{
###
### newPSCredential
###
### Create a new PSCredential object containing the provided
### username and plain-text password.
###
$pass = newSecurePassword $password
$cred = new-object System.Management.Automation.PSCredential( $username, $pass )
$cred
}
Until next time...
If there are things you would like to see written about, please let me know.
At Exchange Connections 2011 in Las Vegas, Greg Taylor of Microsoft made a comment about some schema changes coming in Exchange 2010 Service Pack 2, that were enabling potential future feature content. This led to interest on my part to investigate those changes further.
It's worthwhile to note that the Exchange Team at Microsoft allows schema updates to occur during service pack upgrades, but not during Update Rollups. This could potentially allow the Exchange Team to create schema changes that wouldn't have any support in the cmdlets released for the service pack, but cmdlets could be modified and improved in Update Rollups.
The Exchange Team has actually already released the schema changes that are coming in Exchange 2010 Service Pack 2. You can download a copy of these changes in the Exchange Server Active Directory Schema Changes Reference, October 2011.
The information contained in the document is very Active Directory oriented. So if you aren't very familiar with the AD Schema you may find the information confusing.
There are several very interesting items.
-
The Mail-Recipient class has now gained the Company and Department attributes.
This means that Groups (both security groups and distribution groups) and Contacts (mail contacts) can now be assigned values to the Company and Department attributes.
From a technical perspective, the Mail-Recipient class is a system auxiliary class, for both the Group and Contact classes, and all attributes present in Mail-Recipient are available in them.
-
The ms-Exch-Custom-Attributes class has gained 35 new custom attributes, from ms-Exch-Extension-Attribute-16 to ms-Exch-Extension-Attribute-45, and ms-Exch-Extension-Custom-Attribute-1 through ms-Exch-Extension-Custom-Attribute-5.
This means that Contacts, Groups, Users, Public Folders, Dynamic Distribution Lists, and Recipient Policies all now have a huge number of new attributes that can be assigned arbitrary values by an organization. This is welcome news to organizations who are using many or most of the current custom attributes and are wary to extend the schema themselves.
From a technical perspective, the ms-Exch-Custom-Attributes class is an auxiliary class for all the named classes above.
-
Many new attributes and classes were added to provide support for Address Book Policies and to enhance access to various address lists, global address lists, and offline address lists maintained by Exchange.
The master class is ms-Exch-Address-Book-Mailbox-Policy.
-
There are several new attributes and one new class (ms-Exch-Coexistence-Relationship) that are probably designed to support the Hybrid Coexistence Wizard and to overall simplify the process of configuring hybrid coexistence with Exchange Online.
-
There is a new class (ms-Exch-ActiveSync-Device-Autoblock-Threshold) and a number of new attributes that are within that class that appear to be designed to support automatic throttling of ActiveSync devices. However, that new potential feature was not discussed in Microsoft's blog post on
Announcing Exchange 2010 Service Pack 2.
Along with the above changes, various MAPI IDs were added to provide access to the above attributes and classes from within MAPI applications, and new OIDs were assigned to each of the new attributes and new classes.
While cmdlets may or may not appear in service pack 2 to manipulate all of these objects and attributes, they are welcome additions and will be available for organizations to utilize in custom programs.
Until next time...
If there are things you would like to see written about, please let me know.
In August of 2010, I posted Generating a report on Distribution Groups and their Membership. For most people, that script worked just fine.
However, it had some issues:
- Large groups would cause PowerShell to generate an error about concurrent pipelines
- The script generated string output instead of object output
This new version fixes those issues and adds 'help' content for the script. The script is below, but the information about the reasoning is copied directly from the original post, modified appropriately.
A common request is to get a list of all distribution groups and the members contained in that distribution group.
Note: a distribution group may also be a security group. From an Exchange Server perspective, the important thing is whether the group is mail-enabled or not.
You can take this report and pipe it to out-file in order to save the output to a disk file. Then you can inspect the file later, email it, copy-n-paste it, whatever you want. Export-Csv and Export-CliXml are also good options for exporting this data, especially since there is the potential for the contents of several arrays to be output.
Here is the script:
##
## Report-DistributionGroupsAndMembers.ps1
## v2.0
##
## Michael B. Smith
## http://TheEssentialExchange.com
## August, 2010
## July, 2011
##
## Requires the Exchange Management Shell
## As of version 2.0, requires PowerShell 2.0
## Tested on both Exchange 2007 and Exchange 2010
##
#requires -Version 2.0
if( $args )
{
if( ( $args.Length -eq 1 ) -and
( ( $args[0] -eq "-?" ) -or
( $args[0] -eq "-help" ) ) )
{
@"
NAME
Report-DistributionGroupsAndMembers
SYNOPSIS
This script outputs information about all distribution groups. The
information includes:
GroupName - The name of the distribution group
Identity - The unique identity of the group (an X400 identifier)
ManagerNames - An array containing the names of the managers for the
group. On Exchange 2007, this will contain a maximum of one
entry. On Exchange 2010, there may be many entries. It is
also possible (and quite likely in some environments), for
this array to be empty.
ManagerCanonicalNames - An array containing the canonical names
(also known as the relative distinguished name) for the
managers of this group. It will match, index by index, the
contents of ManagerNames.
Members - An array containing the names of all the members of
this group. It is also possible for this array to be empty.
This script must be executed from within an Exchange Management Shell.
SYNTAX
Report-DistributionGroupsAndMembers [-help]
"@ | out-default
return
}
throw "No parameters are allowed for this script"
}
$distributionGroups = Get-DistributionGroup -ResultSize Unlimited
foreach( $distributionGroup in $distributionGroups )
{
## not all of these temporaries are necessary, but it
## simplifies understanding the PowerShell code
$groupName = $distributionGroup.Name
$groupID = $distributionGroup.Identity
$managedBy = $distributionGroup.ManagedBy
$mgrName = @()
$mgrCName = @()
if( $managedBy -is [Microsoft.Exchange.Data.Directory.ADObjectId] )
{
$mgrName += $managedBy.Name
$mgrCName += $managedBy.RDN.ToString().SubString(3)
}
elseif( $managedBy.Count -gt 0 )
{
foreach( $manager in $managedBy )
{
$mgrName += $manager.Name
$mgrCName += $manager.RDN.ToString().SubString(3)
}
}
$membersArray = @()
$members = Get-DistributionGroupMember -Identity $groupID -ResultSize Unlimited
foreach( $member in $members )
{
$membersArray += $member.Name
}
$members = $null
$hash = @{
GroupName = $groupName
Identity = $groupID
ManagerNames = $mgrName
ManagerCanonicalNames = $mgrCName
Members = $membersArray
}
New-Object PSObject -Property $hash ## inject to the pipeline
}
$distributionGroups = $null
Until next time...
If there are things you would like to see written about, please let me know.
Paul Robichaux, a personal friend and another Exchange MVP is a regular writer for WindowsITPro. While performing research for an upcoming article, he found out some interesting factoids about Microsoft Exchange 2010 licensing.
I and other MVPs found these pieces of information interesting as well.
To make the article more interesting, Paul is taking a survey to see how many folks out there are up-to-speed on Exchange Server 2010 licensing. Please click to take the Exchange 2010 Licensing Survey.
After Paul's article is published, I'll make sure to update this post.
Until next time...
As always, if there are items you would like me to talk about, please drop me a line and let me know!
I've recently become associated with Third Tier, providing back-end support services along with the rest of their team.
Third Tier is a consulting company for consultants. We are a group of senior consultants, providing support for other consultants who have gotten themselves into a tough spot.
We have extensive expertise in Exchange, Active Directory, SharePoint, SQL Server, Windows Server, and Small Business Server.
I continue to own and operate The Essential Exchange and Smith Consulting; Third Tier provides me a way to provide services to another group of clients who may not have felt comfortable approaching me before.
If you are a consultant that occaisionally needs a little extra help, please check us out.
Until next time...
As always, if there are items you would like me to talk about, please drop me a line and let me know!
I originally wrote this article for CDOLive - Slipstick.com and it is republished with permission. The original article is available here. The article was written on May 19, 2011.
Earlier this week, at TechEd in Atlanta, Greg Taylor of the Exchange Product Team introduced the upcoming Service Pack 2 for Exchange Server 2010. As part of that presentation, Greg shared that we can expect it before the end of this calendar year. At just about the same time that Greg was making his presentation, a blog post by Kevin Allison (a General Manager on the Exchange Product Team) was being completed on the MS Exchange Team blog on the same topic.
These days, service packs tend to include a few new features as well as lots of bug fixes. Exchange 2010 service pack 2 is no different. Over 500 bug fixes are planned, plus four major new features. Obviously, nothing is complete until the product ships, but given that these items have been announced, it’s obvious that the team feels pretty good about the potential of these being shipped.
One of the new features is the Hybrid Configuration Wizard. This wizard assists in deploying Exchange, with so-called rich coexistence, between on-premises and Exchange Online. A rich coexistence requirement typically comes into play for larger companies. Larger organizations cannot typically complete their migration within a short period of time (such as a weekend) and cannot afford for email to be down an extended period of time. So, for the days or weeks that both environments are in use, it has to be possible for the environments to coexist. Ongoing coexistence, plus the fact that additions, changes, and subtractions may still be required during this timeframe necessitate a rich coexistence so that these changes can be communicated across the two environments. There are also companies that desire to operate this way long-term, with certain mailboxes in the cloud and others on-premises. Prior to the release of the Hybrid Configuration Wizard, preparing for rich co-existence required 46 separate detailed steps to be completed by the administrator. With the new wizard, the numbers of steps is reduced to six.
The second new feature is Cross-Site Silent Redirection. This will be also typically be used by larger organizations who have Client Access Servers in multiple Active Directory sites. Prior to this new feature, if a user connected to a CAS in an Active Directory site where their mailbox was NOT hosted, they would be forced to log in twice – once when connecting to the first CAS in the wrong site, and a second time after Exchange automatically referred them to a CAS in the proper site. With this new feature, only a single log in is now required. In other words, it now operates as one would expect.
The third new feature is Outlook Web App (OWA) Mini. Most of the sparse documentation so far is referring to this as OWA Mini. However, to those of us who have been using Exchange Server since Exchange 2003 or before are calling this “the return of OMA”. Exchange 2003 supported Outlook Mobile Access (OMA) and OWA Mini is basically a reimplementation of OMA. There are geographic regions of the world where web-mail is still the preferred way to access mailboxes – even on smartphones. OWA Mini provides a very small very minimalistic interface to email, calendar, contacts, and tasks. And on this feature, I say “I told you so”. When Microsoft announced that OMA was being removed in Exchange 2007, I predicted then that they would have to bring it back.
The fourth new feature is long awaited: Address Book Policies (ABPs). In Exchange 2000 through Exchange 2007, Microsoft had always provided some mechanism for a feature known as GAL Segmentation. This allows for different groups of users to receive different Global Address Books. This is something that many companies, both large and small, like to do. However, until the release of Service Pack 2, Microsoft had provided no support for this in Exchange 2010. With SP2, the implementation of ABPs is much better, potentially much more granular, and much easier than it ever has been before. To this, I say “well done!”
Only a small group of partners outside of Microsoft have received Service Pack 2 so far, and its release it still quite a way in the future. But it is definitely a release that I’m looking forward to.
Until next time...
As always, if there are items you would like me to talk about, please drop me a line and let me know!
I had the question posed to me today: if I have a mail-enabled group (either a security group or a distribution group) that has no members, and it receives an email, why don't I get an NDR?
Well - works as intended!
Let's have a thought experiment considering how mail-enabled groups work (from an Exchange perspective):
-
Create a distribution group
-
Assign it one or more email addresses
-
Don't add any members
-
Send an email
-
Let Transport expand the group
-
For every member in the group, make a copy of the email
-
Stop
What's different about the empty group? Absolutely nothing. A copy of the email is made for every member of the group - it just so happens that there aren't any members of the group!
Why would you do this? It's commonly done for employees that have left a company. For some period of time, you may want an ex-employees supervisor or manager to receive their emails. But eventually, you just want them to "go away".
It's also commonly done for "temporary" email addresses. For example, your company decides to run a promotion, and your have your customers send an email to a custom temporary email address. When the promotion is done, black-hole the email.
Now, starting in Exchange 2007 you can also create custom responses with transport rules, if you need or want them.
Black hole distribution groups have worked this way since at least Exchange 5.5. A further note: it's also common to hide these type of mail-enabled groups from the Exchange address book, so that they are not present in the GAL. Also be aware that you can have dozens of addresses assigned to a single group; so in most instances you'll only need a single black hole distribution group in your organization.
Until next time...
As always, if there are items you would like me to talk about, please drop me a line and let me know!
I spent eight hours over the last two days figuring out an interesting - but weird - problem. Once I figured out the problem, correcting it was a simple matter.
I'll just give you the problem description and then the solution. Hopefully it will save you more than a little time in the future.
I have a client (one of several in this particular situation) who has an Exchange 2003 Front-End server located in their DMZ. Yes, that's right, their DMZ. In the "long-ago time", this was considered to be a favored practice by Microsoft. That, of course, has changed over the years. Now, in 2011, we do not want any domain member servers located in the DMZ. Why is that? Because to install a domain member in a DMZ basically means that you have to make "swiss cheese" out of your firewall. And, effectively, if a DMZ server is compromised it means that your entire Active Directory can be compromised. The entire list of potentially affected ports is shown in KB 832017, and at the end of the day - it basically says that you have to open everything above port 1024 to make it all work.
With all that being said, if you are using Exchange 2003 with Server 2003 Active Directory (by which I mean an Active Directory domain controller hosted on a Windows Server 2003 server), you could get by with a much more limited set of ports (although it still isn't small!):
DNS (TCP and UDP 53)
Kerberos (TCP and UDP 88)
RPC Endpoint Mapper (TCP 135)
NetBIOS Name Service (TCP 137)
NetBIOS Datagram Service (TCP 138)
NetBIOS Session Service (TCP 139)
LDAP (TCP and UDP 389)
Directory Services (TCP 445)
LDAP Global Catalog (TCP 3268)
Remote Desktop Protocol (TCP 3389)
Active Directory RPC End-Point (AD-RPC-EP)
You should notice that all of these have defined ports except for the last! By definition, the AD-RPC-EP is a random high port.
However, a somewhat surprising fact is that when you run Active Directory on Windows Server 2003 (in other words, you have a domain controller on Server 2003), the AD-RPC-EP is always either port 1025 or 1026!
Starting with Server 2008, this is no longer true. "Port randomization" ensures that ports are allocated at truly random locations.
So... I have a particular client who, as part of their Exchange 2003 to Exchange 2010 upgrade effort decided that they first wanted to upgrade their Active Directory. That was fine by me - there are no documented restrictions regarding the operating system level of domain controllers to be used by Exchange 2003.
The day we changed the last domain controller from Windows Server 2003 to Windows Server 2008 R2, the Exchange FE server stopped working. Oh, $%&#.
Thankfully, the FE server was only being used for Outlook Web Access (OWA). SMTP was injected into the Exchange environment via a Barracuda cluster directly into the internal back-end server environment.
I'm rather hardheaded. So I wanted to figure this out. And, quite frankly, it took awhile. Eventually, it took an examination using portqry and rpcping (you can find out lots of information about these utilities by using Google or Bing and searching for "portqry site:support.microsoft.com" and "rpcping site:support.microsoft.com"). Comparing the results of those to the "access control lists" from the firewall showed me that the firewall always expected a port to be open at TCP 1025 and/or TCP 1026. Neither of these ports were open on Server 2008 R2!
I went back and investigate some other customers who were running Active Directory on Server 2003. On every single one of their servers, the process lsass.exe had either TCP 1025 or TCP 1026 (or both) open.
A google here, a bing there, and I was led to Active Directory Replication Over Firewalls and KB 224196: Restricting Active Directory replication traffic and client RPC traffic to a specific port.
These led me to understand that on Server 2008 and above AD-RPC-EP could happen at any random port - but that there is a way to specify the port that will be used. YAY!
For the following
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters
Registry value: TCP/IP Port
Value type: REG_DWORD
Value data: (available port)
specifying a "value data" of either 1025 or 1026 ensures that Server 2008 and Server 2008 R2 operate as did Server 2003. After setting this value, it does take a reboot of the affected DC/server for the value to take effect.
Once this was done, my client was happy again! I hope this helps you in your migrations...
Until next time...
As always, if there are items you would like me to talk about, please drop me a line and let me know!
Long ago and far away (way back in 2006) I wrote an article for finding the disk space used by Exchange 2000 and Exchange 2003, Finding disk space used by Exchange. While this worked through a few Exchange 2007 betas, by Exchange 2007 RTM, the STM file had been removed, and the original script would bomb, making the script only useful for Exchange 2000 and Exchange 2003.
Astute VBscript users would determine how to comment out the few lines causing the issue, but for most folks, that was too much.
So, I finally updated that script for Exchange 2007 and Exchange 2010 (tested on both) and updated it to PowerShell.
This is the script:
function build-object( [string]$server, [string]$edbfilepath, [string]$displayName )
{
write-debug "build-object enter"
write-debug "build-object server: $server"
write-debug "build-object edbfilepath: $edbfilepath type $($edbfilepath.gettype().ToString())"
write-debug "build-object displayName: $displayName"
write-debug " "
$o = "" | Select Name, Display, Length
$o.display = $displayName
# if( $server -eq $env:ComputerName )
if( 0 )
{
$r = dir -ea 0 $EdbFilePath
}
else
{
$filename = "\\" + $server + "\" + $edbfilepath.SubString( 0, 1 ) +
"$" + $edbfilepath.SubString( 2 )
write-debug "Calculated filename: $filename"
$r = dir -ea 0 $filename
}
$o.Name = $r.Name
$o.Length = $r.Length
write-debug "build-object exit"
return $o
}
Get-Command Get-ExchangeServer -ea 0 | out-null
if( ! $? )
{
write-error "You must run this script within an Exchange Management Shell"
return
}
$legacy = Get-ExchangeServer $env:Computername -ea 0
if( ! $? )
{
write-error "You must run this script on an Exchange server"
return
}
$org = $legacy.ExchangeLegacyDN.SubString( 3 )
$org = $org.SubString( 0, $org.IndexOf( '/' ))
"Exchange Organization Name: $org"
$default = ( Get-AcceptedDomain |? { $_.Default -eq $true } ).DomainName
"Default SMTP Domain: $default"
$forest = $legacy.DistinguishedName.SubString( $legacy.DistinguishedName.IndexOf( 'DC=' ) )
"Active Directory Forest: $forest"
" "
"All Exchange Servers in forest"
$servers = Get-ExchangeServer | select Name
foreach( $server in $servers )
{
"`tName: $($server.Name)"
}
" "
"All Mailbox Servers in forest"
$mailbox = Get-ExchangeServer |? { $_.ServerRole -match "Mailbox" } | Select Name, IsMemberOfCluster
foreach( $server in $mailbox )
{
"`tName: $($server.Name)"
}
" "
"Acquiring size of databases..."
" "
[int64]$totalSize = 0
foreach( $server in $mailbox )
{
"Server name: $($server.Name)"
[int64]$serverSize = 0
$serverArray = @()
if( $server.IsMemberOfCluster -eq 'Yes' )
{
$mailboxServer = Get-MailboxServer $server.Name -ea 0
$myServer = $mailboxServer.RedundantMachines[0]
$serverArray += (get-mailboxdatabase -server $server.Name -ea 0) |% {
build-object $myServer $_.EdbFilePath $_.AdminDisplayName;
}
}
else
{
$serverArray += (get-mailboxdatabase -server $server.Name -ea 0) |% {
build-object $_.Server $_.EdbFilePath $_.AdminDisplayName;
}
}
$serverArray += (get-publicfolderdatabase -server $server.Name -ea 0) |% {
build-object $_.Server $_.EdbFilePath $_.AdminDisplayName;
}
$serverArray |% { $serverSize += $_.Length }
foreach( $element in $serverArray )
{
if( $element )
{
"`tDatabase name: $($element.Display)"
"`t`tEDB File: $($element.Name)"
"`t`tEDB size: {0} bytes, {1} GB" -f $element.Length.ToString("N0"), ($element.Length / 1GB).ToString("N3")
#$element
}
}
"Total size of databases on server {0} bytes, {1} GB" -f $serverSize.ToString("N0"), ($serverSize / 1GB).ToString("N3")
" "
$totalSize += $serverSize
}
"Total size of all databases in organization {0} bytes, {1} GB" -f $totalSize.ToString("N0"), ($totalSize / 1GB).ToString("N3")
Until next time...
As always, if there are items you would like me to talk about, please drop me a line and let me know!
Each January, April, July, and October Microsofts recognizes certain individuals who - at no payment - assist in providing support to other Microsoft customers in various online forums, mailing lists, news groups, etc. Microsoft calls these people MVPs - Most Valuable Professionals. These people are "Independent Experts providing Real World Answers." For more information about the MVP program, click here.
I've been privileged to be awarded MVP for Exchange Server seven times now. My award cycle is in July - so every year, the week before July 1, I start getting just a little bit nervous as to whether I'll be awarded for the coming year. So far - so good. It is truly a privilege to be an MVP. In Exchange, there are a number of very notable individuals with whom I share the honor: Ed Crowley, Chris Scharff, Paul Robichaux, Tony Redmond, Jim McBee, David Elfassy - names that are likely on your bookshelf if you own any Exchange books. There are, of course, over a hundred other good people I didn't name. Many Exchange MVPs have gone on to work for Microsofton the Exchange Team or on the Lync (OCS) Team.
As of today, there are 88 types of MVPs, totalling 3,799 individuals, world-wide. Of those, there are 113 MVPs for Exchange Server. There are also 49 MVPs for PowerShell.
Now, an MVP doesn't have to make themselves publicly known. They can also choose to make their information only available to other MVPs. (This is somewhat common on the Microsoft forums, where people do not want to their company associations to be known, for whatever reason.)
The question arose regarding how to find this information out programmatically on a PowerShell MVP list. A new PowerShell MVP, Tome Tanasovski, was the first person to post a solution, soon followed by Claus Nielsen, another new PowerShell MVP. Claus' solution improved on Tome's by allowing an MVP to connect the secured MVP site in order to count those MVPs whose information was not public. It also switched from using System.Web .NET classes to using Internet Explorer automation.
I ran into some challenges using their versions, and wanted to add error handling and cleanup to their implementations. Thus, mine was born.
While you personally may not care about the number of MVPs and MVP specialities that exist on a day to day basis, you probably can find some PowerShell techniques here that can come in useful! Including being able to sign into a protected site...
Here is the script:
##
## Count-MVPs.ps1
##
## Michael B. Smith
## January, 2011
##
## Based on an original work by
## Tome Tanasovski, PowerShell MVP
## which was enhanced by
## Claus Nielsen, PowerShell MVP
##
## But this version has more features. :-)
##
## Note: Kirk Munro has yet another version of this script that is
## very developer-ish.
##
Param(
[switch]$gridview,
[string]$username,
[string]$password,
[int]$timeout = 15
)
[System.Reflection.Assembly]::LoadWithPartialName( "System.Web" ) | out-null
$ie = $null
try
{
if( $username )
{
$URI = "https://login.live.com/ppsecure/secure.srf?lc=1033&sf=1&id=259214&ru=https://mvp.support.microsoft.com/communities/mvp.aspx&tw=0&fs=0&kv=0&cb=wizid%3df4502d34-3b8f-4a04-b741-289e08aa1782%26lcid%3d1033%26returnurl%3dhttps%253a%252f%252fmvp.support.microsoft.com%252fcommunities%252fmvp.aspx%26brand%3dMicrosoft&cbcxt=&wp=MCMBI&wa=wsignin1.0&wreply=https://mvp.support.microsoft.com/communities/mvp.aspx&wlsu=1"
}
else
{
$URI = "https://mvp.support.microsoft.com/communities/mvp.aspx"
}
$ie = new-object -com "InternetExplorer.Application"
$ie.visible = $false
$ie.navigate( $URI )
$count = 0
while( $ie.Busy -eq $true )
{
Start-Sleep -Milliseconds 1000
$count++
if( $count -gt $timeout )
{
write-error "Timeout ($timeout seconds) waiting for content from the MVP Community website"
return
}
}
$doc = $ie.Document
#$ie.Document.Body.InnerText.ToString()
if( $username )
{
## log in
$user = $doc.getElementById( "i0116" ) ## login
if( $user )
{
$user.value = $username
}
else
{
"Document element named 'Login' not found"
}
$pass = $doc.getElementById( "i0118" ) ## passwd
if( $pass )
{
$pass.value = $password
}
else
{
"Document element named 'Passwd' not found"
}
$Logon = $doc.getElementById( "i0011" ) ## SI
if( $Logon )
{
$Logon.click()
}
else
{
"Document element named 'SI' not found"
}
$count = 0
while( $ie.LocationURL -ne "https://mvp.support.microsoft.com/communities/mvp.aspx" )
{
if( $ie.LocationURL -eq "https://mvp.support.microsoft.com/mvpinvalidsignin.aspx" )
{
write-error "Invalid username / password combination"
return
}
Start-Sleep -Milliseconds 1000
$count++
if( $count -gt $timeout )
{
"Location URL is $($ie.LocationURL)"
write-error "Timeout ($timeout seconds) waiting to connect to the MVP Community website"
return
}
}
$count = 0
while( $ie.Busy -eq $true )
{
Start-Sleep -Milliseconds 1000
$count++
if( $count -gt $timeout )
{
"IE still busy after $count seconds"
write-error "Timeout ($timeout seconds) waiting for content from the MVP Community website"
return
}
}
#$ie.Document.Body.InnerHTML.ToString()
}
$regex = [regex]'(?m)<A href=\"(\/communities\/mvp.aspx\?product=1&competency=(\S+))\"'
$objects = @()
$totMVPs = 0
$return = $ie.Document.Body.InnerHTML.ToString()
$matches = $regex.Matches( $return )
" "
"There were $($matches.Count) specialties found"
" "
$matches |% {
$specialty = [System.Web.HttpUtility]::UrlDecode( $_.Groups[2].Value )
"Scanning MVPs for $specialty..."
$obj = New-Object psobject
$obj |Add-Member NoteProperty -Name 'Group' -Value ($specialty)
$URI = "https://mvp.support.microsoft.com$($_.Groups[1].Value -replace 'amp;','')"
while( $ie.Busy -eq $true ) { Start-Sleep -Milliseconds 1000; }
$ie.navigate( $URI )
while( $ie.Busy -eq $true ) { Start-Sleep -Milliseconds 1000; }
if( ($ie.Document.Body.InnerText.ToString()) -match 'Results 1 \- \d+ of (\d+)' )
{
$MVPs=""
$MVPs = [int]$matches[1]
"...$MVPs found"
$obj |Add-Member NoteProperty -Name 'Count' -Value ($MVPs)
$totMVPs += $MVPs
}
$objects += $obj
}
" "
"Total MVP Competencies $($objects.Count)"
"Total MVPs listed $totMVPs"
if( $gridview )
{
$objects |sort Count -Descending |ogv
}
else
{
$objects |sort Count -Descending
}
}
finally
{
if( $ie )
{
$ie.Quit()
$ie = $null
}
}
Until next time...
As always, if there are items you would like me to talk about, please drop me a line and let me know!
In October of 2008, I wrote the article adminCount, adminSDHolder, SDProp and you! This article discussed why membership in privileged groups could cause permissions challenges. You may want to refer to it before proceeding.
With Exchange Server 2010, the overall situation has continued to get more restrictive. Exchange Server 2010 needs for users who have mailboxes to inherit permissions in order for Role-Based Access Control (RBAC) and overall Exchange access to work properly. Membership in a privileged group will stop a mailbox from inheriting permissions. Usually, normal mailbox access will work fine (i.e., using Outlook with MAPI) due to historical permissions present on a mailbox, but any other IIS based or MAPI service will fail. This includes, but is not limited to, Blackberry Enterprise Server, Exchange ActiveSync, Outlook Web App, etc.
You can see a complex way of working around this issue described in KB Article 907434: The "Send As" right is removed from a user object after you configure the "Send As" right in the Active Directory Users and Computers snap-in in Exchange Server.But that method is an administrative nightmare.
Realistically speaking - what can you do?
The answer is simple: Get your users out of privileged groups.
No user who is Administrator, a Domain Admin, an Enterprise Admin, or a Schema Admin should be signing into Exchange with an account that is a member of those groups. This is also true for any member of the Built-in\Administrators group that exists on Domain Controllers. Every user who has access to a high-privilege account should also have a normal user account that they use for day-to-day usage - just like everyone else.
For the other privileged groups (Account Operators, Server Operators, Print Operators, Backup Operators, Cert Publishers) - these are legacy groups. They are a carry-over from Windows NT. Don't use them. Instead, use a computer's Local Security Policy - or a Domain Security Policy when appropriate - to assign users who need those capabilities the specific rights they require.
Usually, you will find that the built-in groups provide more power than you think they do (e.g., a member of Account Operators, Server Operators, Print Operators, or Backup Operators can log in locally to a domain controller and shut it down). Mapping between the groups is fairly simple by examing User Rights Assignments in any Local Security Policy. An online version of this is available at: http://technet.microsoft.com/en-us/library/bb726980.aspx in tables 7-7, 7-8, and 7-9.
You may not normally consider <insert-any-group-name-here> to be a privileged group. But Active Directory does. Get users out of privileged groups where possible, and where not possible - assign (and require use of!) users a normal account and a privileged account.
Until next time...
As always, if there are items you would like me to talk about, please drop me a line and let me know!
Yesterday was a major day for patch releases in the Exchange and Outlook world.
For Exchange, Exchange 2010 SP1 got UR2, Exchange 2007 SP3 got its UR2. Exchange 2010 RTM got UR5, and Exchange 2007 SP2 got its UR5. For detailed information about the Exchange updates, please see Multiple December Update Rollup Releases at the Exchange Team Blog.
Also, a cumulative update for Outlook 2007 SP2 has been released that provides support for the Personal Archive in Outlook 2007. This is a removal of a very significant deployment blocker for many companies in regards to Exchange 2010. Please note that this provides support for an on-premise archive with an on-premise mailbox and for an Office 365 mailbox which has an Office 365-based archive. Having an on-premise mailbox with a cloud-based archive is not supported by this update.
http://bit.ly/gb4s5S
This CU includes the ability for an Outlook 2007 client to access the archive in Exchange 2010. This really has been a matter of listening to customers to push to get this into the Outlook product.
With the CU, customers will be able to:
Access email messages in their online archive using Outlook 2007.
Move messages into the online archive using Outlook 2007.
Delegates can see their manager’s online archive.
There are some limitations on Outlook’s support of the archive including:
Working with Archive policies
Independent searches of the archive and the primary mailbox
This will light up the Archive for the following SKUs:
Office Ultimate (retail)
Office ProPlus (volume license)
Office Enterprise (volume license)
Outlook Standalone (retail)
Outlook Standalone (volume license)
Much (but not all) of the text in this posting is Microsoft boilerplate. But I still thought you'd want to know! :-)
In Exchange 2007 and Exchange 2010, added functionality in Move-Mailbox (and New-MoveRequest), along with the support of online mailbox moves have reduced the requirement of moving entire databases around very much (it's easier to just move all the mailbox in the mailbox database to a new mailbox database in the proper location).
But sometimes - you just need to do it. Even with the downtime it may cause.
You CAN do it from the Exchange Management Console or from the Exchange Management Shell. However, the provided status information is worthless. You have no idea how far along the data movement is. What I recommend instead is this process:
-
Dismount the mailbox database
-
Use ROBOCOPY to copy the database file from the old location to the new location
-
Use Move-DatabasePath -ConfigurationOnly to update Exchange with the new location of the database
-
Mount the mailbox database
In pseudo-PowerShell:
-
dismount-database <dbname>
-
robocopy <src-dir> <dest-dir> <dbname>.edb
-
move-DatabasePath -Id <dbname> -ConfigurationOnly -EDBFilePath <new-full-path-to-edb>
-
mount-database <dbname>
ROBOCOPY provides excellent information as to the status of a filecopy.
Also note that "ESEUTIL /y" is a good solution for copying large files as well. It provides respectable filecopy status information.
Until next time...
If there are things you would like to see written about, please let me know.
Edit November 22, 2010 - I'm extremely embarassed. In the original version of this post I used "unmount-database" (which doesn't exist, of course) instead of "dismount-database". Thankfully, only a few dozen people pointed out my error to me.
More Posts
Next page »