Removing Old Emailaddresses/ProxyAddresses in Exchange 2007

I have a client who has multiple business units. Each business unit is placed into its own Organizational Unit (OU) in Active Directory and the business unit has its own unique Email Address Policy (EAP) that (using a RecipientFilter) assigns specific email addresses to all users in that OU.

The client has changed the name of one of their business units, and as of today, the old email addresses were to “go away”.

There is no in-built functionality for automatically removing email addresses. You can modify the EAP to specify a new address template as the default, but even when you remove the old address template, that email address is not removed from the user object (Exchange has worked this way since at least Exchange 2000, Exchange 5.5 used a somewhat different model).

What do you do? Well, you use PowerShell of course! This “one-liner” does the job:

get-mailbox -organizationalunit ad.example.com/BusinessUnitOU/users |% { $a = $_.emailaddresses; $b = $_.emailaddresses; foreach($e in $a) { if ($e.tostring() -match "old-email-domain") { $b -= $e; } } ; $_ | set-mailbox -emailaddresses $b }

You can enter this as a single line (and I did). However, the command is easier to understand when you separate it out, as shown below:

get-mailbox -organizationalunit ad.example.com/BusinessUnitOU/users |% 
    { 
        $a = $_.emailaddresses; 
        $b = $_.emailaddresses; 
        foreach ($e in $a) 
        { 
            if ($e.tostring() -match "old-email-domain") 
            { 
                $b -= $e; 
            } 
         }

         $_ | set-mailbox -emailaddresses $b 
    }


The Get-Mailbox command specifies the unique organizational unit that we are looking at. In this case, Get-Mailbox will return a mailbox object for all mailboxes that exist within the ad.example.com/BusinessUnitOU/users organizational unit. Note that I have not used the distinguishedName format for the organizational unit, but instead the X.500 format. Either is acceptable. However, the X.500 format only requires quoting when you have spaces in your organizational unit names. The distinguishedName format must always be quoted or escaped due to its use of commas (and how PowerShell interprets commas in open text strings).

The “|%” syntax indicates that the output of the Get-Mailbox command is to be evaluated, for each object returned, based on the following script block. In this case, the script block begins on the next line with “{” in column 4 and terminates 12 lines later with a “}” in column 4.

First, we make two copies of the email address collection for the mailbox that is returned. Note that setting “$b = $a” would have a different behavior! Instead of creating a copy of the collection, it would create a reference – that is, $b would point to the same collection as $a, and the following loop would not work properly, as described in the next paragraph.

Next, we evaluate each email address contained within the email address collection ($a). If a particular email address matches the old email address domain, we remove that particular email address from the second collection ($b). If we removed it from the original collection ($a), then the foreach loop would fail. Not a good thing to have happen – which is why we have two collections!

After examining all of the email addresses in the collection, we pipe the mailbox object to Set-Mailbox using the new/adjusted collection. And we are done!

But you don’t need to understand the script in order to use the script. To use the script, replace the -organizationalunit parameter with the one you are wanting to change (or completely remove the parameter if you want to affect all mailboxes). Next, replace old-email-domain with the domain you are want to remove. That’s all it takes.

Until next time…

If there are things you would like to see written about, please let me know!

[Edit March 29, 2012]

Mike Crowley, another Exchange MVP, makes the following comment that I thought I would share:

Michael, this was very helpful, thanks again! I
needed to do this for one of my customers, however the address that needed to
‘go away’ was a parent domain of one that needed to stay (e.g. ‘domain.com’
needed to go away, but ‘subdomain.domain.com’ needed to stay). I modified
your script and ran the below:

get-mailbox -organizationalunit
ad.domain.com/BusinessUnitOU/users | % { $a = $_.emailaddresses; $b =
$_.emailaddresses; foreach($e in $a) { if (($e.tostring() -match
“domain.com”) -and ($e.tostring() -notmatch
“subdomain.domain.com”)) { $b -= $e; } } ; $_ | set-mailbox
-emailaddresses $b }

-Mike


Follow me on twitter: @EssentialExch

Removing Old Emailaddresses/ProxyAddresses in Exchange 2007

I have a client who has multiple business units. Each business unit is placed into its own Organizational Unit (OU) in Active Directory and the business unit has its own unique Email Address Policy (EAP) that (using a RecipientFilter) assigns specific email addresses to all users in that OU.

The client has changed the name of one of their business units, and as of today, the old email addresses were to “go away”.

There is no in-built functionality for automatically removing email addresses. You can modify the EAP to specify a new address template as the default, but even when you remove the old address template, that email address is not removed from the user object (Exchange has worked this way since at least Exchange 2000, Exchange 5.5 used a somewhat different model).

What do you do? Well, you use PowerShell of course! This “one-liner” does the job:

get-mailbox -organizationalunit ad.example.com/BusinessUnitOU/users |% { $a = $_.emailaddresses; $b = $_.emailaddresses; foreach($e in $a) { if ($e.tostring() -match "old-email-domain") { $b -= $e; } } ; $_ | set-mailbox -emailaddresses $b }

