Skip to main content

Replicating an Exchange Folder using WebDAV Replication and CDO 1.2

One of the lesser know and cooler (well I think) features of WebDAV with Exchange is the ability to perform replication of a collection (or a folder). The Exchange SDK has some documentation on how this works and the general plumbing of how you structure requests and responses. Previously I had a script that would copy contacts between a public folder and mailbox this was a pretty simple script with no intelligence so it would produce duplicates if it was run twice etc. I wanted something that would go a little further then this script and allow full one way sync of a private contacts folder with a public contacts folder so basically a script that could be run at intervals and any updates to the private contacts folder such as changes of email addresses, phone number, addition of contacts or deletion of contacts would be reflected in the public contacts folder. For this combining the original CDO 1.2 script to copy contacts (the reason that CDO is used to do the copy instead of WebDAV is the ability to retain the MAPI properties on the item being copied) with WebDAV replication gave the desired functionality.

How it works.

This script is split into many functions and subroutines all which perform different functions here’s a basic run though of how the script works.

The front end of the script sets up variables for the folders that you wish to copy I used this script to copy contacts but it will work fine to replicate any other folder (I haven’t tested calendars). I’ve created two copies of the script the first is a non FBA version and the second is a FBA version if your running forms based authentication. In the FBA version some code is included to perform a synthetic form logon using the username and password and then retrieve the cookies for further use in the script for authentication.

The next part of the script performs a Mapi logon to the source mailbox the session is require to perform the copying of the contacts.

The getpfid function returns the MAPI EntryID for the destination folder by using WebDAV to return the entryID from the destination folder URL. This EntryID is required to perform the copy via CDO 1.2.

To use WebDAV replication you need to use a collblob which “is the opaque binary stream generated by the server that represents the state of the contents of a collection. The collblob contains information about changes in the contents of the collection on the server and the query specified in the request for the Manifest of a Collection. The collblob tracks only resources that match the search criteria specified in the SEARCH Method query in the manifest request. “. ref In WebDAV this collblob property itself is returned as a BASE 64 encoded string. You need to store this property and include it in any queries that you make regarding the folder you are replicating. With this script what I’ve done is when a query is made to ascertain the replication status of the folder the result for the collblob is stored in a custom property on the destination folder. So on the destination folder there is a property that is based on the name of the source folder that holds the collblob from the replication query of the source folder. This is what the next function does Collabblobget retrieves the previous collblob from the destination folder to be used in the query of the source folder.

The QueryMailbox sub executes the WebDAV query on the source folder using the previous collblob if one exists. The result of this query are then parsed the collblob that is returned is first stored in a custom property on the source folder as previously explained. The result of this query will contain all the resources within the collection that have been added, changed or deleted since the last query with that collblob. A case statement is used to create a logic tree on the changetype property depending on what type of change it is different functions and sub’s are called. If this is a new resource then the CopyContact sub is called and the EntryID and repl-UID is passed in. The CopyContact sub uses CDO 1.2 to copy the item between the mailbox and public folder and also it creates a custom property on the item to store the repl-UID of the original items this is important when it comes to making changes or deletion in the future. If the changetype is modify then instead of finding out what property has been changed I decided to simplify things by just deleting the old item and then re-copying the source item. If the changetype is delete then the DeleteContact sub is called which finds the contact in the public folder by searching on the Repl-UID stored in the custom property in the destination folder and then do a WebDAV delete on the resource.

Running the script

Before you run the script you need to customise different variables with the script for the
snServername = "servername"
mnMailboxname = "mailbox"
SourceURL = "http://" & snServername & "/exchange/" & mnMailboxname & "/contacts/"
DestinURL = "http://" & snServername & "/public/foldername/"

For the FBA script you also need the password and domain of the account you are going to use in the synthetic logon.

To run the script itself just use cscript replfld.vbs

This is script is not really designed to sync multiple folders at a time eg if you wanted to sync two users contacts folder with on public folder this type of script will still produce duplicates at the destination folder. I’m working on another script to do this.

I’ve put a downloadable copy of this script here the script itself looks like.

snServername = "servername"
mnMailboxname = "mailbox"
domain = "domain"
strpassword = "password"
strusername = domain & "\" & mnMailboxname
SourceURL = "https://" & snServername & "/exchange/" & mnMailboxname &
"/contacts/"
DestinURL = "https://" & snServername & "/public/foldername/"

