The case of the missing Free/Busy public folder pt.2

In an earlier blog, I described the situation where a customer had improperly decommissioned Exchange 2003 Administrative Groups and ended up with invalid, orphaned legacyExchangeDN values causing all sorts of issues, most Public Folder / Free Busy related. Read more on this story here.

In the blog, I had two options on how to proceed:

  1. Edit the legacyExchangeDN attribute of the users affected;
  2. Recreate the Free/Busy public folder.

In the first blog, I described how to fix the situation using the 2nd option. Here’s how to solve this if you have no Exchange 2003 server left and want to go with the other option.

To fix this situation by changing the legacyExchangeDN values, you need to perform the following steps:

  1. Identify all mailboxes containing improper legacyExchangeDN values;
  2. For all those mailboxes, add the current legacyExchangeDN value as an x500 address;
  3. Fix the current legacyExchangeDN.

Note that by adding the invalid legacyExchangeDN value as an X500 address, we make sure (responding to) old e-mail messages or nickname entries can resolve properly.

You could use tools like ADModify to bulk edit those values. However, you also achieve the same result using a little PowerShell (surprise!), as shown in the following script:

Note: Use the script at your own risk. I cannot accept any responsibility for consequences when using this in your production environment. Before using it, prepare it in a lab environment first: test, test, test! Also, this script fixes invalid legacyExchangeDN values; it does not fix any related invalid settings, like delegates; that might be something for a next version when there’s demand for it.

$oldDN="/o=ADATUM/ou=First Administrative Group"
$newDN="/o=CONTOSO/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients"
$mbx= get-mailbox -Filter "LegacyExchangeDN -like '$($oldDN)/*'"
$mbx | ForEach {
    $x5= "x500:"+ $_.legacyExchangeDN
    Set-Mailbox $_.Identity -EmailAddresses @{Add=$x5}
    $User = [ADSI]("LDAP://"+$_.distinguishedName)
    $newDN= $newDN+ ‚Äú/cn=‚ÄĚ+ $_.Name
    $User.Put("legacyExchangeDN", $newDN)
    $User.SetInfo()
}

To use the script, replace the $oldDN value with your old, invalid legacyExchangeDN value (as reported in the Event Log entries with Event ID 14031). Set $newDN to your new legacyExchangeDN value; the default value of would be in the format ‚Äú/o=<Organisation Name>/ou=<Administrative Group, i.e. Exchange Administrative Group (FYDIBOHF23SPDLT)>/cn=Recipients/cn=<Name>‚ÄĚ.

If you have any questions, drop them in the comments below.

For all the PowerShell purists: Sometimes I prefer readability over trying to fit everything one 1 line. After all, this isn‚Äôt an Obfuscated Code Contest ūüôā

The case of the missing Free/Busy public folder

A customer who recently migrated to Exchange 2010 and was still in the co-existence setup with Exchange 2003, reported lots of users experiencing issues with regards to Free/Busy information. Symptoms were inaccurate or missing Free/Busy information, which especially gets annoying when scheduling appointments.

It turned out these users were using Outlook 2003; users on Outlook 2007 or later experienced no issues. As you probably know, Outlook 2003 still utilizes public folders to publish users’ Free/Busy information. This information is consulted by Outlook 2003 when scheduling appointments; Outlook 2007 or later uses Exchange Web Services for this purpose.

A quick look in the Eventlog revealed lots of 14031 errors were generated by the FreeBusy Assistant:

Err14031

This told us Exchange was unable to store Free/Busy information in a public folder, in this case /o=EUROPE/ou=First Administrative Group. A quick look at the Public Folder Management Console in Exchange 2010 showed that the folder didn’t exist. Since the Free/Busy public folder to be used by an Outlook 2003 user is determined by the legacyExchangeDN attribute, this was the cause of the issue.

The reason for the folder’s absence was unknown so one can only speculate. My best guess is improper decommissioning of the organization and administrative group originally hosting those users, identified by that “orphaned” legacyExchangeDN.

With the Free/Busy public folder missing and the original Exchange infrastructure gone, there are two alternatives to resolving this issue, apart from upgrading clients to a recent version of Outlook of course:

  1. Edit the legacyExchangeDN attribute of the users affected;
  2. Recreate the Free/Busy public folder.