You can enter this as a single line (and I did). However, the command is easier to understand when you separate it out, as shown below:

get-mailbox -organizationalunit ad.example.com/BusinessUnitOU/users |% 
    { 
        $a = $_.emailaddresses; 
        $b = $_.emailaddresses; 
        foreach ($e in $a) 
        { 
            if ($e.tostring() -match "old-email-domain") 
            { 
                $b -= $e; 
            } 
         }

         $_ | set-mailbox -emailaddresses $b 
    }


The Get-Mailbox command specifies the unique organizational unit that we are looking at. In this case, Get-Mailbox will return a mailbox object for all mailboxes that exist within the ad.example.com/BusinessUnitOU/users organizational unit. Note that I have not used the distinguishedName format for the organizational unit, but instead the X.500 format. Either is acceptable. However, the X.500 format only requires quoting when you have spaces in your organizational unit names. The distinguishedName format must always be quoted or escaped due to its use of commas (and how PowerShell interprets commas in open text strings).

The “|%” syntax indicates that the output of the Get-Mailbox command is to be evaluated, for each object returned, based on the following script block. In this case, the script block begins on the next line with “{” in column 4 and terminates 12 lines later with a “}” in column 4.

First, we make two copies of the email address collection for the mailbox that is returned. Note that setting “$b = $a” would have a different behavior! Instead of creating a copy of the collection, it would create a reference – that is, $b would point to the same collection as $a, and the following loop would not work properly, as described in the next paragraph.

Next, we evaluate each email address contained within the email address collection ($a). If a particular email address matches the old email address domain, we remove that particular email address from the second collection ($b). If we removed it from the original collection ($a), then the foreach loop would fail. Not a good thing to have happen – which is why we have two collections!

After examining all of the email addresses in the collection, we pipe the mailbox object to Set-Mailbox using the new/adjusted collection. And we are done!

But you don’t need to understand the script in order to use the script. To use the script, replace the -organizationalunit parameter with the one you are wanting to change (or completely remove the parameter if you want to affect all mailboxes). Next, replace old-email-domain with the domain you are want to remove. That’s all it takes.

Until next time…

If there are things you would like to see written about, please let me know!

[Edit March 29, 2012]

Mike Crowley, another Exchange MVP, makes the following comment that I thought I would share:

Michael, this was very helpful, thanks again! I
needed to do this for one of my customers, however the address that needed to
‘go away’ was a parent domain of one that needed to stay (e.g. ‘domain.com’
needed to go away, but ‘subdomain.domain.com’ needed to stay). I modified
your script and ran the below:

get-mailbox -organizationalunit
ad.domain.com/BusinessUnitOU/users | % { $a = $_.emailaddresses; $b =
$_.emailaddresses; foreach($e in $a) { if (($e.tostring() -match
“domain.com”) -and ($e.tostring() -notmatch
“subdomain.domain.com”)) { $b -= $e; } } ; $_ | set-mailbox
-emailaddresses $b }

-Mike


Follow me on twitter: @EssentialExch

Microsoft MVP 2009…

Yesterday I was notified that my MVP was renewed for another year (MVPs are awarded for one-year at a time, and there is a quarterly cycle of Jan/Apr/Jul/Oct). This is my sixth MVP award and part of it is due to you, my loyal readers! Officially, my MVP award is for “Windows Server System – Exchange Server”, but that’s generally shortened to “Exchange MVP”.

If you aren’t familiar with the MVP program, check out http://mvp.support.microsoft.com and my personal MVP profile at https://mvp.support.microsoft.com/profile/Michael.B..

Thanks for your ongoing support!

Until next time…

If there are things you would like to see written about, please let me know!


Follow me on twitter: @EssentialExch

Getting the Contents of an Active Directory Integrated DNS Zone

Microsoft has long offered (where “long” means “since Windows Server 2003”) the “dnscmd.exe” program to control the actions of a DNS server. And, if you installed the adminpack (RSAT-Tools-DNS on Vista/Server 2008 and above) you could perform this control on a remote workstation/server as well.

However, the output of dnscmd leaves….something…. to be desired, especially when you want to format and manipulate that output.

Using a prior blog post of mine (Hex and Decimal Output In PowerShell), I more-or-less reverse-engineered the format of how Active Directory Integrated zones (DNS domains) are stored in Active Directory. For all of my personal testing, the program below was successful at displaying the output and decoding it properly. However, that doesn’t mean I decode everything!!! Just those things I could test.

A key desire of mine was to be able to get the contents of an ADI zone into a CSV (comma-separated-value) format; so of course that option is present. I also wanted proper timeout values for aging/scavenging, so of course that happens by default (which dnscmd can’t do at all).

If the script below doesn’t work for you, I’d be interested in hearing about it, and why it doesn’t work. Of course, I make no promises, but I’ll probably fix those issues. 🙂

Without further ado…