szXml = "destination=https://" & snServername & "/exchange/&flags=0&username=" &
strusername
szXml = szXml & "&password=" & strpassword & "&SubmitCreds=Log On&forcedownlevel=0&trusted=0"
set req = createobject("microsoft.xmlhttp")
req.Open "post", "https://" & snServername & "/exchweb/bin/auth/owaauth.dll",
False
req.send szXml
reqhedrarry = split(req.GetAllResponseHeaders(), vbCrLf,-1,1)
for c = lbound(reqhedrarry) to ubound(reqhedrarry)
if instr(lcase(reqhedrarry(c)),"set-cookie: sessionid=") then reqsessionID =
right(reqhedrarry(c),len(reqhedrarry(c))-12)
if instr(lcase(reqhedrarry(c)),"set-cookie: cadata=") then reqcadata=
right(reqhedrarry(c),len(reqhedrarry(c))-12)
next
set objSession = CreateObject("MAPI.Session")
strProfile = snServername & vbLf & mnMailboxname
objSession.Logon "",,, False,, True, strProfile
Set objInfoStore = objSession.GetInfoStore(objSession.Inbox.StoreID)
Set objpubstore = objSession.InfoStores("Public Folders")
pfPublicFolderID = getpfid()
colbblob = Collabblobget()
QueryMailbox(colbblob)

wscript.echo "Done"

sub QueryMailbox(colbblob)

strQuery = "<?xml version=""1.0""?><D:searchrequest xmlns:D = ""DAV:"" xmlns:R=""http://schemas.microsoft.com/repl/""><R:repl><R:collblob>"
& colbblob & "</R:collblob></R:repl>"
strQuery = strQuery & "<D:sql>SELECT ""DAV:href"", ""urn:schemas:httpmail:subject"",
""http://schemas.microsoft.com/mapi/proptag/x0fff0102"",""
http://schemas.microsoft.com/repl/repl-uid""
"
strQuery = strQuery & " FROM scope('shallow traversal of """
strQuery = strQuery & SourceURL & """') Where ""DAV:ishidden"" = False AND ""DAV:isfolder""
= False "
strQuery = strQuery & "</D:sql></D:searchrequest>"
req.open "SEARCH", SourceURL, false
req.setrequestheader "Content-Type", "text/xml"
req.SetRequestHeader "cookie", reqsessionID
req.SetRequestHeader "cookie", reqCadata
req.setRequestHeader "Translate","f"
req.send strQuery
If req.status >= 500 Then
wscript.echo "Status: " & req.status
wscript.echo "Status text: An error occurred on the server."
ElseIf req.status = 207 Then
wscript.echo "Status: " & req.status
wscript.echo "Status text: " & req.statustext
set oResponseDoc = req.responseXML
set oNodeList = oResponseDoc.getElementsByTagName("d:collblob")
For i = 0 To (oNodeList.length -1)
set oNode = oNodeList.nextNode
colblob = oNode.Text
Collabblobset(colblob)
Next
set idNodeList = oResponseDoc.getElementsByTagName("f:x0fff0102")
set replidNodeList = oResponseDoc.getElementsByTagName("d:repl-uid")
set replchangeType = oResponseDoc.getElementsByTagName("d:changetype")
for id = 0 To (idNodeList.length -1)
set oNode1 = idNodeList.nextNode
set oNode2 = replidNodeList.nextNode
set oNode3 = replchangeType.nextNode
select case oNode3.text
case "new" call CopyContact(Octenttohex(oNode1.nodeTypedValue),oNode2.text)
case "delete" wscript.echo oNode3.text
wscript.echo oNode2.text
DeleteContact(oNode2.text)
case "change" Wscript.echo "Change"
call DeleteContact(oNode2.text)
call CopyContact(Octenttohex(oNode1.nodeTypedValue),oNode2.text)
end select
next
Else
wscript.echo "Status: " & req.status
wscript.echo "Status text: " & req.statustext
wscript.echo "Response text: " & req.responsetext
End If

End Sub

function Collabblobget()

xmlreqtxt = "<?xml version='1.0'?><a:propfind xmlns:a='DAV:' xmlns:cp='" &
SourceURL & "'><a:prop><cp:collblob/></a:prop></a:propfind>"
req.open "PROPFIND", DestinURL, false, "", ""
req.setRequestHeader "Content-Type", "text/xml; charset=""UTF-8"""
req.setRequestHeader "Depth", "0"
req.SetRequestHeader "cookie", reqsessionID
req.SetRequestHeader "cookie", reqCadata
req.setRequestHeader "Translate", "f"
req.send xmlreqtxt
set oResponseDoc = req.responseXML
set oCobNode = oResponseDoc.getElementsByTagName("d:collblob")
For i1 = 0 To (oCobNode.length -1)
set oNode = oCobNode.nextNode
Collabblobget = oNode.Text
Next