The 1st option has consequences for the users, like being able to reply to earlier e-mail by co-workers. This can be resolved by adding the current legacyExchangeDN as an X500 address to the current set of e-mail addresses, but that also makes things a bit messy.

The 2nd option is to recreate the Free/Busy public folder; Here’s how to proceed:

  1. First, using the Exchange System Manager (luckily, Exchange 2003 was still present), create an Administrative Group, e.g. First Administrative Group
  2. Then, using ADSIedit.msc, navigate to CN=<Administrative Group>,CN=Administrative Groups,CN=<Organization>,CN=Microsoft Exchange,CN=Services,CN=Configuration,DC=<domain>
  3. Right-click the Administrative Group, e.g. First Administrative Group, and click Properties. There, edit the legacyExhangeDN attribute. Set it to match the missing Free/Busy public folder, e.g. /o=EUROPE/ou=First Administrative Group
  4. Next, edit the siteFolderServer attribute. Set it to match the distinguishedName of a a public folder database. Note that you can pick the Exchange 2003 as well as Exchange 2010 Public Folder database here. In this example, I picked the Exhange 2003 public folder database, hence the storage group (SG1):

siteFolderServer

Now we need to wait for the store to recreate the Free/Busy public folder during its maintenance cycle, which may take up to 24h. If you’re in a hurry, and the situation allows you because of the service interruption, you can also restart the Information Store. When the Information Store has created the Free/Busy public folder, event 3047 is logged by the MSExchangeIS Public Store:

Recreate_FB_PF

To verify this, startup the Public Folder Management Console or any other Public Folder management tool, and you’ll see the newly created folder:

PFMC_Appear

After a while you’ll notice Outlook 2003 users are now storing their Free/Busy information in this public folder and Free/Busy will work again for these users. You can verify clients are storing Free/Busy information using EMS, ExFolders or MFCMAPI, e.g.:


Finally, don’t forget to create replicas of this new Free/Busy public folder when appropriate.

Creating Batches of Legacy Mailbox Move Requests

When migrating users from Exchange 2003 to Exchange 2010, you get to a point where you actually want to move all those Exchange 2003 mailboxes. When you have a decent environment and a lot of mailboxes are involved, you perhaps may want to move those mailboxes in a more phased, batch-oriented fashion.

If so, perhaps the following one-liner may come in handy, which is to be executed from the Exchange Management Shell:

Get-Mailbox -RecipientTypeDetails LegacyMailbox -ResultSize Unlimited¬†| Where { ($_.MailboxMoveStatus -eq ‘None’) -and ( $_.WhenChanged -lt (Get-Date).AddDays(-1)) } | Select -First 100 | New-MoveRequest

I’ll talk you through the one-liner:

  • We’ll start off by selecting all mailboxes using Get-Mailbox using an unlimited result size (defaults to 1,000). By selecting only “LegacyMailbox” using the¬†RecipientTypeDetails parameter, we’ll only select the Exchange 2003 mailboxes;
  • Next, we filter the those mailboxes on the following properties:
    • MailboxMoveStatus. By selecting “None”, we get mailboxes not in the process of being moved;
    • WhenChanged. We select mailboxes unaltered in the last 24 hours to accomodate¬†for users currently accessing their mailbox (since it’s an offline move). For this purpose, we take the current timestamp (Get-Date) and subtract 1 day (by adding -1 day which is the same).
  • Then¬† we select only the first N (the sample uses 100) mailboxes of the result¬†(or lower if there are less mailboxes elegible so far);
  • Finally we pipe that to New-MoveRequest. We don’t mention any Remote parameters so it’s a local move. Also, by omitting the target database, we let¬†Exchange select¬†an¬†Exchange 2010¬†database. ¬†This is done in a round-robin fashion (as you can see below) and does all the work for us like checking availability¬†of the database as well the auto provisioning status of those databases or servers.

Now you can start or schedule this and check back in the morning on the results. When situation requires, you can start off using smaller batches increasing things depending on the results, ultimately leaving out the Select and date condition altogether.

And don’t forget to clean up those move requests afterwards.

MapiExceptionNotFound: Unable to delete mailbox

Johan Veldhuis blogged about the problem we encountered when moving mailboxes cross-forest from Exchange 2003 to Exchange 2010 (also see my 2-part article on cross-forest move). We received the following mailbox move failure message:

Warning: Unable to update AD information for the source mailbox at the end of the move.¬† Error details: An error occurred while updating a user object after the move operation. ‚Äď> Failed to find the address type object in Active Directory for address type ‚ÄúSMTP:AMD64‚Ä≥.

Failed to cleanup the source mailbox after the move.
Error details: MapiExceptionNotFound: Unable to delete mailbox. (hr=0√ó8004010f, ec=-2147221233)

So, besides an Exchange 2010 mailbox the Exchange 2003 mailbox was still there, and AD attributes weren’t changed on the source AD object (e.g. HomeMDB still pointing to Exchange 2003). This is a potential serious condition as incoming e-mail might be delivered to the Exchange 2003 mailbox instead of the new Exchange 2010 mailbox, depending on your setup and speed to remove the source mailbox.

To assist with this issue Microsoft released hotfix 940012 for Exchange 2003. It will generate an event on Exchange 2003 when it cannot remove the mailbox. It will also log which folder might be the culprit. After detecting and identifying¬† the mailbox with issues, you still need to remove it manually (steps should be known; if not, they’re contained in the kb article).

You can view kb article 940012 here and download the related hotfix for Exchange Server 2003 SP2(!) through here.

MAPI Client and Collaboration Data Objects 1.2.1

The MAPI and CDO libraries, still required by some applications or scripts talking to Exchange, have been updated.

The libaries are meant to unwind the application and storage layers, making applications and code using these libraries independent of the underlying Exchange or Outlook version. It also enables certain applications/scripts to run remotely (mostly apps/scripts from the pre-PowerShell era). Only problem you can encounter is when applications/scripts make use of certain functionality only to be found in certain Exchange/Outlook versions, requiring specific MAPI or CDO library versions.

These libraries may be required by certain 3rd party applications, scripts, etc. that need to interface with Exchange, e.g. Blackberry Enterprise Server, the Transporter Suite (for Notes-Exchange co-existence/interoperability), Quest Exchange tools, etc. etc.

Note that MapiCDO release follows Exchange Server 2003’s lifecycle and it is recommended you upgrade your application or port your application/script to and start leveraging new Exchange interfaces, e.g. Exchange Web Services, Outlook Object Model or the Outlook MAPI Client Library.

ExchangeMapiCDO 1.2.1  version 6.5.8165.0 can be download here.

Rollup time

Besides the Exchange 2007 SP2 RU4 of yesterday and this morning’s Exchange 2010 RTM RU3, Microsoft also released Exchange 2007 SP1 Rollup 10 (KB981407) and a security fix for Exchange 2003 SP2 (KB976702).

Both Exchange 2007 SP1 RU10 and the Exchange 2003 SP2 update fix the issue documented in security bulletin MS10-024, being a vulnerability issue in Microsoft Exchange and the Windows SMTP Service which could lead to a denial of service.

You can download Exchange 2007SP1 RU10 here and the Exchange 2003 SP2 fix here.

And pssst .. there’s even a Exchange 2000 SP3 fix here.

Cross-Forest Mailbox Move (2)

Note: This is part 2; part 1 can be found here.

After the post on experiences regarding Cross-Forest Mailbox Move, the problems with the “sample” Powershell script and the script created in good ol’ VB, I got lots of requests to publish the script. After thinking this over, I made it ready for publishing. That means stripping excessive code and changing domain names etc. to a more descriptive labels.

The script does require some explanation:

  • The script uses 1 input file (users.txt) and produces 1 output file (output.log) (included below);
  • The reason for sending output to screen (optional) as well as file is to be able to check it properly (e.g. using notepad) when running it for a set of users;
  • users.txt contains a single line with the source and target account names. This is the same file we used for ADMT imput. Reason for having a source as well as a new account name is that in the migration process account are renamed. ADMT can do this for you, but the script will need both the old and the new name name to connect to the objects and copy/set several attributes;
  • It’s VB, had been kept simple and didn’t went through a code beautifier. That means no full function headers, input/output descriptions or extensive error handling;
  • Modify the constants using information from your environment, e.g. source/target domain, servers, LegacyExchangeDN etc.
  • The script uses fixed servers. This is to make sure we’re talking to the same server(s) as ADMT and to prevent replication issues because of lag;
  • Use the script at your own risk. I cannot accept any responsibility for consequences when using this in your production environment;
  • Use it in a lab environment first; test, test, test!

Users.txt

SourceName,TargetSAM
jtest,jtest

CrossForestMovePrep.vbs

