PowerShell Quick Script: Finding the Exchange Schema Version

Every major Exchange release comes with updates to the Active Directory schema. In this case, "major release" means new major version (at RTM), every service pack, and (probably) every Cumulative Update with the new servicing model introduced for Exchange 2013.

Each update is unique to that particular release and, in general, they are cumulative. A notable exception to this was when Exchange Server 2007 SP3 had a higher schema version than that of Exchange Server 2010 RTM.

Over the lifetime of modern Exchange (since the integration to Active Directory with Exchange 2000), there have been a number of issues making it important to know the current schema version of Exchange. Most instructions on the web suggest using ADSIEdit to examine the relevant variable and value.

However, that is potentially risky (because ADSIEdit can be a dangerous tool) and can be a little confusing to use.

Here is a quick little PowerShell script to report on the proper value:

$root = [ADSI]"LDAP://RootDSE"
$name = "CN=ms-Exch-Schema-Version-Pt," + $root.schemaNamingContext
$value = [ADSI]( "LDAP://" + $name )
"Exchange Schema Version = $( $value.rangeUpper )"

The ms-Exch-Schema-Version-Pt attribute is never assigned to a class in the schema, it is used exclusively to identify the value of the Exchange Schema Version.

To anyone who has used ADSI in PowerShell or VBScript before, the little four-line script will appear very familiar. The PowerShell ADSI accelerator syntax allows for the corresponding PowerShell script to be shorter than the equivalent VBScript script.

In order for this script to work, it must be executed on a computer joined to an Active Directory domain. The execution context for the script (that is, the user account) requires no special privileges.

Oh, and if you prefer PowerShell one-liners, here is the same script as a one-liner for you:

"Exchange Schema Version = " + ([ADSI]("LDAP://CN=ms-Exch-Schema-Version-Pt," + ([ADSI]"LDAP://RootDSE").schemaNamingContext)).rangeUpper

Follow me on Twitter @essentialexch

 

Exchange Server 2013 Gotchas

Exchange Server 2013 reached RTM a couple of months ago and has since reached General Availability (GA).

In my personal opinion, Exchange 2013 RTM is not ready for prime time. Microsoft made a decision to release all of Wave 15 (Office desktop applications and servers) at the same time; as well as release Windows 8, Windows RT, and Windows Server 2012 at the same time. I think this decision was seriously flawed. It is obvious that the products were not complete at RTM (witness Windows 2012 and Windows 8 having 300 MB of patches between RTM and GA, and Exchange 2013 not supporting any interop with prior versions of Exchange at either RTM or GA). It is easy to conclude that the RTM dates were artificially imposed.

I have prepared a class on Exchange 2013 for one of my clients and part of that class was to discuss the limitations associated with Exchange 2013 RTM when compared to Exchange 2010 SP2. Note that the rest of the class discussed many of the new features and capabilities that have been added to Exchange 2013. So… the story is not all bad.

But as a summary of my opinion, Exchange 2013 RTM is not ready for prime time. Right now, it can only be installed in a green-field environment (that is, an environment where Exchange did not previously exist), so it is a safe bet that the Exchange team agrees with that as well. We can hope that some updates will quickly come out to address some of the current deficiencies.

This list is by no means exhaustive. And, as always, whether a particular issue is important to your organization requires you to evaluate your environment.

OWA

  • Help -> About is gone
  • It's very slow.
  • No S/MIME support
  • No Public Folder support, either for legacy public folders or modern public folders.
  • No distribution list moderation
  • No way to move the reading pane
  • Built-in spell-check is gone. IE 10 provides spell-check natively, but earlier versions of IE do not. A third-party add-in or an alternate browser is required.
  • Other things are gone; don't waste too much time looking for them.

Client Connectivity

  • No BES support
  • …on a related note (and likely the primary reason BES is not yet available), the CDO/MAPI download is not yet available for Exchange 2013.
  • Outlook 2003 is no longer supported.
  • Direct MAPI access to the Exchange server is no longer supported. RPC/HTTP (Outlook Anywhere) is required.
  • Outlook now reports that the server is it connected to is <<guid>>@<<active-directory-domain>>. This is intentional, if misguided.