End function

Sub Collabblobset(colblob)
xmlstr = "<?xml version=""1.0""?>" _
& "<g:propertyupdate " _
& " xmlns:g=""DAV:"" xmlns:e=""http://schemas.microsoft.com/exchange/""" _
& " xmlns:dt=""urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/"" " _
& " xmlns:cp=""" & SourceURL & """ " _
& " xmlns:header=""urn:schemas:mailheader:"" " _
& " xmlns:mail=""urn:schemas:httpmail:""> " _
& " <g:set> " _
& " <g:prop> " _
& " <cp:collblob>" & colblob & "</cp:collblob> " _
& " </g:prop> " _
& " </g:set> " _
& "</g:propertyupdate>"

req.open "PROPPATCH", DestinURL, False
req.setRequestHeader "Content-Type", "text/xml;"
req.SetRequestHeader "cookie", reqsessionID
req.SetRequestHeader "cookie", reqCadata
req.setRequestHeader "Translate", "f"
req.setRequestHeader "Content-Length:", Len(xmlstr)
req.send(xmlstr)
wscript.echo req.responsetext

end sub

Sub CopyContact(messageEntryID,ReplID)
set objcontact = objSession.getmessage(messageEntryID)
set objCopyContact = objcontact.copyto(pfPublicFolderID,objpubstore.ID)
objCopyContact.Unread = false
objCopyContact.Fields.Add "0x8542", vbString, ReplID,"0820060000000000C000000000000046"
objCopyContact.Update
Set objCopyContact = Nothing
wscript.echo objcontact.subject

end Sub

Sub DeleteContact(replUID)

strQuery = "<?xml version=""1.0""?><D:searchrequest xmlns:D = ""DAV:"">"
strQuery = strQuery & "<D:sql>SELECT ""DAV:Displayname"""
strQuery = strQuery & " FROM scope('shallow traversal of """
strQuery = strQuery & DestinURL & """') Where ""http://schemas.microsoft.com/mapi/id/{00062008-0000-0000-C000-000000000046}/0x8542""
= '" & replUID & "' AND ""DAV:isfolder"" = False "
strQuery = strQuery & "</D:sql></D:searchrequest>"
req.open "SEARCH", DestinURL, false
req.setrequestheader "Content-Type", "text/xml"
req.SetRequestHeader "cookie", reqsessionID
req.SetRequestHeader "cookie", reqCadata
req.setRequestHeader "Translate","f"
req.send strQuery
wscript.echo req.responsetext
If req.status >= 500 Then
wscript.echo "Status: " & req.status
wscript.echo "Status text: An error occurred on the server."
ElseIf req.status = 207 Then
wscript.echo "Status: " & req.status
wscript.echo "Status text: " & req.statustext
set oResponseDoc = req.responseXML
set oNodeList = oResponseDoc.getElementsByTagName("a:href")
For i = 0 To (oNodeList.length -1)
set oNode = oNodeList.nextNode
wscript.echo oNode.text
req.open "DELETE", oNode.text, false
req.SetRequestHeader "cookie", reqsessionID
req.SetRequestHeader "cookie", reqCadata
req.send
wscript.echo "Status: " & req.status
Next
Else
wscript.echo "Status: " & req.status
wscript.echo "Status text: " & req.statustext
wscript.echo "Response text: " & req.responsetext
End If

end Sub

function getpfid()

xmlreqtxt = "<?xml version='1.0'?><a:propfind xmlns:a='DAV:' xmlns:e='http://schemas.microsoft.com/mapi/proptag/'><a:prop><e:x0FFF0102

/></a:prop></a:propfind>"
req.open "PROPFIND", DestinURL, false, "", ""
req.setRequestHeader "Content-Type", "text/xml; charset=""UTF-8"""
req.setRequestHeader "Depth", "0"
req.SetRequestHeader "cookie", reqsessionID
req.SetRequestHeader "cookie", reqCadata
req.setRequestHeader "Translate", "f"
req.send xmlreqtxt
set oResponseDoc = req.responseXML
set oNodeList = oResponseDoc.getElementsByTagName("d:x0FFF0102")
For i = 0 To (oNodeList.length -1)
set oNode = oNodeList.nextNode
getpfid = Octenttohex(oNode.nodeTypedValue)
Next