'*--------------------------------------------------------------------------------
'* Name     : PrepareForestMove
'* Created By      : Michel de Rooij
'* E-mail    : michel@eightwone.com
'* Date            : 20100217
'* Version    : 0.22
'*--------------------------------------------------------------------------------
'* Changes:
'* 0.21 Initial version
'* 0.22 Made changes to address single-value proxyAddress attributes
'*--------------------------------------------------------------------------------

Option Explicit

Const strUserfile            = "users.txt"
Const strOutputFileName            = "output.log"
Const DEBUGOUTPUT            = 1

Const conSourceServer               = "dc.olddomain.nl"
Const conSourceDomain                = "olddomain.nl"
Const contargetServer                = "dc.newdomain.com"
Const conTargetDomain                = "newdomain.com"

Const conLegacyExchangeDN            = "/o=NEWEXORG/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients/cn="
Const conTargetEmailDomain            = "target.com"

' AD putex cmds
Const ADS_PROPERTY_CLEAR = 1
Const ADS_PROPERTY_UPDATE = 2
Const ADS_PROPERTY_APPEND = 3
Const ADS_PROPERTY_DELETE = 4

' FileSystem
Const ForWriting =2
Const ForReading =1

'*********************************************************
' MAIN
'*********************************************************

Dim oFSO, strFile, objFile, hOutputFileHandle, bProcessLine, strLine
Dim arrUsers, strUser, strCmd, strNewUser

Set oFSO = CreateObject("Scripting.FileSystemObject")
Set hOutputFileHandle= oFSO.OpenTextFile( strOutputFileName, ForWriting, True)

debug("Start")

strFile= strUserFile

if NOT oFSO.fileExists( strFile) then
 die( "Input file "& strFile& " does not exist")
end if

debug("Reading names from "& strFile)
set objFile= oFSO.OpenTextFile( strFile, ForReading, True)

while not objFile.atEndOfStream

 bProcessLine= True
 strLine= objFile.readLine

 if isEmpty(strLine) then
 bProcessLine= False
 Else
 If left(strLine, 1)= ";" Then
 bProcessLine= False
 Else
 If inStr( strLine, ",") > 0 Then
 ' Line OK
 arrUsers= split( strLine, ",")
 strUser= arrUsers(0)
 strNewUser= arrUsers(1)

 If strUser= "SourceName" Then
 ' Input file header, skip        
 bProcessLine= False
 End If
 Else
 bProcessLine= False
 debug("** INFO: Skipping line "& strLine)
 End If    
 End If
 End If

 If bProcessLine Then

 debug(strNewUser& ": Syncing Exchange Attributes from "& struser)
 syncAttributes strUser, conSourceServer, conSourceDomain, strNewUser, conTargetServer, conTargetDomain

 End If
Wend

debug("Finished")

objFile.Close
hOutputFileHandle.Close  

set hOutputFileHandle= Nothing
set objFile= Nothing
set oFSO= Nothing

wscript.quit(0)

'*******************************************************************
' Purpose: output to screen when DEBUGOUTPUT is 1, always to file
'*******************************************************************
Function debug(strMsg)
 hOutputFileHandle.write ("["& FormatDateTime(now(),4) & "] "& strMsg & chr(13)& chr(10))
 if DEBUGOUTPUT=1 then
 wscript.echo strMsg
 end if
End Function

'*********************************************************
' Purpose: terminate with message
'*********************************************************
Function die(strMsg)
 wscript.echo strMsg
 wscript.quit (1)
End Function

'*********************************************************
' displayString
' Returns string from varType item/elements
'*********************************************************
Function displayString( varObj)
 Dim tmp, item
 tmp= ""
 select case VarType( varObj)

 Case vbEmpty
 tmp= "(Empty)"
 Case vbNull
 tmp= "(Null)"
 Case vbInteger, vbLong, vbSingle, vbDouble, vbByte, vbDecimal, vbCurrency, vbDate, vbBoolean
 tmp= cStr( varObj)
 Case vbString
 tmp= varObj
 Case vbObject
 tmp= "(Object)"
 Case vbvariant
 tmp= "(Variant)"
 Case 8209
 tmp= "("& OctetToHexStr( varObj)& ")"
 Case vbArray, 8204
 For each item in varObj
 If tmp="" Then
 tmp= tmp+ item
 Else
 tmp= tmp+ ", "+item
 End If
 Next
 tmp= "["& tmp& "]"
 Case Else

 End Select

 displaystring= tmp& " #"& varType( varObj)