##
## dns-dump.ps1
##
## Michael B. Smith
## michael at smithcons dot com
## http://TheEssentialExchange.com/blogs/michael
## May/June, 2009
##
## Use as you wish, no warranties expressed, implied or explicit.
## Works for me, but it may not for you.
## If you use this, I would appreciate an attribution.
##

Param(
	[string]$zone,
	[string]$dc,
	[switch]$csv,
	[switch]$help
)

function dumpByteArray([System.Byte[]]$array, [int]$width = 9)
{
	## this is only used if we run into a record format
	## we don't understand.

	$hex = ""
	$chr = ""
	$int = ""

	$i = $array.Count
	"Array contains {0} elements" -f $i
	$index = 0
	$count = 0
	while ($i-- -gt 0)
	{
		$val = $array[$index++]

		$hex += ("{0} " -f $val.ToString("x2"))

		if ([char]::IsLetterOrDigit($val) -or 
		    [char]::IsPunctuation($val)   -or 
		   ([char]$val -eq " "))
		{
			$chr += [char]$val
		}
		else
		{
			$chr += "."
		}

		$int += "{0,4:N0}" -f $val

		$count++
		if ($count -ge $width)
		{
			"$hex $chr $int"
			$hex = ""
			$chr = ""
			$int = ""
			$count = 0
		}		
	}

	if ($count -gt 0)
	{
		if ($count -lt $width)
		{
			$hex += (" " * (3 * ($width - $count)))
			$chr += (" " * (1 * ($width - $count)))
			$int += (" " * (4 * ($width - $count)))
		}

		"$hex $chr $int"
	}
}

function dword([System.Byte[]]$arr, [int]$startIndex)
{
	## convert four consecutive bytes in $arr into a
	## 32-bit integer value... if I had bit-manipulation
	## primitives in PowerShell, I'd use them instead
	## of the multiply operator.

	$res = $arr[$startIndex]
	$res = ($res * 256) + $arr[$startIndex + 1]
	$res = ($res * 256) + $arr[$startIndex + 2]
	$res = ($res * 256) + $arr[$startIndex + 3]

	return $res
}