Installation and Architecture

  • Cannot uninstall individual roles from a server, must uninstall all of Exchange
  • Install is painfully slow
  • The Hub Transport role is gone. There is now a Front End Transport service on CAS servers and Mailbox Transport services on Mailbox servers.
  • The Unified Messaging role is gone. There is a now a Unified Messaging Call Router service on CAS servers and a Unified Messaging service on Mailbox servers.
  • The CAS consists of three pieces: CAFE' (Client Access Front End), which proxies all end-user protocols to the appropriate mailbox server (completing the decoupling of the MAPI endpoint started in Exchange 2010) and handles Outlook Web App; FET (Front End Transport) which proxies SMTP protocols to the mailbox server and is responsible for TLS setup; and Unified Messaging Call Router.
  • After an installation or an upgrade, services not starting is an endemic problem. You will likely need to increase ServicesPipeTimeout on your Exchange servers.
  • Documentation is minimal at best
  • Deployment and sizing guidance is non-existent.
  • Cannot be installed along with Exchange 2007 or Exchange 2010
  • Exchange 2013 Edge server is not available
  • Forefront Protection for Exchange is gone
  • For both Exchange 2010 and Exchange 2013, applying updates can often screw up the winrm configuration. If you get errors in EMS or EAC regarding "The WS-Management service cannot process the request", try this first:

    winrm quickconfig
    iisreset

  • Since you cannot interop with legacy public folders in RTM, if you need an Organizational Forms Library, you must create it yourself. To create an Organizational Forms Library:

    1. Create "Organizational Forms Library" folder under the Eforms Registry:

    New-publicfolder "Organizational Forms Library" -path "\non_ipm_subtree\Eforms Registry"

    2. Set the locale ID for the Org Forms Library:

    Set-PublicFolder "\non_ipm_subtree\Eforms Registry\Organizational Forms Library" -EformsLocaleID EN-US

    It is no longer necessary to set the PR_URL_NAME property.

Exchange Management

  • The Exchange Management Console is gone as is the Exchange Control Panel. They are mainly replaced by the Exchange Administration Center (EAC); which is completely web based.
  • If you are attempting to use EAC with IE 10, you need KB2761465 (released on December 11, 2012).
  • The Exchange Best Practices analyzer is no more.
  • The Exchange Mail Flow Troubleshooter is no more.
  • The Exchange Performance Troubleshooter is no more.
  • The Exchange Routing Log Viewer is no more.
  • The EAC does not provide a preview (or an after-view for that matter) of the PowerShell it executed.
  • Antispam and antimalware is crippled compared to earlier releases

    The E15 AV does not offer a quarantine
    The E15 AS does offer a quarantine (for the administrator, not per-user)

  • Antispam cannot be managed from the Exchange Administration Center; it must be managed using PowerShell in the Exchange Management Shell
  • Kerberos Constrained Delegation (KCD) is not supported for OWA
  • This isn't new, but should be reinforced: DO NOT TURN OFF IPV6. Microsoft does not perform any testing to determine the effects of disabling IPv6. Therefore, Microsoft recommends that you leave IPv6 enabled, even if you do not have an IPv6-enabled network, either native or tunneled. See http://technet.microsoft.com/en-us/network/cc987595.aspx.
  • System Center Data Protection Manager (DPM) version required for backups of Exchange 2013 is SC DPM 2012 SP1