End Function

'*********************************************************
' OctetToHexStr
' Convert OctetString (byte array) to Hex string.
'*********************************************************
Function OctetToHexStr (arrbytOctet)
 Dim k
 OctetToHexStr = ""
 For k = 1 To Lenb (arrbytOctet)
 OctetToHexStr = OctetToHexStr & Right("0" & Hex(Ascb(Midb(arrbytOctet, k, 1))), 2)
 Next
End Function

'*********************************************************
' syncAttributes
' migrates attributes from source to target
'*********************************************************
Function syncAttributes (strUser, SourceServer, SourceDomain, strNewUser, TargetServer, TargetDomain)

 dim strDNSource, strDNTarget, objSource, objTarget, n, strMail
 strDNSource= getDN( struser, SourceServer, SourceDomain, "")
 strDNTarget= getDN( strNewuser, TargetServer, TargetDomain, "")
 If strDNSource<>"" AND strDNTarget <> "" Then
 set objSource= getObject( "LDAP://"& SourceServer& "/"& strDNSource)
 set objTarget= getObject( "LDAP://"& TargetServer& "/"& strDNTarget)

 copyAttribute "mail", objSource, objTarget, False
 copyAttribute "mailNickname", objSource, objTarget, False
 copyAttribute "msExchMailboxGuid", objSource, objTarget, False
 setAttribute "targetaddress", objSource.get( "mail"), objTarget
 copyAttribute "proxyAddresses", objSource, objTarget, True
 addAttribute "proxyAddresses", "X500:"& objSource.get( "LegacyExchangeDN"), objTarget

 strMail= objSource.get( "mail")
 n= instr( strMail, "@")
 debug( strMail)
 addAttribute "proxyAddresses", "smtp:"& left( strMail, n-1)& "@"& conTargetEMailDomain, objTarget

 setAttribute "msExchRecipientDisplayType", -2147483642, objTarget
 setAttribute "msExchRecipientTypeDetails", 128, objTarget
 setAttribute "legacyExchangeDN", conLegacyExchangeDN& objSource.get("cn"), objTarget

 objTarget.setInfo

 Else
 debug("*** ERR: Cannot retrieve DNs for Source or Target")
 syncAttributes= False
 End If
End Function

'*********************************************************
' getDN
' Retrieves the DN for a user object
'*********************************************************
Function getDN( struser, strServer, strDomain, strOU)
 dim objConn, objCmd, strQuery, objRS, strAttr, strRDNLDAP, strDNSLDAP
 strRDNLDAP= RDN2LDAPPATH( strOU)
 strDNSLDAP= DNSDomain2LDAPPath( strDomain)
 strAttr= "distinguishedName"
 set objConn= createObject( "ADODB.Connection")
 set objCmd= createObject( "ADODB.Command")
 objConn.Provider= "ADsDSOObject"
 objConn.Open "ADs provider"
 objCmd.ActiveConnection= objConn
 strQuery= "<LDAP://"& strServer
 If strServer <> "" Then
 strQuery= strQuery& "/"
 End If
 strQuery= strQuery& strRDNLDAP
 If strOU <> "" Then
 strQuery= strQuery& ","
 End If
 strQuery= strQuery& strDNSLDAP& ">"
 strQuery= strQuery+ ";(&(objectCategory=person)(objectClass=user)(SAMAccountName="& strUser&"));"& strAttr& ";subtree"
 objCmd.CommandText = strQuery
 on error resume next
 set objRS= objCmd.execute
 if err.number <> 0 Then
 debug( "*** ERR: Error "& err.number& " executing ["& strQuery& "]")
 getDN= ""
 Else
 on error goto 0
 Select Case objRS.recordCount
 Case 0
 debug( "*** ERR: User object "& struser& " not found")
 getDN= ""
 Case 1
 getDN= objRS.Fields( strAttr)
 'debug( getDN)
 Case Else
 debug("*** ERR: Ambigious user object "& struser)
 getDN= ""
 End Select
 End If
 set objRS= Nothing
 set objCmd= Nothing
 set objConn= Nothing
End Function