function analyzeArray([System.Byte[]]$arr, [System.Object]$var)
{
	$nameArray = $var.distinguishedname.ToString().Split(",")
	$name = $nameArray[0].SubString(3)

	## AGE is stored backwards. The most-significant-byte comes
	## last, instead of first, unlike all the other 32-bit values.
	$age = $arr[23]
	$age = ($age * 256) + $arr[22]
	$age = ($age * 256) + $arr[21]
	$age = ($age * 256) + $arr[20]
	if ($age -ne 0)
	{
		## hours since January 1, 1601 (start of Windows epoch)
		## there is a long-and-dreary way to do this manually,
		## but get-date makes it trivial to do the conversion.
		$timestamp = (get-date -year 1601 -month 1 -day 1 -hour 0 -minute 0 -second 0).AddHours($age)
	}

	$ttl = dword $arr 12

	if ($arr[0] -eq 4 -and $arr[1] -eq 0)
	{
		# "A" record
		$ip = "{0}.{1}.{2}.{3}" -f $arr[24], $arr[25], $arr[26], $arr[27]

		if ($csv)
		{
			$formatstring = "{0},{1},{2},{3},{4}"
		}
		else
		{
			$formatstring = "{0,-30}`t{1,-24}`t{2}`t{3}`t{4}"
		}

		if ($age -eq 0)
		{
			$formatstring -f $name, "[static]", $ttl, "A", $ip
		}
		else
		{
			$formatstring -f $name, ("[" + $timestamp.ToString() + "]"), $ttl, "A", $ip
		}
	}
	elseif ((($arr[0] -eq 80) -or ($arr[0] -eq 82)) -and $arr[1] -eq 0)
	{
		# "SOA" record
		# "Start-Of-Authority"

		$nslen = $arr[44]
		$segments = $arr[45]
		$index = 46
		$nsname = ""
		while ($segments-- -gt 0)
		{
			$segmentlength = $arr[$index++]
			while ($segmentlength-- -gt 0)
			{
				$nsname += [char]$arr[$index++]
			}
			if ($segments -gt 0) { $nsname += "." }
		}
		$priserver = $nsname
		# "Primary server: $nsname"

		$index += 1
		$nslen = $arr[$index++]
		$segments = $arr[$index++]

		$nsname = ""
		while ($segments-- -gt 0)
		{
			$segmentlength = $arr[$index++]
			while ($segmentlength-- -gt 0)
			{
				$nsname += [char]$arr[$index++]
			}
			if ($segments -gt 0) { $nsname += "." }
		}
		# "Responsible party: $nsname"
		$resparty = $nsname

		#"TTL: $ttl"
		# "Age: $age"

####		$unk1 = dword $arr 16
####		"Unknown1: $unk1"

		$serial = dword $arr 24
		# "Serial: $serial"

		$refresh = dword $arr 28
		# "Refresh: $refresh"

		$retry = dword $arr 32
		# "Retry: $retry"

		$expires = dword $arr 36
		# "Expires: $expires"

		$minttl = dword $arr 40
		# "Minimum TTL: $minttl"

		if ($age -eq 0)
		{
			$agestr = "[static]"
		}
		else
		{
			$agestr = "[" + $timestamp.ToString() + "]"
		}

		if ($csv)
		{
			$formatstring = "{0},{1},{2},{3},{4},{5},{6},{7},{8},{9},{10}"

			$formatstring -f $name, $agestr, $ttl, `
				"SOA", $priserver, $resparty, `
				$serial, $refresh, $retry, `
				$expires, $minttl
		}
		else
		{
			$formatstring = "{0,-30}`t{1,-24}`t{2}`t{3}"

			$formatstring -f $name, $agestr, $ttl, "SOA"
			(" " * 32) + "Primary server: $priserver"
			(" " * 32) + "Responsible party: $resparty"
			(" " * 32) + "Serial: $serial"
			(" " * 32) + "TTL: $ttl"
			(" " * 32) + "Refresh: $refresh"
			(" " * 32) + "Retry: $retry"
			(" " * 32) + "Expires: $expires"
			(" " * 32) + "Minimum TTL (default): $minttl"
		}

		#### dumpByteArray $arr
	}
	elseif ((($arr[0] -eq 32) -or ($arr[0] -eq 30)) -and $arr[1] -eq 0)
	{
		# "NS" record
		$nslen = $arr[24]
		$segments = $arr[25]
		$index = 26
		$nsname = ""
		while ($segments-- -gt 0)
		{
			$segmentlength = $arr[$index++]
			while ($segmentlength-- -gt 0)
			{
				$nsname += [char]$arr[$index++]
			}
			if ($segments -gt 0) { $nsname += "." }
		}

		if ($csv)
		{
			$formatstring = "{0},{1},{2},{3},{4}"
		}
		else
		{
			$formatstring = "{0,-30}`t{1,-24}`t{2}`t{3}`t{4}"
		}

		if ($age -eq 0)
		{
			$formatstring -f $name, "[static]", $ttl, "NS", $nsname
		}
		else
		{
			$formatstring -f $name, ("[" + $timestamp.ToString() + "]"), $ttl, "NS", $nsname
		}
	}
	elseif ((($arr[0] -eq 36) -or ($arr[0] -eq 38)) -and $arr[1] -eq 0)
	{
		# "SRV" record

		$port = $arr[28]
		$port = ($port * 256) + $arr[29]

		$weight = $arr[26]
		$weight = ($weight * 256) + $arr[27]

		$pri = $arr[24]
		$pri = ($pri * 256) + $arr[25]

		$nslen = $arr[30]
		$segments = $arr[31]
		$index = 32
		$nsname = ""
		while ($segments-- -gt 0)
		{
			$segmentlength = $arr[$index++]
			while ($segmentlength-- -gt 0)
			{
				$nsname += [char]$arr[$index++]
			}
			if ($segments -gt 0) { $nsname += "." }
		}

		if ($csv)
		{
			$formatstring = "{0},{1},{2},{3},{4},{5}"
		}
		else
		{
			$formatstring = "{0,-30}`t{1,-24}`t{2}`t{3} {4} {5}"
		}

		if ($age -eq 0)
		{
			$formatstring -f `
				$name, "[static]", `
				$ttl, "SRV", `
				("[" + $pri.ToString() + "][" + $weight.ToString() + "][" + $port.ToString() + "]"), `
				$nsname
		}
		else
		{
			$formatstring -f `
				$name, ("[" + $timestamp.ToString() + "]"), `
				$ttl, "SRV", `
				("[" + $pri.ToString() + "][" + $weight.ToString() + "][" + $port.ToString() + "]"), `
				$nsname
		}

	}
	else
	{
		$name
		$var.distinguishedname.ToString()
		dumpByteArray $arr
	}

}

function processAttribute([string]$attrName, [System.Object]$var)
{
	$array = $var.$attrName.Value
####	"{0} contains {1} rows of type {2} from {3}" -f $attrName, $array.Count, $array.GetType(), $var.distinguishedName.ToString()

	if ($array -is [System.Byte[]])
	{
####		dumpByteArray $array
####		" "
		analyzeArray $array $var
####		" "
	}
	else
	{
		for ($i = 0; $i -lt $array.Count; $i++)
		{
####			dumpByteArray $array[$i]
####			" "
			analyzeArray $array[$i] $var
####			" "
		}
	}
}

