Just a quick note.... Exchange Server 2013 Service Pack 1 has been released.

Among other changes, this version of Exchange Server provides support for installation on Windows Server 2012 R2 and provides support for Windows Server 2012 R2 domain controllers.

The blog post announcing the release is here. You can download the release here.

At the same time were releases for two legacy versions of Exchange: Update Rollup 5 for Exchange 2010 Service Pack 3 and Update Rollup 13 for Exchange 2007 Service Pack 3.

The announcement for those releases is here. At the time of this writing, no information is available about the contents of those rollups.

More information coming soon!

...

Follow me on twitter: @essentialexchange

I apologize!

This blog was offline for a couple of weeks.

It was completely beyond my control.

I am hosted by GoDaddy.com and they spent several days saying "this is your problem" before they finally admitted "oh, ok, this is our problem."

I do plan to move to another hosting platform in the next few months... but honestly, I still had planned that it would hosted by GoDaddy. I may now need to re-evaluate that decision.

Again, I apologize.

I will try to ensure this does not happen again.

Regards,
Michael B.

.....

Please follow me on Twitter as @essentialexch

Posted by michael | with no comments
Filed under:

Most companies have a set of primary Internet domains (Exchange accepted domains) that they use to assign to users. However, a constant is that most companies also assign a secondary email address that has a domain which is identical for all of their users. That is true in my environment, and in that of all customers I have worked with in the past.

However, Exchange tends to generate (especially if you have migrated from legacy Exchange versions) far more email addresses than that for any given Exchange object (user, group, contact).

The script below reports on each user's primary SMTP address, plus a secondary SMTP address whose domain is specified as a parameter.

The script uses the Active Directory PowerShell module, which must be installed on the computer where this script is executed. The script does not use any Exchange specific features, thus the Exchange Management Shell is not required. I tested the script on Exchange 2010 and Exchange 2013, but it should work on Exchange 2007 as well.

A couple of techniques worth noting are used here. First, instead of a function, I use a filter. A filter is a very special kind of a function optimized for working with pipelines of objects. Inputs from the pipeline are passed to the filter using $_.

Second, I parse the proxyAddresses attribute. This attribute contains a list of all addresses that are assigned to a given user. It is an array (a collection) of all those addresses. Importantly, for each item in the collection, the address itself is prefixed by an address type. For SMTP addresses, the address type is "smtp". For FAX addresses, the address type is "fax". For X500 addresses, the address type is "x500". Etc. Also importantly, if the address type is capitalized, then a particular address is the primary address of that type - the default. If the address type is not capitalized, then the address is a secondary address. There may be any number of secondary addresses of a particular type. There may be only one primary address of a particular type.

In order to detect primary addresses, I use the "-ceq" operator in PowerShell. This is "cased equal". That means that the case of the letters is significant. By default, comparisons in PowerShell are not case significant.

Finally, Exchange objects will always have the proxyAddresses attribute populated. This fact is used to build LDAP query utilized to find Exchange objects.

I hope you find this useful!

##
## Get-PrimaryAndSecondary
##
## Michael B. Smith
## April, 2013
##
Param(
	[string]$secondaryDomain = "@TheEssentialExchange.com"
)

[int]$secondaryDomainLen = $secondaryDomain.Length

filter strip-Addresses
{
	$proxies = $_.proxyAddresses

	$primary   = ""
	$secondary = ""

	$object = "" | Select GivenName, Surname, sAMAccountName, PrimarySmtp, SecondarySmtp

	$object.GivenName      = $_.GivenName
	$object.SurName        = $_.SurName
	$object.sAMAccountName = $_.sAMAccountName

	foreach( $proxy in $proxies )
	{
		$len = $proxy.Length

		## note: "SMTP:".Length == 5

		## note: The primary SMTP address has a CAPITALIZED "SMTP:" prefix
		## all secondary SMTP addresses have a lowercase "smtp:" prefix

		## note: any interesting secondary proxy address will be longer than 
		## "SMTP:".Length + $secondaryDomainLen

		if( $len -gt 5 )
		{
			$prefix = $proxy.SubString( 0, 5 )
			$temp   = $proxy.SubString( 5 )	##strip off "smtp:", if present

			if( $prefix -ceq "SMTP:" )
			{
				$primary = $temp
				if( $secondary.Length -gt 0 )
				{
					break   ## we have both primary and secondary, 
						## we don't need to look any more
				}
			}
			elseif( $prefix -ceq "smtp:" -and $len -gt ( 5 + $secondaryDomainLen ) )
			{
				if( $temp.EndsWith( $secondaryDomain ) )
				{
					$secondary = $temp
					if( $primary.Length -gt 0 )
					{
						break   ## we have both primary and secondary, 
							## we don't need to look any more
					}
				}
			}
		}
	}

	$object.PrimarySmtp   = $primary
	$object.SecondarySmtp = $secondary

	$object
}