'*********************************************************
' DNSDomain2LDAPPath( str)
' Makes an LDAP notation for a DNS domain name
' e.g. corp.local => DC=corp,DC=local
'*********************************************************
Function DNSDomain2LDAPPath( str)
 Dim tmp1, tmp2, tmp3
 tmp1= split( str, ".")
 tmp2= ""
 For each tmp3 in tmp1
 If tmp2<>""  then
 tmp2= tmp2& ","
 End If
 tmp2= tmp2& "dc="& tmp3
 Next
 DNSDomain2LDAPPath= tmp2
End Function

'*********************************************************
' RDN2LDAPPath( str)
' Makes an LDAP notation for a Relative Distinguished Name
' e.g. Domain Accounts/3rd party => OU=3rd party,OU=Domain Accounts
'*********************************************************
Function RDN2LDAPPath( str)
 Dim tmp1, tmp2, tmp3
 tmp1= split( str, "/")
 tmp2= ""
 For each tmp3 in tmp1
 If tmp2<>"" then
 tmp2= ","& tmp2
 End If
 tmp2= "ou="& tmp3& tmp2
 Next
 RDN2LDAPPath= tmp2
End Function

'*********************************************************
' copyAttribute
' Copies attribute(s) to target
'*********************************************************
Function copyAttribute( strAttribute, objSource, objTarget, boolMulti)
dim boolUpdate, varItem
If isEmpty( objSource.get( strAttribute)) Then
  debug( strAttribute& " not set, clearing")
  objTarget.PutEx ADS_PROPERTY_CLEAR, strAttribute, 0
Else
  varItem= objSource.get( strAttribute)
  If boolMulti Then
    if isArray( varItem) Then
      debug( "Setting "& strAttribute& " to multi-value "& displayString( varItem))
      objTarget.PutEx ADS_PROPERTY_UPDATE, strAttribute, varItem
    Else
      debug( "Setting "& strAttribute& " to single-value "& displayString( varItem))
      objTarget.PutEx ADS_PROPERTY_UPDATE, strAttribute, array( varItem)
    End If
  Else
    debug( "Setting "& strAttribute& " to "& displayString( varItem))
    objTarget.Put strAttribute, varItem
  End If
End If
on error goto 0
objTarget.SetInfo
End Function

'*********************************************************
' setAttribute
' Sets attribute to target
'*********************************************************
Function setAttribute( strAttribute, strValue, objTarget)
 debug("Setting "& strAttribute& " to "& displayString(strValue))
 objTarget.Put strAttribute, strValue
 objTarget.SetInfo
End Function

'*********************************************************
' addAttribute
' Adds attribute to target
'*********************************************************
Function addAttribute( strAttribute, varAttribute, objTarget)
 dim boolUpdate, tmp
 boolUpdate= True
 If isEmpty( varAttribute) Then
 ' not set, skipping
 Else
 'on error resume next
 If isEmpty( objTarget.get( strAttribute)) Then
 boolUpdate= True
 Else
 If isArray( objTarget.get( strAttribute)) Then
 For each tmp in objTarget.get( strAttribute)
 If tmp = varAttribute Then
 boolUpdate= False
 End If
 Next
 Else
 boolUpdate= varAttribute= objTarget.get( strAttribute)
 End If
 End If
 on error goto 0
 If boolUpdate Then
 debug("Adding "& varAttribute& " to "& strAttribute)
 objTarget.PutEx ADS_PROPERTY_APPEND, strAttribute, array( varAttribute)
 Else
 debug( varAttribute& " already in "& strAttribute)
 End If
 End If
 objTarget.SetInfo
End Function

Output.log (sample)

[16:24] Start
[16:24] Reading names from users.txt
[16:24] jtest: Syncing Exchange Attributes from jtest
[16:24] Setting mail to jtest@source.nl #8
[16:24] Setting mailNickname to jtest #8
[16:24] Setting msExchMailboxGuid to (70C2360FB0330346A925172CA0473B9F) #8209
[16:24] Setting targetaddress to jtest@target.com #8
[16:24] Setting proxyAddresses to [SMTP:jtest@source.nl, X400:c=US;a= ;p=DemoOrg;o=Exchange;s=Old;g=Mr;] #8204
[16:24] Adding X500:/o=DemoOrg/ou=First Administrative Group/cn=Recipients/cn=jtest to proxyAddresses
[16:24] jtest@source.nl.nl
[16:24] Adding smtp:jtest@target.com to proxyAddresses
[16:24] Setting msExchRecipientDisplayType to -2147483642 #3
[16:24] Setting msExchRecipientTypeDetails to 128 #2
[16:24] Setting legacyExchangeDN to /o=NEWORG/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients/cn=Mr Old #8
[16:24] Finished