Mailboxes and Databases

  • Mailbox sizes MAY appear to increase substantially when moving a mailbox to an Exchange 2013 mailbox server. In Exchange 2010 and before, only select properties of a particular mailbox item were assigned as part of the mailboxes diskspace allocation, causing under-reporting. Now, all item properties for a particular mailbox item are assigned to the mailboxes disk space allocation. However, some items in Exchange 2013 are now compressed which were not before. This can lead to a reduction in reported and allocated diskspace. So, prediction is basically impossible. Just be aware that it may happen.
  • Corrupt PropertyTags during a mailbox move are common. Using (Get-MoveRequestStatistics -IncludeReport <<alias-name>>).Report.Failures you can find the rule or message that is causing the problem and remove it.
  • Changes made to improve Office 365 and hybrid deployments had an unintended consequence (this is my conclusion). When you are performing impersonation (e.g., to open a different user's mailbox via EWS), you should always impersonate using the email address.
  • As a corollary, it is recommended that the account UPN match the primary email address.
  • In a change that you won't know about until you need to know it – MRS Proxy is not enabled by default in Exchange 2013. Use Set-WebServicesVirtualDirectory to enable it.
  • Clean-MailboxDatabase is gone

    Update-StoreMailboxState is designed to replace it
    Requires that you know the guid of the deleted mailbox
    No on-premises cmdlets allow you to find those out!

  • Get-LogonStatistics is non-operational. The cmdlet is still present, but it doesn't work.
  • Exchange 2013 Enterprise Edition supports only 50 mailbox databases instead of the 100 supported in Exchange 2010
  • MRM 1.0 (Messaging Record Management – Managed Folders) is non-operational on Exchange 2013 mailbox servers. The cmdlets are still present, and will affect down-level servers (which you can't use right now), but they don't work with Exchange 2013 servers.
  • Moving mailboxes using the migration wizard in EAC can generate large amounts of log files for the database which hosts the arbitration mailbox. Use New-MoveRequest instead.
  • In a positive change, Office Filter Packs are no longer required. There is a new search technology used in all Wave 15 (Office 2013) products and it knows how to search all the Office file formats. This also includes the PDF format, so a separate iFilter installation for PDF is no longer required.
  • When using Database Availability Groups (DAGs) on Windows Server 2012, you must manually create the Cluster Network Object (CNO) and provision the CNO by assigning permissions to it.
  • While Windows Server 2012 provides support for large sectors (4 KB), Exchange 2013 does not support large sectors. Emulation of large sectors (512 E) is supported provided that all database copies are on 512 E.
  • The above statement is, in general, true. Additional capabilities of Windows Server 2012 are not supported by Exchange Server 2013. This specifically includes but is not limited to Hyper-V Replica.

Good luck!

[Edit at 19:55 on 6-January-2013 to clarify why you may need an organizational forms library and to add the note regarding lack of spell-check in OWA (hat-tips to Ben Winzenz and Tim Robichaux).]

[Edit at 21:24 on 8-January-2013 to fix several grammar and spelling errors. Oops.]

 

Windows Management Framework 3.0 / PowerShell 3.0 and Exchange

In the last few days, Windows Management Framework 3.0 (WMF 3.0) has begun appearing in Microsoft Update (MU), Windows Update (WU), Windows Software Update Services (WSUS), and on Configuration Manager Software Update Points. This basically means that Microsoft is now suggesting that WMF 3.0 be installed on all of your servers where the update is applicable.

This update is released as KB 2506146 and KB 2506143.

DON'T DO IT.

WMF 3.0 includes PowerShell 3.0.

PowerShell 3.0 is a great improvement to PowerShell. No question about it.

However, Exchange 2010 is NOT currently qualified to work with PowerShell 3.0. And, in fact, it doesn't. It will break. PowerShell 3.0 compatibility will come with Exchange 2010 Service Pack 3, due sometime in the first half of calendar year 2013 (word on the street says first quarter).

If you have installed WMF 3.0, you will also find that Exchange Update Rollups will fail to install.

Exchange 2007 is also not qualified to work with PowerShell 3.0. And, as far as I know, never will be.

You absolutely, positively, do not want to install the update on your Exchange servers.

You also do not want to install the update on workstations or utility servers where you have Exchange Management Tools installed.

I have also heard reports that SharePoint 2010 also has problems with the WMF 3.0 release. I can believe it. You should avoid that as well.

Good luck!

P.S. Exchange 2013 does work with WMF 3.0 and in fact, WMF 3.0 is required to install Exchange 2013. If you are one of the rare few running Exchange 2013, you do not need to be concerned about this.

 

Wave 15 reaches RTM, including Exchange 2013

Wave 15 of Office products reached the Release To Manufacturing (RTM) stage today, October 11, 2012 (10/11/12 – heh).

These products include Exchange Server 2013, Lync Server 2013, SharePoint Server 2013, and Microsoft Office 2013.

Note that RTM, also known as "code-complete", means that development is done on the products and they will now be written to ISOs, DVDs burned, retail boxes built, etc.

This does NOT mean that the releases are currently available. As of today, releases are still only available to TAP and RDP participants.

General Availability (GA) defines the timeframe when the releases will be available to everyone. This is targeted for first quarter calendar-year 2013 for all the Wave 15 products.

However, volume license customers will be able to download these products on the Volume Licensing Service Center (VLSC) by mid-November and the products will be on the VL price lists starting December 1, 2012.

Specifically for Exchange, the build number for the RTM release is 15.0.516.32. Exchange 2013 was code-named E15 throughout its development cycle.

For your information, "Wave 15" refers to the fact that version numbers of all Office product lines have been synchronized. The major version number for all products is "15".

For more information about Exchange, refer to the Exchangae team blog: The New Exchange Reaches RTM!

For more information about the other Office products, refer to the Office blog: Office Reaches RTM.

It is interesting to note that both releases refer to "the New Exchange" and "the new Office" – taking a cue from Apple, I presume; in not tying the announcement to a specific release of the product.

 

Security Advisory Affecting Exchange: 2749655

Today Microsoft released Security Advisory 2749655, Compatibility Issues Affecting Signed Microsoft Binaries. I encourage you to read the security advisory. We also saw that Exchange team post the following blog entry Re-released Exchange 2010 and Exchange 2007 update rollups. Microsoft re-released the rollups in order to ensure that third party programs are not impacted by the security advisory.

That is, Microsoft re-released the updates as a pre-emptive measure – just in case.

If you have installed the security advisory, you really don't need to re-install the updates. Quoting from the security advisory:

What does this update do?
This update will help to ensure the continued functionality of all software that was signed with a specific certificate that did not use a timestamp Enhanced Key Usage (EKU) extension. To extend their functionality, WinVerifyTrust will ignore the lack of a timestamp EKU for these specific X.509 signatures.
……

Note regarding the impact of not installing a rereleased update
Customers who installed the original updates are protected from the vulnerabilities addressed by the updates. However, because improperly signed files, such as executable images, would not be considered correctly signed after the expiration of the CodeSign certificate used in the signing process of the original updates, Microsoft Update may not install some security updates after the expiration date. Other effects include, for example, that an application installer may display an error message. Third-party application whitelisting solutions may also be impacted. Installing the rereleased updates remediates the issue for the affected updates.

Long story short – Microsoft has released the updates "just in case".

Given that Exchange 2010 SP2 Update Rollups can take upwards of an hour to install and your Exchange server is offline while you doing the installation – you may just want to wait for the next UR.

Please note: the Exchange 2010 SP2 UR4 re-release does include one new patch. You can install that separately in a very short timeframe: Outlook only returns one result after you click the "Your search returned a large number of results. Narrow your search, or click here to view all results" message.

 

Microsoft Security Advisory 2737111 – Exchange 2007/2010/2013

Well, in case you haven’t seen it, an Exchange Security advisory was released today.

“Vulnerabilities in Microsoft Exchange and FAST Search Server 2010 for SharePoint Parsing Could Allow Remote Code Execution”

http://technet.microsoft.com/en-us/security/advisory/2737111

And yes, it also affects the Exchange 2013 Preview.

It wasn’t immediately obvious to me, but every Exchange CAS/CAFE has these libraries installed. Microsoft licenses them from Oracle.

I’m guessing that that will change the workaround to:

Get-OwaVirtualDirectory |? {
$_.OwaVersion -eq ‘Exchange2007’ -or $_.OwaVersion -eq ‘Exchange2010’ –or $_.OwaVersion –eq ‘Exchange2013’ } |
Set-OwaVirtualDirectory -WebReadyDocumentViewingOnPublicComputersEnabled:$False `
-WebReadyDocumentViewingOnPrivateComputersEnabled:$False

But I’m on the road and don’t have access to my E15 test lab at the moment…

Thanks to Susan Bradley, the SBS Diva, for pointing out to me that this doesn’t require third party add-ins.

[Edit]

More information:

“In Microsoft Exchange Server 2007 and Exchange Server 2010, Outlook Web App (OWA) users are provided with a feature called WebReady Document Viewing that allows users to view certain attachments as a web page instead of relying on local applications to open/view it. Oracle Outside In is used by the conversion process in the server backend to support the WebReady feature. Microsoft licenses this library from Oracle.”

In the Exchange Server 2007/2010 scenario, the conversion process that uses Oracle Outside In, TranscodingService.exe, runs as LocalService.

http://blogs.technet.com/b/srd/archive/2012/07/24/more-information-on-security-advisory-2737111.aspx

 

Exchange Server 2013 on Windows Server 2012

Unless you've been living under a rock, you know that the public betas of all the Wave 15 products, including Office, Exchange, Lync, SharePoint, etc. were released earlier this week as "2013" products. These were all released on Monday July 16, 2012. This follows by a couple of weeks the "release previews" of Windows 8 and Windows Server 2012.

You can download the Exchange Server 2013 public beta/preview here.

In the original release, it was not supported to install the Exchange preview on the Server 2012 preview. Today, Microsoft has changed that guidance and now supports installing Exchange Server 2013 on Windows Server 2012.

At this time, it requires a couple of additional manual steps. You can find information about how to install Exchange Server 2013 on Windows Server 2012 here.

We can reasonably expect that this will be cleaned up before RTM.

 

Processing Large and Embedded Groups in PowerShell

I'm working with a client that has – over an extended period of time – accumulated thousands of security groups, most of which are mail enabled for use by both Exchange and for setting rights on various objects.

Becasue they use embedded groups (that is, placing a group as a member of another group), they have a horrible time keeping straight who is actually a member of those groups.

Recently an unintentional disclosure at the client has caused them to desire to clean this mess up. 🙂

They found that many tools weren't robust enough to deal with their environment. Their groups may contain thousands of members and dozens of embedded groups. And sometimes, the groups are recursive (or nested). That is, GroupA includes GroupB, GroupB includes GroupC, and GroupC includes GroupA. Also, due to historial reasons, many of their users have non-standard primary group memberships and those needed to also be included as part of the evaluation process.

Note: be aware – most tools will only return 1,500 members for a group (1,000 if your FFL is Windows 2000 mixed or Windows 2000 native). This includes most tools from Microsoft (e.g., dsquery and dsgroup). Some of the tools that handle that properly will go into an infinite loop if there are nested groups. Since the primary group is stored differently than other group memberships, most tools simply ignore it (the RID of the group is stored in the primaryGroupId attribute of a user object, instead of using a memberOf backlink or the member attribute forward link from the group itself).

We were unable to find a tool (which doesn't mean one isn't out there!) that handled all of these issues properly.

So, I wrote one. In PowerShell.

Note that performance is not great when you are scanning nested groups. This is because it is necessary to evaluate every member to determine the type of the member – user, group, contact, etc. That adds significant additional processing overhead.

Each individual piece of this script is pretty obvious (except for the "range" processing required for looking at large group memberships). But after putting it all together, it's a thing of magic. 🙂

Enjoy!


###
### Get-GroupMember
###
### This function processes LARGE groups. Most normal utilities are limited
### to returning a maximum of 1,500 members for a group. To get all members
### of a group requires using a "ranged" member attribute. Few programs,
### including many from Microsoft, go to that much trouble. This one does.
###
### Also, retrieving membership from embedded groups, while avoiding the
### problems that can occur with group recursion, is something that many
### programs do not handle properly. This one does.
###
### Also, some programs do not handle empty groups properly (including the 
### example range program on MSDN from Microsoft). This one does.
###
### Also, some programs do not also check for the primaryGroupID membership, 
### and thus cannot return the membership of, for example, 'Domain Users'. 
### This one does.
###
### The ADSpath for each member of the group is written to the pipeline.
###
### Michael B. Smith
### michael at TheEssentialExchange dot come
### May, 2012
###
### Parameters:
###
###	-group 			The short name for the group. This is looked
###				up to find the distinguishedName of the group.
###
###	-ExpandEmbeddedGroups	Whether to recurse and get the membership of
###				groups contained within the parent group. If
###				this option is specified, all embedded groups
###				are scanned (including groups embedded within
###				groups embedded within groups,etc. etc.).
###
###	-Verbose		Display to the host function entry/exit and
###				status information.
###
###	-VeryVerbose		Display to the host the ADSpath of each member
###				of the group (as well as write it to the pipe).
###
###	-Statistics		Display to the host some basic statistics about
###				the query (number of users, number of embedded
###				groups, number of contacts).
###

Param(
	[string]$group	= (throw "group must be specified"),
	[switch]$ExpandEmbeddedGroups,
	[switch]$Statistics,
	[switch]$Verbose,
	[switch]$VeryVerbose
)

### for the Statistics option

$script:groupUsers    = 0
$script:groupGroups   = 0
$script:groupContacts = 0

function msg
{
	if( -not $Verbose )
	{
		return
	}

	$str = ''
	foreach( $arg in $args )
	{
		$str += $arg
	}
	write-host $str
}

function vmsg
{
	if( -not $VeryVerbose )
	{
		return
	}
	msg $args
}

function Get-PrimaryGroupID
{
	Param(
		[string]$indent,
		[string]$ADSpath
	)

	msg "${indent}Get-PrimaryGroupId: enter, ADSpath = $adspath"

	[string]$pgToken = 'primaryGroupToken'

	### format of argument: LDAP://CN=Domain Users,CN=Users,DC=smithcons,DC=local

	$groupDE  = New-Object System.DirectoryServices.DirectoryEntry( $ADSpath )
	$searcher = New-Object System.DirectoryServices.DirectorySearcher( $groupDE )
	$searcher.Filter = "(objectClass=*)"

	$searcher.PropertiesToLoad.Add( $pgToken ) | Out-Null

	$result = $searcher.FindOne()
	if( $result -ne $null )
	{
		if( $result.Properties.Contains( $pgToken ) -eq $true )
		{
			msg "${indent}Get-PrimaryGroupId: exit, token = $($result.Properties.primarygrouptoken)"

			return $result.Properties.primarygrouptoken
		}
	}

	msg "${indent}Get-PrimaryGroupId: exit, token not found"
	return 0
}

function Search-PrimaryGroupID
{
	Param(
		[string]$indent,
		[string]$namingContext,
		[int]$primaryGroup,
		[hashtable]$dictionary
	)

	msg "${indent}Search-PrimaryGroupId: enter, namingcontext = '$namingContext', primaryGroup = $primaryGroup"

	$ldapFilter = "(primaryGroupID=$primaryGroup)"

	$directorySearcher = New-Object System.DirectoryServices.DirectorySearcher
	$directorySearcher.PageSize    = 1000
	$directorySearcher.SearchRoot  = ( "LDAP://" + $namingContext )
	$directorySearcher.SearchScope = "subtree"
	$directorySearcher.Filter      = $ldapFilter

	### load the properties we want

	$directorySearcher.PropertiesToLoad.Add( "distinguishedName" ) | Out-Null
	$directorySearcher.PropertiesToLoad.Add( "objectClass" )       | Out-Null

	$results = $directorySearcher.FindAll()
	if( $results -ne $null )
	{
		msg "${indent}Search-PrimaryGroupId: found $($results.Count) results"
		foreach( $result in $results )
		{
			$myadspath   = $result.Path
			$objCount    = $result.Properties.objectclass.count
			$objectClass = $result.Properties.objectclass[ $objCount - 1 ]

			if( $objectClass -eq 'user' )
			{
				if( $dictionary.$myadspath -eq 1 )
				{
					msg "${indent}Search-PrimaryGroupID: continue duplicate user"
					return
				}
				$dictionary.$myadspath = 1
				$script:groupUsers++
				write-output $myadspath
				vmsg "${indent}Search-PrimaryGroupId: $myadspath"
			}
			else
			{
				write-error "Invalid objectclass for primarygroupid: $objectClass"
			}
		}
	}
	else
	{
		msg "${indent}Search-PrimaryGroupID: result from FindAll() was null"
	}

	msg "${indent}Search-PrimaryGroupId: exit"
}

function Search-Group
{
	Param(
		[string]$indent,
		[string]$ADSpath,
		[hashtable]$dictionary
	)

	### based originally on http://msdn.microsoft.com/en-us/library/bb885125.aspx
	### but has bug-fixes and enhancements

	msg "${indent}Search-Group: enter, $ADSpath"

	$groupDE  = New-Object System.DirectoryServices.DirectoryEntry( $ADSpath )
	$searcher = New-Object System.DirectoryServices.DirectorySearcher( $groupDE )
	$searcher.Filter = "(objectClass=*)"

	[bool]$lastLoop = $false
	[bool]$quitLoop = $false

	[int]$step = 999
	[int]$low  = 0
	[int]$high = $step

	do {
		if( $lastLoop -eq $false )
		{
			[string]$member = 'member;range=' + $low.ToString() + '-' + $high.ToString()
		}
		else
		{
			[string]$member = 'member;range=' + $low.ToString() + '-' + '*'
		}
		msg "${indent}Search-Group: member = $member"

		$searcher.PropertiesToLoad.Clear()        | Out-Null
		$searcher.PropertiesToLoad.Add( $member ) | Out-Null

		$result = $searcher.FindOne()
		if( $result -eq $null )
		{
			### not sure what to do here
			msg "${indent}Search-Group: searcher failure"
			break
		}

		if( $result.Properties.Contains( $member ) -eq $true )
		{
			$entries = $result.Properties.$member
			msg "${indent}Search-Group: entries.Count = $($entries.Count)"
			foreach( $entry in $entries )
			{
				if( $ExpandEmbeddedGroups )
				{
					$memberObj   = [ADSI] "LDAP://$entry"
					$objectClass = $memberObj.objectClass.Item( $memberObj.objectClass.Count - 1 )
					$myadspath   = $memberObj.Path
					$memberObj   = $null
				}
				else
				{
					$myadspath   = $entry
					$objectClass = 'user'
				}
				write-output $myadspath ### output to pipeline

				switch( $objectClass )
				{
					'group'
						{
							if( $dictionary.$myadspath -eq 1 )
							{
								msg "${indent}Search-Group: continue duplicate group"
								continue
							}
							$dictionary.$myadspath = 1
							$script:groupGroups++
							vmsg "${indent}Search-Group: group $myadspath"
							Search-Group ( $indent + '  ' ) $myadspath $dictionary
						}
					'contact'
						{
							if( $dictionary.$myadspath -eq 1 )
							{
								msg "${indent}Search-Group: continue duplicate contact"
								continue
							}
							$dictionary.$myadspath = 1
							$script:groupContacts++
							vmsg "${indent}Search-Group: contact $myadspath"
						}
					'user'
						{
							if( $dictionary.$myadspath -eq 1 )
							{
								msg "${indent}Search-Group: continue duplicate user"
								continue
							}
							$dictionary.$myadspath = 1
							$script:groupUsers++
							vmsg "${indent}Search-Group: user $myadspath"
						}
					'foreignSecurityPrincipal'
						{
							### do nothing
						}
					default
						{
							write-error "Search-Group: unhandled objectClass as member of group: $objectClass"
						}
				}
			}

			### could just say: $quitLoop = $lastLoop
			### but it's not a worthwhile optimization
			### (due to a loss of clarity in WHY)

			if( $lastLoop -eq $true )
			{
				msg "${indent}Search-Group: set quitLoop = true"
				$quitLoop = $true
			}
		}
		else
		{
			if( $lastLoop -eq $true )
			{
				msg "${indent}Search-Group: set quitLoop = true"
				$quitLoop = $true
			}
			else
			{
				msg "${indent}Search-Group: set lastLoop = true"
				$lastLoop = $true
			}
		}

		if( $lastLoop -eq $false )
		{
			msg "${indent}Search-Group: old low = $low, old high = $high"
			$low  = $high + 1
			$high = $low  + $step
			msg "${indent}Search-Group: new low = $low, new high = $high"
		}

	} until( $quitLoop -eq $true )

	$object   = $null
	$searcher = $null
	$groupDE  = $null

	$primaryID = Get-PrimaryGroupId $indent $ADSpath
	if( $primaryID -gt 0 )
	{
		Search-PrimaryGroupId $indent $script:defaultNC $primaryId $dictionary
	}

	msg "${indent}Search-Group: exit, $ADSpath"
}

function Search-ADForGroup
{
	Param(
		[string]$indent,
		[string]$group
	)

	msg "${indent}Search-ADForGroup: enter, group = $group"

	### build the LDAP search to find the group distinguishedName from the provided short name

	$rootDSE    = [ADSI]"LDAP://RootDSE"
	$defaultNC  = $rootDSE.defaultNamingContext
	$ldapFilter = "(&(objectCategory=group)(name=$group))"
	$rootDSE    = $null

	$directorySearcher = New-Object System.DirectoryServices.DirectorySearcher
	$directorySearcher.PageSize    = 1000
	$directorySearcher.SearchRoot  = ( "LDAP://" + $defaultNC )
	$directorySearcher.SearchScope = "subtree"
	$directorySearcher.Filter      = $ldapFilter

	### Define the property we want (if we don't specify at least one property,
	### then "all default properties" get loaded - that's slower).

	$directorySearcher.PropertiesToLoad.Add( "distinguishedName"  ) | Out-Null

	$groups = $directorySearcher.FindAll()
	if( $groups -eq $null )
	{
		write-error "Search-ADForGroup: No such group found: $group"
	}
	elseif( $groups.Count -eq 1 )
	{
		$script:defaultNC = $defaultNC
		msg "${indent}Search-ADForGroup: exit, $($groups.Item( 0 ).Path)"

		return $groups.Item( 0 ).Path ### same as ADSpath in VBScript
	}
	else
	{
		write-error "Search-ADForGroup: Multiple groups were found that match: $group"
	}

	msg "${indent}Search-ADForGroup: exit, null"
	return $null
}

	###
	### Main
	###

	if( $VeryVerbose )
	{
		$Verbose = $true
	}

	$result = Search-ADForGroup '' $group
	if( $result -ne $null )
	{
		$dictionary = @{}

		Search-Group '' $result $dictionary

		if( $Statistics )
		{
			write-host " "
			write-host "Users: $($script:groupUsers)"
			write-host "Groups: $($script:groupGroups)"
			write-host "Contacts: $($script:groupContacts)"
		}

		$dictionary = $null
	}

 

Creating Lots of Exchange Mailboxes for Testing Purposes

I'm in the process of developing a large new script and to test it properly, I needed to create several thousand new users with mailboxes. While I've previously written scripts like this for Exchange 2003, I didn't have one handy for Exchange 2007 and later.

So I whipped one up and decided to share! As you can probably tell, this script will create, by default, 6,000 users and mailboxes. You do have to tell it the password to assign, and you should change the $upnDomain to be a proper choice for your environment.

Enjoy!


Param(
	[int]$minValue =    1,
	[int]$maxValue = 6000,
	[string]$upnDomain = 'smithcons.local'
)

$password = ( get-credential UserName-Not-Important ).password

for( $i = $minValue; $i -le $maxValue; $i++ )
{
	$user = "User" + $i.ToString()
	New-Mailbox $user `
		-alias $user `
		-userprincipalname ( $user + '@' + $upnDomain ) `
		-samaccountname $user `
		-firstname $user `
		-lastname $user `
		-password $password
}

'Done'

 

How to Make Your PowerShell Session into an Exchange Management Shell

In my last blog post, Determining the Exchange Version Without Using Get-ExchangeServer,I showed how to read the Exchange version from the registry. Shortly after posting that, the questions began to roll in about "how to make my PowerShell session behave like the Exchange Management Shell".

Well, it's pretty easy.

In the New-ExchangeConnection function, I use "dot-sourcing" to minimize the work. When you dot-source something, it means you are basically including the contents of the file into YOUR file, at that location. So, I use the same setup that EMS uses; just changed to work on the proper version of Exchange, plus the little extra magic that usually happens behind the scenes, but doesn't because it's happening in my script instead of in the "real" EMS.

So here it is!


###
### New-ExchangeConnection
###
### Create a connection to an Exchange server, in a version
### appropriate way.
###

function New-ExchangeConnection
{
	### Load the versioning function

	. .\Get-ExchangeVersion.ps1

	$exchangeVersion = Get-ExchangeVersion
	if( $exchangeVersion )
	{
		switch ( $exchangeVersion.Version )
		{
			'2007'
				{
					### This first segment is handled by a PSConsole file in EMS itself
					Get-PSSnapin Microsoft.Exchange.Management.PowerShell.Admin -EA 0
					if ($?)
					{
						## snap-in already loaded, do nothing
					}
					else
					{
						Add-PSSnapin Microsoft.Exchange.Management.PowerShell.Admin
					}

					### This line makes the rest of the magic happen
					. 'C:\Program Files\Microsoft\Exchange Server\bin\Exchange.ps1'


					return 1
				}
			'2010'
				{
					### with the advent of remote PowerShell in E14, almost all
					### of the magic gets hidden
					$credential = $null
					$global:remoteSession = $null
					. 'C:\Program Files\Microsoft\Exchange Server\V14\bin\RemoteExchange.ps1'
					Connect-ExchangeServer -auto

					return 1
				}
			default
				{
					write-error "Unsupported version $($exchangeVersion.Version)"
					return 0
				}
		}
	}

	write-error "This is not an Exchange server"
	return 0
}