Import-Module ActiveDirectory

Get-AdUser -LDAPFilter "(&(objectCategory=user)(proxyAddresses=*))" `
	-Properties GivenName, SurName, proxyAddresses -ResultSetSize $null | 
	strip-Addresses

Follow me on twitter, @essentialexch

Posted by michael | with no comments

This is an update of my post Determining the Exchange Version - without using Get-ExchangeServer, from April 25, 2012. Since then, Exchange 2013 has been released! I've had several requests from people who are not PowerShell scripters to update that function. So here it is. I have repeated the text from the prior below.

If you write lots of Exchange scripts (as I do), and have several different versions of Exchange on which you need to run those scripts (as I do); you soon see the need for being able to determine what version of Exchange a particular server is running - and you may need to do this outside of the Exchange Management Shell.

This may be necessary because of different behaviors that are required with the Exchange Management Shell depending on the version of Exchange. It also may be required because the version of Exchange pre-dates the Exchange Management Shell (i.e., Exchange 2003). As an additional rationale, your script may need to load the Exchange Management Shell and cannot do that properly without knowing the version of Exchange that is being targeted (the process differs between Exchange 2007 and Exchange 2010 and Exchange 2013).

Thus, I've written a couple of functions that I wrap in a script to give me that information. Get-ExchangeServer, the function presented in this post, returns a simple object containing the name of the server examined, plus the version of Exchange that is running on that server. That is, in C pseudo-syntax:

struct result {
    string Name;
    string Version;
}

If the result of the function is $null, then the version could not be determined and the server targeted either does not exist or is (very likely) not running Exchange. If the server does not exist (or the firewall on the server prevents remote management via WMI) then an error is displayed.

The version information about Exchange is stored in the registry of each Exchange server. The script below shows some techniques for accessing the registry to obtain string (reg_sz) and 32-bit short integer (reg_dword) values while using PowerShell. Note that PowerShell has multiple mechanisms for accessing this information, including the so-called Registry Provider. I personally find using the WMI functions to be a bit easier to handle.

You can include these functions directly into your PowerShell profile, or you can dot-source the function on an as-needed basis.

Without further ado, enjoy!

###
### Get-ExchangeVersion
###
### Return the version of the specified Exchange server
###
### 2013-04-11
###	Updated to support Exchange Server 2013 and E16
###

Set-StrictMode -Version 2.0

$HKCR = 2147483648
$HKCU = 2147483649
$HKLM = 2147483650

function RegRead
{
	Param(
		[string]$computer,
		[int64] $hive,
		[string]$keyName,
		[string]$valueName,
		[ref]   $value,
		[string]$type = 'reg_sz'
	)

	[string]$fn = "RegRead:" ## function name

	try {
		$wmi = [wmiclass]"\\$computer\root\default:StdRegProv"
		if( $wmi -eq $null )
		{
			return 1
		}
	}
	catch {
		$error[0]
		write-error "$fn Could not open WMI access to $computer"
		return 1
	}

	switch ( $type )
	{
		'reg_sz'
			{
				$r = $wmi.GetStringValue( $hive, $keyName, $valueName )
				$value.Value = $r.sValue
			}
		'reg_dword'
			{
				$r = $wmi.GetDWORDValue( $hive, $keyName, $valueName )
				$value.Value = $r.uValue
			}
		default
			{
				write-error "$fn Unsupported type: $type"
			}
	}

	$wmi = $null

	return $r.ReturnValue
}

function Get-ExchangeVersion
{
	Param(
		[string]$computer = '.'
	)

	[string]$fn = "Get-ExchangeVersion:" ## function name

	if( $computer -eq '.' -or [String]::IsNullOrEmpty( $computer ) )
	{
		$computer = $env:ComputerName
	}

	## Exchange E16 (assumption!)
	## HKLM\Software\Microsoft\ExchangeServer\v16\Setup
	## MsiProductMajor (DWORD 16)

	## Exchange 2013
	## HKLM\Software\Microsoft\ExchangeServer\v15\Setup
	## MsiProductMajor (DWORD 15)

	## Exchange 2010
	## HKLM\Software\Microsoft\ExchangeServer\v14\Setup
	## MsiProductMajor (DWORD 14)

	## Exchange 2007
	## HKLM\SOFTWARE\Microsoft\Exchange\Setup
	## MsiProductMajor (DWORD 8)

	## Exchange 2003
	## HKLM\SOFTWARE\Microsoft\Exchange\Setup
	## Services Version (DWORD 65)

	$v = 0

	$i = RegRead $computer $HKLM 'Software\Microsoft\ExchangeServer\v16\Setup' 'MsiProductMajor' ( [ref] $v ) 'reg_dword'
	if( ( $i -eq 0 ) -and ( $v -eq 16 ) )
	{
		$obj = "" | Select Name, Version
		$obj.Name = $computer
		$obj.Version = 'E16'
		return $obj
	}

	$i = RegRead $computer $HKLM 'Software\Microsoft\ExchangeServer\v15\Setup' 'MsiProductMajor' ( [ref] $v ) 'reg_dword'
	if( ( $i -eq 0 ) -and ( $v -eq 15 ) )
	{
		$obj = "" | Select Name, Version
		$obj.Name = $computer
		$obj.Version = '2013'
		return $obj
	}

	$i = RegRead $computer $HKLM 'Software\Microsoft\ExchangeServer\v14\Setup' 'MsiProductMajor' ( [ref] $v ) 'reg_dword'
	if( ( $i -eq 0 ) -and ( $v -eq 14 ) )
	{
		$obj = "" | Select Name, Version
		$obj.Name = $computer
		$obj.Version = '2010'
		return $obj
	}

	$i = RegRead $computer $HKLM 'Software\Microsoft\Exchange\Setup' 'MsiProductMajor' ( [ref] $v ) 'reg_dword'
	if( ( $i -eq 0 ) -and ( $v -eq 8 ) )
	{
		$obj = "" | Select Name, Version
		$obj.Name = $computer
		$obj.Version = '2007'
		return $obj
	}

	$i = RegRead $computer $HKLM 'Software\Microsoft\Exchange\Setup' 'Services Version' ( [ref] $v ) 'reg_dword'
	if( ( $i -eq 0 ) -and ( $v -eq 65 ) )
	{
		$obj = "" | Select Name, Version
		$obj.Name = $computer
		$obj.Version = '2003'
		return $obj
	}

	### almost certainly not an Exchange server

	return $null
}

Please follow me on Twitter, @essentialexch

Posted by michael | with no comments

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

Posted by michael | with no comments

One of the features added to PowerShell v2 (and of course continued with PowerShell v3) is splatting. Without going into extreme detail (there are plenty of other blogs and books that do that), splatting allows you to pass parameters to a PowerShell cmdlet via an associative array (that is a fancy name for a hash table).

The hash table contains the parameter name as a key and the parameter's value as a value. The hash table contains two matched arrays, the keys array and the values array. For example, if a hash table contains three values, then accessing $hash.keys[ 1 ] will provide the name of a given hash entry and $hash.values[ 1 ] will provide the corresponding value for that entry.

The big deal behind splatting is that it allows you to easily construct custom parameter lists that get passed to cmdlets based on the special needs of a calling script. That sounds dense. :) But it is a good thing. For example, if you want to call a cmdlet with different parameters based on the time of day, or on a particular computer, or whether your script was started with a switch parameter, or whether a particular string parameter has a value - then splatting is for you.

Splatting has a somewhat hidden advantage. Switch parameters require special handling under normal circumstances. With splatting, no special handling is required. You set a switch to $true or $false, without using special spacing and/or a colon.

I use splatting extensively in scripts I have developed since PowerShell v2 was released. Today, my friend Carl Webster, the Accidental Citrix Admin, asked about a way to simplify calling cmdlets in certain of his scripts. My answer was "use splatting". After a couple of back-and-forth emails, about the best way to use splatting, I concluded that the easiest thing to do was to generalize a routine to provide splatting for a cmdlet and pass that over to Carl.

After further thought and evolution on the basic concept of the routine I sent to Carl, I decided it could be a great deal more powerful (get it - powerful vs. PowerShell? HAHAHAHA) if I added in some logging capabilities and provided for the suppression of empty parameters. Thus was born Invoke-Splat. I have already taken Invoke-Splat and used it to simplify several of my own scripts.

Invoke-Splat takes the cmdlet to invoke as a parameter, plus all of the parameters for that cmdlet, as "parameter value" pairs, plus a couple of optional switch parameters to control Invoke-Splat itself. Invoke-Splat is dependent on PowerShell's named parameter matching, combined with positional parameter matching, and that everything that does not meet either of those, is stored in the special argument $args.

The splatNotEmpty switch ensures that parameters whose values evaluate as either null or empty strings are not passed as parameters to the called cmdlet. This is especially important for parameters that cannot normally be null or empty!

The splatDebug switch causes the cmdlet and each of the "parameter value" pairs to be output to the calling host.

The splatCmdlet string parameter is (obviously) the name of the cmdlet that will be executed with the other parameters.

Note that the other parameters may be empty!

For example:

Invoke-Splat Get-Process ID $PID FileVersionInfo $false -splatDebug

and

Invoke-Splat Get-Process ID $PID Name '' -splatDebug -splatNotEmpty

Without further ado, here is Invoke-Splat.

function Invoke-Splat
{
	Param(
		[string]$splatCmdlet,
		[switch]$splatNotEmpty,
		[switch]$splatDebug
	)

	## $args[ 0 .. n ] are matching "parameter argument" pairs

	$splats = @{}
	for( $i = 0; $i –lt $args.Length; $i = $i + 2 )
	{
		if( $splatNotEmpty )
		{
			## don’t pass arguments with empty strings
			if( ( $args[ $i + 1 ] –as [string] ).Length –gt 0 )  
			{
				$splats.$( $args[ $i ] ) = $args[ $i + 1 ]
			}
		}
		else
		{
			$splats.$( $args[ $i ] ) = $args[ $i + 1 ]
		}
	}
	if( $splatDebug )
	{
		write-host -ForeGroundColor Yellow "DEBUG: Invoke-Splat: cmdlet = $splatCmdlet"
		foreach( $entry in $splats.Keys )
		{
			write-host -ForeGroundColor Yellow "DEBUG: Invoke-Splat: entry: $entry = $($splats.$entry)"
		}
	}

	& $splatCmdlet @splats
}

Please follow me on Twitter: @essentialexch.

Thanks for your visit!

Posted by michael | with no comments
Filed under: ,

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.]

Posted by michael | with no comments

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 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.

 

Posted by michael | with no comments

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.

Posted by michael | with no comments
Filed under: ,

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

Posted by michael | with no comments

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.

Posted by michael | with no comments

I do bit shifting in PowerShell all the time. It's quite necessary when you are working with option values in Active Directory, WMI, and various Win32 interfaces. .NET tends to use enumerations, which are tad easier to deal with in PowerShell.

I had never felt it necessary to break my routines out before, but I saw a posting on this topic by another individual today and I just thought he was making it harder than it had to be. I wanted to present another option to the community for these types of routines. I'm not putting down his work in any way - it was quite ingenious. I never would've thought of doing it that way.

Just FYI, 'shr' is an abbreviation for 'shift right' and correspondingly 'shl' is an abbreviation for 'shift left'. When I was in college (in the stone age) those function names were used in "Pascal extension libraries" and in "Fortran libraries" for performing shift operations. Shift operations are built-in as native operations to many (most?) compiled languages such as C and its various dialects.

Enjoy.

###
### Bit-shift operations
### bit-shift.ps1
###
### for bytes and short integers
###
### Michael B. Smith
### michael at TheEssentialExchange.com
### May, 2012
###

$bitsPerByte = 8	## [byte]
$bitsperWord = 16	## [int16] or [uint16]

function shift-left( [int]$valuesize, [int]$mask, $val, [int]$bits )
{
	if( $bits -ge $valuesize )
	{
		return 0
	}
	if( $bits -eq 0 )
	{
		return $val
	}
	if( $bits -lt 0 )
	{
		write-error "Can't shift by a negative value of bits"
		return -1
	}

	### it's possible to write this so that you never
	### overshift and generate an overflow. it's easier
	### to use a larger variable and mask at the end.

	[int]$result = $val
	for( $i = 0; $i -lt $bits; $i++ )
	{
		$result *= 2
	}

	return ( $result -band $mask )
}

function shift-right( [int]$valuesize, [int]$mask, $val, [int]$bits )
{
	if( $bits -ge $valuesize )
	{
		return 0
	}
	if( $bits -eq 0 )
	{
		return $val
	}
	if( $bits -lt 0 )
	{
		write-error "Can't shift by a negative value of bits"
		return -1
	}

	for( $i = 0; $i -lt $bits; $i++ )
	{
		## normally PowerShell does banker's rounding (well, .NET does)
		## we have to override that here to get true integer division.
		$val = [Math]::Floor( $val / 2 )
	}

	return $val
}

function shl-byte( [byte]$val, [int]$bits )
{
	$result = shift-left $bitsPerByte 0xff $val $bits
	if( $result -lt 0 )
	{
		return $result
	}

	return ( [byte]$result )
}

function shr-byte( [byte]$val, [int]$bits )
{
	$result = shift-right $bitsPerByte 0xff $val $bits
	if( $result -lt 0 )
	{
		return $result
	}

	return ( [byte]$result )
}

function shl-word( [uint16]$val, [int]$bits )
{
	$result = shift-left $bitsPerWord 0xffff $val $bits
	if( $result -lt 0 )
	{
		return $result
	}

	return ( [uint16]$result )
}

function shr-word( [uint16]$val, [int]$bits )
{
	$result = shift-right $bitsPerWord 0xffff $val $bits
	if( $result -lt 0 )
	{
		return $result
	}

	return ( [uint16]$result )
}

function shl( $val, [int]$bits )
{
	if( $val -is [byte] )
	{
		return ( shl-byte $val $bits )
	}
	elseif( $val -is [int16] )
	{
		return [int16]( shl-word $val $bits )
	}
	elseif( $val -is [uint16] )
	{
		return ( shl-word $val $bits )
	}
	elseif( ( $val -lt 65536 ) -and ( $val -ge 0 ) ) ### pretend it's uint16
	{
		return ( shl-word ( [uint16]$val ) $bits )
	}

	write-error "value is an invalid type"
	return -1
}

function shr( $val, [int]$bits )
{
	if( $val -is [byte] )
	{
		return ( shr-byte $val $bits )
	}
	elseif( $val -is [int16] )
	{
		return [int16]( shr-word $val $bits )
	}
	elseif( $val -is [uint16] )
	{
		return ( shr-word $val $bits )
	}
	elseif( ( $val -lt 65536 ) -and ( $val -ge 0 ) ) ### pretend it's uint16
	{
		return ( shr-word ( [uint16]$val ) $bits )
	}

	write-error "value is an invalid type"
	return -1
}
Posted by michael | with no comments
Filed under: ,

I can't speak as to whether this is supported or not, but if you want DPM 2010 to be able to back up a SQL 2012 database instance, then you need to make a simple security change. Otherwise, DPM 2010 reports that "The replica is not consistent."

In words, NT AUTHORITY\System must be granted Sysadmin permissions on the database instance. NT AUTHORITY\System, by default, has public permissions on the instance in SQL 2012. However, in earlier versions of SQL, NT AUTHORITY\System has Sysadmin permissions at installation.

Below, the first figure identifies the specific login in the SQL Server Management Studio that has to be modified. The second figure identifies the box that needs to be checked. After checking the box, click OK to commit the change.

After this, you can open the DPM Console and for all of those SQL 2012 databases that are not consistent, go to the Protection workspace, right-click on the database(s), and then click on "Perform Consistency Check".

Figure 1 - The NT AUTHORITY\System login

Figure 2 - Setting the Sysadmin Server Instance Role

Posted by michael | with no comments
Filed under: ,

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
	}
More Posts Next page »