Cross-Forest Mailbox Move

Note: This is part 1; part 2 can be found here.

Currently I’m working with Johan Veldhuis on a project where we’ll be moving mailboxes from Exchange 2003 to Exchange 2010. Accounts and mailboxes will be migrated from forest to forest and to make things more interesting, the account migration is isolated from the mailbox migration. In other words, we start with a single forest after which we’ll create a account/resource forest setup from where we’ll consolidate things again. The account/resource forest setup is not completely by the book as the accounts in the resource forest won’t be disabled. This is because ActiveSync devices will be migrated at a later stage; this means they can use their existing authentication information.

Note: In the remainder of this article the following variables are used:

  • olddomain.nl is the source forest with Exchange 2003;
  • newdomain.com is the new forest with Exchange 2010;
  • Jan Test is our to be migrated test user with User ID jtest;
  • source.nl is an e-mail domain for which Exchange 2003 is authoritative;
  • target.com is an e-mail domain for which Exchange 2010 is authoritative.

The starting point of this blog is the situation where both forests olddomain.nl and newdomain.com are up and running. There is a trust between olddomain.nl and newdomain.com, ADMT and the Password Export Server are configured and running properly. This means we can successfully migrate user accounts from olddomain.nl to the new forest newdomain.com; the user’s mailbox will remain on Exchange 2003 in olddomain.nl.

Here is where the first problem was encountered. The Active Directory schemas mismatch, so ADMT will only migrate a basic set of attributes. This means that although newdomain.com has Exchange attributes, these won’t be migrated. Also, to be able to use the mailbox properly from their newdomain.com account, we need to add mailbox permissions for the newdomain.com account. For this purpose we created a post-migration script which adds Full Mailbox Access and External Account permissions to the mailbox for the newdomain.com account. After that we have reached an intermediary state, with accounts in newdomain.com and resources (mailboxes) in olddomain.nl; Users will log on using an account from the newdomain.com account forest and access their mailbox in the olddomain.nl resource forest.

The next phase will be migrating Exchange 2003 mailboxes from olddomain.nl to Exchange 2010 mailboxes in newdomain.com. Microsoft provides a nice TechNet article (633491) on how to prepare mailboxes for cross-forest mailbox move requests. The article also links to a (sample) PowerShell script, part of ILM 2007 SP1, located here. The script, Prepare-MoveRequest.ps1, should prepare the target forest for mailbox move request. Unfortunately, we couldn’t get it working properly.

First, the script complained it couldn’t find the specified Identity in the target forest. This was odd; the account is present, it was created by ADMT. Diving into the code it looks for the identity in various attributes:

filterParm = “(& (objectClass=user)” +
”¬†¬† ( (| (mailnickname=$escapedIdentity)” +
”¬†¬†¬†¬†¬†¬†¬† (cn=$escapedIdentity)” +
”¬†¬†¬†¬†¬†¬†¬† (proxyAddresses=SMTP:$escapedIdentity)” +
”¬†¬†¬†¬†¬†¬†¬† (proxyAddresses=smtp:$escapedIdentity)” +
”¬†¬†¬†¬†¬†¬†¬† (proxyAddresses=X500:$escapedIdentity)” +
”¬†¬†¬†¬†¬†¬†¬† (proxyAddresses=x500:$escapedIdentity)” +
”¬†¬†¬†¬†¬†¬†¬† (objectGUID=$escapedIdentity)” +
”¬†¬†¬†¬†¬†¬†¬† (displayname=$escapedIdentity))))”

$srcObject = findADObject $srcdc $filterParm