function usage
{
"
.\dns-dump -zone  [-dc ] [-csv] |
	   -help

dns-dump will dump, from Active Directory, a particular named zone. 
The zone named must be Active Directory integrated.

Zone contents can vary depending on domain controller (in regards
to replication and the serial number of the SOA record). By using
the -dc parameter, you can specify the desired DC to use. Otherwise,
dns-dump uses the default DC.

Usually, output is formatted for display on a workstation. If you
want CSV (comma-separated-value) output, specify the -csv parameter.
Use out-file in the pipeline to save the output to a file.

Finally, to produce this helpful output, you can specify the -help
parameter.

This command is basically equivalent to (but better than) the:

	dnscmd /zoneprint 
or
	dnscmd /enumrecords  '@'

commands.

Example 1:

	.\dns-dump -zone essential.local -dc win2008-dc-3

Example 2:

	.\dns-dump -help

Example 3:

	.\dns-dump -zone essential.local -csv |
            out-file essential.txt -encoding ascii

	Note: the '-encoding ascii' is important if you want to
	work with the file within the old cmd.exe shell. Otherwise,
	you can usually leave that off.
"
}

	##
	## Main
	##

	if ($help)
	{
		usage
		return
	}

	if ($args.Length -gt 0)
	{
		write-error "Invalid parameter specified"
		usage
		return
	}

	if (!$zone)
	{
		throw "must specify zone name"
		return
	}

	$root = [ADSI]"LDAP://RootDSE"
	$defaultNC = $root.defaultNamingContext

	$dn = "LDAP://"
	if ($dc) { $dn += $dc + "/" }
	$dn += "DC=" + $zone + ",CN=MicrosoftDNS,CN=System," + $defaultNC

	$obj = [ADSI]$dn
	if ($obj.name)
	{
		if ($csv)
		{
			"Name,Timestamp,TTL,RecordType,Param1,Param2"
		}

		#### dNSProperty has a different format than dNSRecord
		#### processAttribute "dNSProperty" $obj

		foreach ($record in $obj.psbase.Children)
		{
			####	if ($record.dNSProperty) { processAttribute "dNSProperty" $record }
			if ($record.dnsRecord)   { processAttribute "dNSRecord"   $record }
		}
	}
	else
	{
		write-error "Can't open $dn"
	}

	$obj = $null

Until next time…

If there are things you would like to see written about, please let me know!


Follow me on twitter: @EssentialExch

Getting the Contents of an Active Directory Integrated DNS Zone

Microsoft has long offered (where “long” means “since Windows Server 2003”) the “dnscmd.exe” program to control the actions of a DNS server. And, if you installed the adminpack (RSAT-Tools-DNS on Vista/Server 2008 and above) you could perform this control on a remote workstation/server as well.

However, the output of dnscmd leaves….something…. to be desired, especially when you want to format and manipulate that output.

Using a prior blog post of mine (Hex and Decimal Output In PowerShell), I more-or-less reverse-engineered the format of how Active Directory Integrated zones (DNS domains) are stored in Active Directory. For all of my personal testing, the program below was successful at displaying the output and decoding it properly. However, that doesn’t mean I decode everything!!! Just those things I could test.

A key desire of mine was to be able to get the contents of an ADI zone into a CSV (comma-separated-value) format; so of course that option is present. I also wanted proper timeout values for aging/scavenging, so of course that happens by default (which dnscmd can’t do at all).

If the script below doesn’t work for you, I’d be interested in hearing about it, and why it doesn’t work. Of course, I make no promises, but I’ll probably fix those issues. 🙂

Without further ado…

##
## dns-dump.ps1
##
## Michael B. Smith
## michael at smithcons dot com
## http://TheEssentialExchange.com/blogs/michael
## May/June, 2009
##
## Use as you wish, no warranties expressed, implied or explicit.
## Works for me, but it may not for you.
## If you use this, I would appreciate an attribution.
##

Param(
	[string]$zone,
	[string]$dc,
	[switch]$csv,
	[switch]$help
)

function dumpByteArray([System.Byte[]]$array, [int]$width = 9)
{
	## this is only used if we run into a record format
	## we don't understand.

	$hex = ""
	$chr = ""
	$int = ""

	$i = $array.Count
	"Array contains {0} elements" -f $i
	$index = 0
	$count = 0
	while ($i-- -gt 0)
	{
		$val = $array[$index++]

		$hex += ("{0} " -f $val.ToString("x2"))

		if ([char]::IsLetterOrDigit($val) -or 
		    [char]::IsPunctuation($val)   -or 
		   ([char]$val -eq " "))
		{
			$chr += [char]$val
		}
		else
		{
			$chr += "."
		}

		$int += "{0,4:N0}" -f $val

		$count++
		if ($count -ge $width)
		{
			"$hex $chr $int"
			$hex = ""
			$chr = ""
			$int = ""
			$count = 0
		}		
	}

	if ($count -gt 0)
	{
		if ($count -lt $width)
		{
			$hex += (" " * (3 * ($width - $count)))
			$chr += (" " * (1 * ($width - $count)))
			$int += (" " * (4 * ($width - $count)))
		}

		"$hex $chr $int"
	}
}

function dword([System.Byte[]]$arr, [int]$startIndex)
{
	## convert four consecutive bytes in $arr into a
	## 32-bit integer value... if I had bit-manipulation
	## primitives in PowerShell, I'd use them instead
	## of the multiply operator.

	$res = $arr[$startIndex]
	$res = ($res * 256) + $arr[$startIndex + 1]
	$res = ($res * 256) + $arr[$startIndex + 2]
	$res = ($res * 256) + $arr[$startIndex + 3]

	return $res
}