end function

Function Octenttohex(OctenArry)
ReDim aOut(UBound(OctenArry))
For i = 1 to UBound(OctenArry) + 1
if len(hex(ascb(midb(OctenArry,i,1)))) = 1 then
aOut(i-1) = "0" & hex(ascb(midb(OctenArry,i,1)))
else
aOut(i-1) = hex(ascb(midb(OctenArry,i,1)))
end if
Next
Octenttohex = join(aOUt,"")
End Function

Popular posts from this blog

Testing and Sending email via SMTP using Opportunistic TLS and oAuth in Office365 with PowerShell

As well as EWS and Remote PowerShell (RPS) other mail protocols POP3, IMAP and SMTP have had OAuth authentication enabled in Exchange Online (Official announcement here ). A while ago I created  this script that used Opportunistic TLS to perform a Telnet style test against a SMTP server using SMTP AUTH. Now that oAuth authentication has been enabled in office365 I've updated this script to be able to use oAuth instead of SMTP Auth to test against Office365. I've also included a function to actually send a Message. Token Acquisition  To Send a Mail using oAuth you first need to get an Access token from Azure AD there are plenty of ways of doing this in PowerShell. You could use a library like MSAL or ADAL (just google your favoured method) or use a library less approach which I've included with this script . Whatever way you do this you need to make sure that your application registration  https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-

How to test SMTP using Opportunistic TLS with Powershell and grab the public certificate a SMTP server is using

Most email services these day employ Opportunistic TLS when trying to send Messages which means that wherever possible the Messages will be encrypted rather then the plain text legacy of SMTP.  This method was defined in RFC 3207 "SMTP Service Extension for Secure SMTP over Transport Layer Security" and  there's a quite a good explanation of Opportunistic TLS on Wikipedia  https://en.wikipedia.org/wiki/Opportunistic_TLS .  This is used for both Server to Server (eg MTA to MTA) and Client to server (Eg a Message client like Outlook which acts as a MSA) the later being generally Authenticated. Basically it allows you to have a normal plain text SMTP conversation that is then upgraded to TLS using the STARTTLS verb. Not all servers will support this verb so if its not supported then a message is just sent as Plain text. TLS relies on PKI certificates and the administrative issue s that come around certificate management like expired certificates which is why I wrote th

The MailboxConcurrency limit and using Batching in the Microsoft Graph API

If your getting an error such as Application is over its MailboxConcurrency limit while using the Microsoft Graph API this post may help you understand why. Background   The Mailbox  concurrency limit when your using the Graph API is 4 as per https://docs.microsoft.com/en-us/graph/throttling#outlook-service-limits . This is evaluated for each app ID and mailbox combination so this means you can have different apps running under the same credentials and the poor behavior of one won't cause the other to be throttled. If you compared that to EWS you could have up to 27 concurrent connections but they are shared across all apps on a first come first served basis. Batching Batching in the Graph API is a way of combining multiple requests into a single HTTP request. Batching in the Exchange Mail API's EWS and MAPI has been around for a long time and its common, for email Apps to process large numbers of smaller items for a variety of reasons.  Batching in the Graph is limited to a m
All sample scripts and source code is provided by for illustrative purposes only. All examples are untested in different environments and therefore, I cannot guarantee or imply reliability, serviceability, or function of these programs.

All code contained herein is provided to you "AS IS" without any warranties of any kind. The implied warranties of non-infringement, merchantability and fitness for a particular purpose are expressly disclaimed.