No matter what we tried, no match. Odd. Also, the script contains code which creates objects when it determines it needs to and there’s no switch to turn this behaviour off. That switch would be nice, because the last thing we want is to end up with hundreds of mail-enabled user objects when there are already user account objects present. Since we had little time to debug and fix the script, we decided to prepare the target account in newdomain.com ourselves. I took a script from one of my earlier projects (VB), which I created for similar merger / split scenarios in the past, and checked it against the information in the TechNet article mentioned earlier. In the end we had a script which transforms users into mail-enabled users as follows:

  1. Copy the following atributes from the account in olddomain.nl to the account in newdomain.com: mail, mailNickname, msExchMailboxGuid, proxyAddresses;
  2. Adds the LegacyExchangeDN attribute from olddomain.nl account as an X500 address to the account in newdomain.com;
  3. Sets msExchRecipientDisplayType to -2147483642;
  4. Sets msExchRecipientTypeDetails to 128;
  5. Sets targetAddress in newdomain.com to the value of mail attribute in the olddomain.nl;
  6. Adds a constructed e-mail address (e-mail account olddomain.nl + @ + new e-mail domain, e.g. j.test@target.com) to the proxyAddresses in newdomain.com;
  7. Sets legacyExchangeDN on the new account in newdomain.com to “/o=<ORG>/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients/cn=<CN>”, where <CN> is the cn of the account.

This is more or less the same as the Prepare-MoveRequest.ps1 should do, but this one worked like expected. Note that the last three actions are important for getting the move request to work. The targetAddress should be set properly, because new-moveRequest will set the target Address of the olddomain.nl account to the primary SMTP address of the newdomain.com account. LegacyExchangeDN and the proxyAddress should be added because new-moveRequest uses these for lookup and matching (I assume, because we will also will specify the identity itself). The proxyAddress e-mail address for which Exchange 2010 in newdomain.com is authoritative will be promoted to primary SMTP e-mail address.

Now, after preparing the newdomain.com account and transforming it into a mail-enabled user, we can use new-moveRequest to transfer the mailbox from Exchange 2003 to Exchange 2010:

$cred = get-credential
new-moverequest -identity <userid> -RemoteLegacy -TargetDatabase “<Mailbox Database Name>” -RemoteGlobalCatalog dc.olddomain.nl -RemoteCredential $cred -TargetDeliveryDomain <target.com>

An explanation:

  • We need to specify RemoteLegacy because the source forest isn’t Exchange 2010;
  • TargetDatabase identifies the target database;
  • Using RemoteGlobalCatalog we specify a GC from the remote forest;
  • With RemoteCredential, in combination with get-credential, is used to prompt for credentials and use them in the cmdlet;
  • TargetDeliveryDomain specifies the external e-mail domain that is used to set the targetAddress in the source forest, so normally this should be an e-mail domain for which the source forest isn’t and the target forest is authoritative.

Presto! This is what the objects look like after performing the new-moverequest:

olddomain.nl
cn=Jan Test
mail=jtest@target.com
legacyExchangeDN=/o=<ORG>/ou=First Administrative Group/cn=Recipients/cn=jtest88083336
mailNickname=jtest
proxyAddresses=X500:/o=<ORG>/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients/cn=Jan Test;
smtp:J.Test@source.nl;
SMTP:jtest@target.com;
X400:c=NL;a= ;p=<org>;o=Exchange;s=Test;g=Jan;;
targetAddress=SMTP:jtest@target.com

newdomain.com
cn=Jan Test
mail=jtest@target.com
legacyExchangeDN=/o=<ORG>/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients/cn=Jan Test;
mailNickname=jtest
proxyAddresses=smtp:jtest@newdomain.com‚Ä®;SMTP:jtest@target.com;X400:C=NL;A= ;P=<org>;O=Exchange;S=Test;G=Jan‚Ä®;smtp:J.Test@source.nl‚Ä®;smtp:J.Test@target,com‚Ä®;X500:/o=<ORG>/ou=First Administrative Group/cn=Recipients/cn=jtest88083336
targetAddress=SMTP:jtest@target.com

Notice that new-moveRequest adds a random number “88083336” to legacyExchangeDN in olddomain.nl and to X500 in newdomain.com. This is to promote the newdomain.com account for legacyExchangeDN usage and enables mailflow from the old olddomain.nl to newdomain.com when replying on old e-mail.

Update: Also worth noting is we an additional challenge because the source domain name equaled their e-mail namespace, which they want to keep using during and after the migration. Therefor we had to introduce a bogus local namespace for which the Exchange 2003 becomes authoritative and 2010 is not. We can use recipient policies to stamp this address to mail-enabled objects and by creating a connector from Exchange 2010 to Exchange 2003 using this namespace, we can route e-mail from migrated users to non-migrated users.

I hope this information is useful. For more information on new-moveRequest for cross-forest moves, check here. When you have questions, post them in the comments below or send me an e-mail.