function analyzeArray([System.Byte[]]$arr, [System.Object]$var)
{
	$nameArray = $var.distinguishedname.ToString().Split(",")
	$name = $nameArray[0].SubString(3)

	## AGE is stored backwards. The most-significant-byte comes
	## last, instead of first, unlike all the other 32-bit values.
	$age = $arr[23]
	$age = ($age * 256) + $arr[22]
	$age = ($age * 256) + $arr[21]
	$age = ($age * 256) + $arr[20]
	if ($age -ne 0)
	{
		## hours since January 1, 1601 (start of Windows epoch)
		## there is a long-and-dreary way to do this manually,
		## but get-date makes it trivial to do the conversion.
		$timestamp = (get-date -year 1601 -month 1 -day 1 -hour 0 -minute 0 -second 0).AddHours($age)
	}

	$ttl = dword $arr 12

	if ($arr[0] -eq 4 -and $arr[1] -eq 0)
	{
		# "A" record
		$ip = "{0}.{1}.{2}.{3}" -f $arr[24], $arr[25], $arr[26], $arr[27]

		if ($csv)
		{
			$formatstring = "{0},{1},{2},{3},{4}"
		}
		else
		{
			$formatstring = "{0,-30}`t{1,-24}`t{2}`t{3}`t{4}"
		}

		if ($age -eq 0)
		{
			$formatstring -f $name, "[static]", $ttl, "A", $ip
		}
		else
		{
			$formatstring -f $name, ("[" + $timestamp.ToString() + "]"), $ttl, "A", $ip
		}
	}
	elseif ((($arr[0] -eq 80) -or ($arr[0] -eq 82)) -and $arr[1] -eq 0)
	{
		# "SOA" record
		# "Start-Of-Authority"

		$nslen = $arr[44]
		$segments = $arr[45]
		$index = 46
		$nsname = ""
		while ($segments-- -gt 0)
		{
			$segmentlength = $arr[$index++]
			while ($segmentlength-- -gt 0)
			{
				$nsname += [char]$arr[$index++]
			}
			if ($segments -gt 0) { $nsname += "." }
		}
		$priserver = $nsname
		# "Primary server: $nsname"

		$index += 1
		$nslen = $arr[$index++]
		$segments = $arr[$index++]

		$nsname = ""
		while ($segments-- -gt 0)
		{
			$segmentlength = $arr[$index++]
			while ($segmentlength-- -gt 0)
			{
				$nsname += [char]$arr[$index++]
			}
			if ($segments -gt 0) { $nsname += "." }
		}
		# "Responsible party: $nsname"
		$resparty = $nsname

		#"TTL: $ttl"
		# "Age: $age"

####		$unk1 = dword $arr 16
####		"Unknown1: $unk1"

		$serial = dword $arr 24
		# "Serial: $serial"

		$refresh = dword $arr 28
		# "Refresh: $refresh"

		$retry = dword $arr 32
		# "Retry: $retry"

		$expires = dword $arr 36
		# "Expires: $expires"

		$minttl = dword $arr 40
		# "Minimum TTL: $minttl"

		if ($age -eq 0)
		{
			$agestr = "[static]"
		}
		else
		{
			$agestr = "[" + $timestamp.ToString() + "]"
		}

		if ($csv)
		{
			$formatstring = "{0},{1},{2},{3},{4},{5},{6},{7},{8},{9},{10}"

			$formatstring -f $name, $agestr, $ttl, `
				"SOA", $priserver, $resparty, `
				$serial, $refresh, $retry, `
				$expires, $minttl
		}
		else
		{
			$formatstring = "{0,-30}`t{1,-24}`t{2}`t{3}"

			$formatstring -f $name, $agestr, $ttl, "SOA"
			(" " * 32) + "Primary server: $priserver"
			(" " * 32) + "Responsible party: $resparty"
			(" " * 32) + "Serial: $serial"
			(" " * 32) + "TTL: $ttl"
			(" " * 32) + "Refresh: $refresh"
			(" " * 32) + "Retry: $retry"
			(" " * 32) + "Expires: $expires"
			(" " * 32) + "Minimum TTL (default): $minttl"
		}

		#### dumpByteArray $arr
	}
	elseif ((($arr[0] -eq 32) -or ($arr[0] -eq 30)) -and $arr[1] -eq 0)
	{
		# "NS" record
		$nslen = $arr[24]
		$segments = $arr[25]
		$index = 26
		$nsname = ""
		while ($segments-- -gt 0)
		{
			$segmentlength = $arr[$index++]
			while ($segmentlength-- -gt 0)
			{
				$nsname += [char]$arr[$index++]
			}
			if ($segments -gt 0) { $nsname += "." }
		}

		if ($csv)
		{
			$formatstring = "{0},{1},{2},{3},{4}"
		}
		else
		{
			$formatstring = "{0,-30}`t{1,-24}`t{2}`t{3}`t{4}"
		}

		if ($age -eq 0)
		{
			$formatstring -f $name, "[static]", $ttl, "NS", $nsname
		}
		else
		{
			$formatstring -f $name, ("[" + $timestamp.ToString() + "]"), $ttl, "NS", $nsname
		}
	}
	elseif ((($arr[0] -eq 36) -or ($arr[0] -eq 38)) -and $arr[1] -eq 0)
	{
		# "SRV" record

		$port = $arr[28]
		$port = ($port * 256) + $arr[29]

		$weight = $arr[26]
		$weight = ($weight * 256) + $arr[27]

		$pri = $arr[24]
		$pri = ($pri * 256) + $arr[25]

		$nslen = $arr[30]
		$segments = $arr[31]
		$index = 32
		$nsname = ""
		while ($segments-- -gt 0)
		{
			$segmentlength = $arr[$index++]
			while ($segmentlength-- -gt 0)
			{
				$nsname += [char]$arr[$index++]
			}
			if ($segments -gt 0) { $nsname += "." }
		}

		if ($csv)
		{
			$formatstring = "{0},{1},{2},{3},{4},{5}"
		}
		else
		{
			$formatstring = "{0,-30}`t{1,-24}`t{2}`t{3} {4} {5}"
		}

		if ($age -eq 0)
		{
			$formatstring -f `
				$name, "[static]", `
				$ttl, "SRV", `
				("[" + $pri.ToString() + "][" + $weight.ToString() + "][" + $port.ToString() + "]"), `
				$nsname
		}
		else
		{
			$formatstring -f `
				$name, ("[" + $timestamp.ToString() + "]"), `
				$ttl, "SRV", `
				("[" + $pri.ToString() + "][" + $weight.ToString() + "][" + $port.ToString() + "]"), `
				$nsname
		}

	}
	else
	{
		$name
		$var.distinguishedname.ToString()
		dumpByteArray $arr
	}

}

function processAttribute([string]$attrName, [System.Object]$var)
{
	$array = $var.$attrName.Value
####	"{0} contains {1} rows of type {2} from {3}" -f $attrName, $array.Count, $array.GetType(), $var.distinguishedName.ToString()

	if ($array -is [System.Byte[]])
	{
####		dumpByteArray $array
####		" "
		analyzeArray $array $var
####		" "
	}
	else
	{
		for ($i = 0; $i -lt $array.Count; $i++)
		{
####			dumpByteArray $array[$i]
####			" "
			analyzeArray $array[$i] $var
####			" "
		}
	}
}

function usage
{
"
.\dns-dump -zone  [-dc ] [-csv] |
	   -help

dns-dump will dump, from Active Directory, a particular named zone. 
The zone named must be Active Directory integrated.

Zone contents can vary depending on domain controller (in regards
to replication and the serial number of the SOA record). By using
the -dc parameter, you can specify the desired DC to use. Otherwise,
dns-dump uses the default DC.

Usually, output is formatted for display on a workstation. If you
want CSV (comma-separated-value) output, specify the -csv parameter.
Use out-file in the pipeline to save the output to a file.

Finally, to produce this helpful output, you can specify the -help
parameter.

This command is basically equivalent to (but better than) the:

	dnscmd /zoneprint 
or
	dnscmd /enumrecords  '@'

commands.

Example 1:

	.\dns-dump -zone essential.local -dc win2008-dc-3

Example 2:

	.\dns-dump -help

Example 3:

	.\dns-dump -zone essential.local -csv |
            out-file essential.txt -encoding ascii

	Note: the '-encoding ascii' is important if you want to
	work with the file within the old cmd.exe shell. Otherwise,
	you can usually leave that off.
"
}

	##
	## Main
	##

	if ($help)
	{
		usage
		return
	}

	if ($args.Length -gt 0)
	{
		write-error "Invalid parameter specified"
		usage
		return
	}

	if (!$zone)
	{
		throw "must specify zone name"
		return
	}

	$root = [ADSI]"LDAP://RootDSE"
	$defaultNC = $root.defaultNamingContext

	$dn = "LDAP://"
	if ($dc) { $dn += $dc + "/" }
	$dn += "DC=" + $zone + ",CN=MicrosoftDNS,CN=System," + $defaultNC

	$obj = [ADSI]$dn
	if ($obj.name)
	{
		if ($csv)
		{
			"Name,Timestamp,TTL,RecordType,Param1,Param2"
		}

		#### dNSProperty has a different format than dNSRecord
		#### processAttribute "dNSProperty" $obj

		foreach ($record in $obj.psbase.Children)
		{
			####	if ($record.dNSProperty) { processAttribute "dNSProperty" $record }
			if ($record.dnsRecord)   { processAttribute "dNSRecord"   $record }
		}
	}
	else
	{
		write-error "Can't open $dn"
	}

	$obj = $null

Until next time…

If there are things you would like to see written about, please let me know!


Follow me on twitter: @EssentialExch

Named Properties, What Lies Ahead…

Microsoft is preparing to make a potentially breaking change that deals with how Exchange Server 2007 handles named properties. This change is scheduled to be a part of Service Pack 2 and of Exchange Server 2010 at RTM.

This change involves the promotion (i.e., the visibility to non-MAPI clients) of X-* headers in incoming e-mail messages. This can potentially affect the operation of e-mail clients that depend on POP-3 and IMAP (and even Outlook, if you are using rules that look at headers).

Unlike certain earlier versions of Exchange, Exchange 2007 and above do not have a STM file – this means that incoming Internet e-mail is ALWAYS translated to MAPI format and that all headers are not necessarily retained – especially if they are X-* headers. This can cause a fidelity issue (i.e., you can’t reproduce EXACTLY what you received). For probably 99.999% of customers – this isn’t an issue. At least, that’s the opinion.

So…. Microsoft is looking for input and they’ve asked for Exchange MVPs to help get the word out.

To express your opinion, see Named Properties, Round 2: What lies Ahead.

Until next time…

If there are things you would like to see written about, please let me know!


Follow me on twitter: @EssentialExch

Named Properties, What Lies Ahead…

Microsoft is preparing to make a potentially breaking change that deals with how Exchange Server 2007 handles named properties. This change is scheduled to be a part of Service Pack 2 and of Exchange Server 2010 at RTM.

This change involves the promotion (i.e., the visibility to non-MAPI clients) of X-* headers in incoming e-mail messages. This can potentially affect the operation of e-mail clients that depend on POP-3 and IMAP (and even Outlook, if you are using rules that look at headers).

Unlike certain earlier versions of Exchange, Exchange 2007 and above do not have a STM file – this means that incoming Internet e-mail is ALWAYS translated to MAPI format and that all headers are not necessarily retained – especially if they are X-* headers. This can cause a fidelity issue (i.e., you can’t reproduce EXACTLY what you received). For probably 99.999% of customers – this isn’t an issue. At least, that’s the opinion.

So…. Microsoft is looking for input and they’ve asked for Exchange MVPs to help get the word out.

To express your opinion, see Named Properties, Round 2: What lies Ahead.

Until next time…

If there are things you would like to see written about, please let me know!


Follow me on twitter: @EssentialExch

Removing the Last Exchange 2003 Server…

Microsoft has lots of guidance about removing the last Exchange 2003 server from an administrative group (see KB 822931) and I definitely recommend you give that a read. They also have a technet article about removing the last Exchange 2003 server from your organization (after you’ve upgraded to Exchange 2007, of course). You should give that a read too.

But if you do these things lots of times (and if you are a consultant, you probably do – or if you play in your Exchange lab a lot, you probably do too); you just need a quick list of reminders. Here is the list I take onsite with me, when I’m removing an Exchange 2003 server:

Verify that all mailbox moves are complete (either within the console or “get-mailbox -server “).

Verify that all public folder moves are complete (“get-publicfolderstatistics -server “). Note: if they aren’t this can be tough. Check the scripts in $ExScripts like MoveAllReplicas and RemoveReplicaFromPFRecursive.

If you do NOT have an Edge server, verify that the Default receive connector allows Anonymous connections.

If you DO have an Edge server, verify that Edge synchronization has occurred and is operational (“test-edgesynchronization”).

Move all Offline Address Book generation servers to servers that will continue to exist

Move the “Default Public Store” on all Exchange 2003 Mailbox Stores to point to a Exchange 2007 PF

Delete the Public Folder databases from the Exchange 2003 server (note: this is not a required step, but if you can’t do this, then de-install of Exchange will fail – so this is a good place to go ahead and figure that out).

Delete both sides of Interop RGCs (and verify that they are the only RGCs still present: “get-routinggroupconnector”)

Delete SMTP connectors from ESM on the Exchange 2003 server (you can do this from the EMC on Exchange 2007 later, but you’ll get a version warning)

Evaluate Recipient Policies and delete all unused RPs from ESM on the Exchange 2003 server

Verify status of all recipient policies (ensure that Mailbox Manager boxes are unchecked)

Note: you may want to record Mailbox Manager settings to recreate MRM policies on Exchange 2007 to replace the MM policies

Relocate the PF heirarchy (in Exchange 2003 ESM, right-click the Exchange 2007 Administrative Group, select Next -> PF Container, drag PF object from the Exchange 2003 Administrative Group to the Exchange 2007 Administrative Group)

Delete the Domain Recipient Update Service(s) from ESM on Exchange 2003

Point the Enterprise Recipient Update Server to an Exchange 2007 mailbox server (or delete the RUS from Active Directory using adsiedit or LDP)

Uninstall Exchange

…..

Now, this is one of those postings where I have to say “this works for me”. I bet it’ll work for you too – but I can’t guarantee it!

Until next time…

If there are things you would like to see written about, please let me know!


Follow me on twitter: @EssentialExch