Thursday, May 26, 2005

Displaying a collapsible conversation view of messages within in a folder using ASP.Net

I’ve been playing around with displaying message conversations in a group by format similar to what you can do with Outlook customized views. The and stuff is certainly a lot more accommodating when you are trying to do this vs ADO and ASP classic. Although they still have a little way to go with displaying hierarchal data. (that said the 2.0 looks like it’s a great leap forward). For my code though I only had 1.1 so I had to make do with what there is.

As a base for this code I used the stuff I posted here and added in some other fields to retrieve to allow me to start to group by conversation. The main fields I’ve used to do the group by are the PR_Conversation_topic x0070001E and PR_Subject_prefix x003D001E. Basically I used the prefix fields to work out if a mail in the queryied folder was the first mail in a conversation (eg any response will have a prefix the first message won’t). And the PR_Conversation_topic was used to relate the emails together.

A quick run though of the flow of the page is that it makes a WebDAV request for the last 1000 items in the target folder. The number of items retrieved is controlled via the range header. Request.AddRange("rows", 0,1000)

Once the request is received it populates an ADO dataset with all the props I started out using readxml but it didn’t work so well when parsing date-time datatypes. I think speed wise there’s not much advantage anyway. I’ve also added an auto-number field to the record-set so I could use it to create reference id’s in some of the div's I’ve used on the page.

The next part of the code creates a self-referencing relation on the datatable that joins the Subject to the PR_Conversation_topic. The next part of the code then creates a view that filters the records so you only see the initial conversation starting email (eg anything that hasn’t got a Subject prefix). This view is then bound to an ASP data repeater.

The displaying of the data is where some of the tricks come in I’ve used two methods to display the data in a collapsible conversation view. The first is to display the data in a hierarchal format I’ve used nested repeaters. This article came in handy for working out how to use this sort of thing. To get it to display the child rows of the data from the dataview CreateChildView is used which retrieves the rows from the underlying table using the data relation but ignoring the view constraints I put on (which is pretty cool)

To create a collapsing view of the data when you click the subject of the message I used this method from the guysfromrolla . Because I wanted to display tabular data I added code to place the records in html tables. I’ve also added a field that will hyperlink to OWA if you want to see the content of the message.

This still needs a bit of work I’d like to paginate the results that come back and also include a row that shows when the last time someone sent an email in that conversation. This code works best in a folder where there are a lot of conversations for instance I’m using it to view data that is sent to a mailing list. The one thing I couldn’t do that outlook does is sort the data by last updated conversation I’m still thinking about this.

The data section of the code looks like this if you want to look at the whole thing I’ve put a downloadable copy here.

Public Class AcceptAllCertificatePolicy
Implements ICertificatePolicy
Public Overridable Function CheckValidationResult(ByVal srvPoint As ServicePoint, ByVal certificate As X509Certificate, ByVal request As WebRequest, ByVal problem As Integer) As Boolean Implements ICertificatePolicy.CheckValidationResult
Return True 'this accepts all certificates
End Function
End Class

Sub Page_Load(sender As Object, e As EventArgs)
System.Net.ServicePointManager.CertificatePolicy = New AcceptAllCertificatePolicy
Dim Request As System.Net.HttpWebRequest
Dim Response As System.Net.HttpWebResponse
Dim strRootURI As String
Dim strQuery As String
Dim bytes() As Byte
Dim workrow As System.Data.DataRow
Dim resrow As System.Data.DataRow
Dim impersonationContext As System.Security.Principal.WindowsImpersonationContext
Dim currentWindowsIdentity As System.Security.Principal.WindowsIdentity
currentWindowsIdentity = CType(User.Identity, System.Security.Principal.WindowsIdentity)
impersonationContext = currentWindowsIdentity.Impersonate()
Dim MyCredentialCache As System.Net.CredentialCache
Dim resdataset As New System.Data.DataSet
Dim RequestStream As System.IO.Stream
Dim ResponseStream As System.IO.Stream
Dim ResponseXmlDoc As System.Xml.XmlDocument
Dim hrefNodes,SubjectNodes,FromnameNodes,x003D001ENodes,x0070001ENodes,datereceivedNodes As System.Xml.XmlNodeList
Dim objsearch As New System.DirectoryServices.DirectorySearcher
Dim strrootdse As String = objsearch.SearchRoot.Path
Dim objdirentry As New system.DirectoryServices.DirectoryEntry(strrootdse)
Dim objresult As system.DirectoryServices.SearchResult
Dim stremailaddress As String
Dim strhomeserver As String
Dim ResDsSet as DataSet = New DataSet
objsearch.Filter = "(&(&(&(& (mailnickname=*) (| (&(objectCategory=person)(objectClass=user)(|(homeMDB=*)" _
& "(msExchHomeServerName=*))) )))(objectCategory=user)(userPrincipalName=*)(mailNickname=" & System.Environment.UserName & ")))"
objsearch.SearchScope = DirectoryServices.SearchScope.Subtree
objsearch.Sort.Direction = DirectoryServices.SortDirection.Ascending
objsearch.Sort.PropertyName = "mail"
Dim colresults As DirectoryServices.SearchResultCollection = objsearch.FindAll()
For Each objresult In colresults
stremailaddress = objresult.GetDirectoryEntry().Properties("mail").Value
strhomeserver = objresult.GetDirectoryEntry().Properties("msExchHomeServerName").Value
Dim emailNameNodes As System.Xml.XmlNodeList
strhomeserver = Right(strhomeserver, Len(strhomeserver) - (InStr(strhomeserver, "cn=Servers/cn=") + 13))
strRootURI = "https://" & strhomeserver & "/exchange/" & stremailaddress & "/Inbox/"
strQuery = "" & _
"" & _
"SELECT ""urn:schemas:mailheader:subject"", ""urn:schemas:httpmail:fromname"", " & _
" ""urn:schemas:httpmail:datereceived"" , """", " & _
" """" FROM """ & strRootURI & """" & _
"WHERE ""DAV:ishidden"" = false AND ""DAV:isfolder"" = false AND ""DAV:contentclass"" = 'urn:content-classes:message'" & _
Request = CType(System.Net.WebRequest.Create(strRootURI), _
Request.Credentials = System.Net.CredentialCache.DefaultCredentials
Request.Method = "SEARCH"
bytes = System.Text.Encoding.UTF8.GetBytes(strQuery)
Request.ContentLength = bytes.Length
RequestStream = Request.GetRequestStream()
RequestStream.Write(bytes, 0, bytes.Length)
Request.ContentType = "text/xml"
Request.AddRange("rows", 0,1000)
Request.Headers.Add("Translate", "F")
Response = CType(Request.GetResponse(), System.Net.HttpWebResponse)
ResponseStream = Response.GetResponseStream()
ResponseXmlDoc = New System.Xml.XmlDocument
ResDsSet = New System.Data.DataSet
Dim resultstable As DataTable
resultstable=new DataTable()
resultstable.TableName = "queryres"
Dim autoid As Data.DataColumn
autoid = resultstable.Columns.Add("autoid", GetType(Int32))
autoid.AutoIncrement = True
autoid.AutoIncrementSeed = 1
autoid.AutoIncrementStep = 1
hrefNodes = ResponseXmlDoc.GetElementsByTagName("a:href")
SubjectNodes = ResponseXmlDoc.GetElementsByTagName("d:subject")
FromnameNodes = ResponseXmlDoc.GetElementsByTagName("e:fromname")
x003D001ENodes = ResponseXmlDoc.GetElementsByTagName("f:x003D001E")
x0070001ENodes = ResponseXmlDoc.GetElementsByTagName("f:x0070001E")
datereceivedNodes = ResponseXmlDoc.GetElementsByTagName("e:datereceived")
If SubjectNodes.Count > 0 Then
Dim i As Integer
For i = 0 To datereceivedNodes.Count - 1
AddRow(resultstable, hrefNodes(i).InnerText, FromnameNodes(i).InnerText, SubjectNodes(i).InnerText,x0070001ENodes(i).InnerText,x003D001ENodes(i).InnerText,datereceivedNodes(i).InnerText)
End If
Dim relcol1, relcol2 As Data.DataColumn
relcol1 = ResDsSet.Tables("queryres").Columns("subject")
relcol2 = ResDsSet.Tables("queryres").Columns("x0070001E")
Dim grprelations As Data.DataRelation
grprelations = New Data.DataRelation("GroupEmails", relcol1, relcol2, False)
Dim fldView As DataView = New DataView(ResDsSet.Tables("queryres"), "x003D001E = ''", "", DataViewRowState.CurrentRows)
Dim mailView As DataView
ConversationRepeater.DataSource = fldView

End Sub

Sub AddRow(resultstable As DataTable,href as string, FromName As String, Subject As String, x0070001E as string,x003D001E as string, Daterecieved as string )
Dim row As DataRow
Dim Daterecieved1 As System.DateTime
Daterecieved1 = CDate(Daterecieved)
row("Daterecieved")=Daterecieved1.ToShortDateString & " " & Daterecieved1.ToShortTimeString
End Sub

Tuesday, May 24, 2005

Script to Copy Public folder contacts to a mailbox's Contacts folder using CDO 1.2

Somebody asked today about this script I posted a while ago about copying contacts between mailbox's using the vCard method in CDOEX. While this script works okay for simple contacts if you have rich contacts where you have lots of additionally information that CDOEX doesn't include in the vcard stream then it can be a lot of work going through and copying each one of these Mapi properties manually (which I've already done for a few props in this script). The other option if you want to do this in a script is to use MAPI via CDO 1.2. The big advantage of using MAPI is that you will retain all the MAPI properties as they are on the object and also you can run the script remotely where the CDOEX script must be run locally on the Exchange server. The actually question was about copying contacts from a public folder to a mailbox so I've come up with a script that will do this using MAPI via CDO 1.2. To use this script you need to know the entryID for the public folder where the contacts are stored .You can find the folder entryID using something like Outlookspy or Mdbvu32 (from the exchange support tools directory) or I've included a simple utility script that will output what the entryID of a store item is from the OWA URL. This simple utility script look like.

OWAURL = "http://server/public/folder"
set xmlobjreq = createobject("Microsoft.XMLHTTP")
xmlreqtxt = "<?xml version='1.0'?><a:propfind xmlns:a='DAV:'

'><a:prop><e:x0FFF0102/></a:prop></a:propfind>" "PROPFIND", OWAURL, false, "", ""
xmlobjreq.setRequestHeader "Content-Type", "text/xml; charset=""UTF-8"""
xmlobjreq.setRequestHeader "Depth", "0"
xmlobjreq.setRequestHeader "Translate", "f"
xmlobjreq.send xmlreqtxt
set oResponseDoc = xmlobjreq.responseXML
set oNodeList = oResponseDoc.getElementsByTagName("d:x0FFF0102")
For i = 0 To (oNodeList.length -1)
set oNode = oNodeList.nextNode
wscript.echo Octenttohex(oNode.nodeTypedValue)

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)))
aOut(i-1) = hex(ascb(midb(OctenArry,i,1)))
end if
Octenttohex = join(aOUt,"")
End Function

The script that copies the contacts from a public folder to mailbox's contact folder looks like I've put a downloadable copy of both scripts here. Remember these scripts do a dumb copy so they will just copy or duplicate contacts with no intelligence so used inappropriately they could cause duplicate contact issues.

servername = "servername"
mailbox = "mailbox"
pubContactsfolderid = "00000....etc"
set objSession = CreateObject("MAPI.Session")
strProfile = servername & vbLf & mailbox
objSession.Logon "",,, False,, True, strProfile
Set objcontactfolder = objSession.getdefaultfolder(5)
Set objInfoStore = objSession.GetInfoStore(objSession.Inbox.StoreID)
Set objpubstore = objSession.InfoStores("Public Folders")
set objpubContactsfolder = objSession.getfolder(pubContactsfolderid,
for each objcontact in objpubContactsfolder.messages
set objCopyContact = objcontact.copyto(objcontactfolder.ID,objInfoStore.ID)
objCopyContact.Unread = false
Set objCopyContact = Nothing
wscript.echo objcontact.subject

Friday, May 13, 2005

C# Catchall Onarrival Event sink

One of the Event sinks that comes in handy from time to time especially if people own a lot of domains is the catch all event sink from .Now the .NET frameworks a bit more prevalent and the fact I wanted to use this on my Internal servers for something I thought I’d give converting this sink to C# ago. There are two ways you can go about writing SMTP event sinks in managed code the first is to build some wrappers as outlined in on msdn. This gives you access to all the protocol and transport events. The second way to build a SMTP event sink in managed code is to use the CDO onarrival event whose interfaces are defined in the cdoex.dll file (or cdosys.dll if you don’t have Exchange). The downside of using the CDO interface is that it adds significant overhead and is synchronous but the upside is that it that it handles most of the parsing and MIME issues. There’s a good doc here that discusses the issues . But using C# is a step up from VBS and should avoid all those nasty STA issues discussed here.

For the code itself its mostly based on the code from the KB with one major exception. I’ve added a section that checks and sets an X-header on the message if it’s processed by the sink. I did this to make sure the sink wouldn’t run multiple times on a message which was in response to an issue that I had with this sink (as well as the original script) in my environment. What happened for me was that I was a little lazy when I registered the sink and instead of just registering it to run on messages being sent to my catch all domain I registered it to run on all messages that where being processed by the server. This was fine for all normal message traffic that flowed though the server but a problem arose when I had some messages that where bound for a mail enabled public folders. I have a front-backend setup and I have a public store mounted on my front end server which contains the folder hierarchy. So when my front end server received the message bound from a mail-enabled public folder it would deliver it locally first as per its logic and then it would resubmit it once it work out where a replica for that folder existed ref . When the event sink ran on this resubmitted message even though it wasn’t making any changes to it the code still goes though the process of writing the recipient list back to the envelope field and calls to update the message. Something was happening within this process which would then cause a message loop on my front-backend servers which would just continually bounce the message between each of the servers until I removed the sink. This may mean I have a problem somewhere else and it wouldn’t have happened if I had bound the sink correctly in the first place but it was enough to prompt me to change this sink to prevent this type of thing happening In the future. The one draw back of adding an X-header was that it invalidates any digital signatures but for a catch domain this isn’t a big deal.

Down to the coding

The first thing to do is create a new classlibrary project in visual studio

To create the sink you need to grab 3 dll’s from your server the first is the codex.dll you also should grab the seo.dll from %windir%\system32\inetsrv and the last dll you need is the PIA for ado. I used the PIA from OWA on Exchange 2003 which is the Adodb.dll file in the Exchsrvr\OMA\browse\bin directory. This is one thing you need to be careful of as there are a few PIA’s kicking around which are different versions. Before you use it you may want to list to see if any are registered in the GAC by using gacutil –l. Usually there isn’t but I had a problem on one server where I had a version of ADODB registered in the GAC which was a different version then the PIA I was trying to use which caused a muck of problems.

Once you have all the DLL’s you need to create strong named assemblies for codex and seo so you need to first create a keypair with sn.exe eg sn.exe –k :SMTPOnarrival.key

Then using Tlbimp.exe build some Interop dll’s I’ve used the namespace switch to make sure CDOEX gets assigned CDO for the namespace eg

tlbimp cdoex.dll /namespace:CDO /keyfile:SMTPOnarrival.key /out:Interop.cdoex.dll

tlbimp seo.dll /namespace:SEO /keyfile:SMTPOnarrival.key /out:Interop.seo.dll

Once you’ve done this you can then reference all three dll’s in your project and you also need to add the keypair name to AssemblyKeyFile property in the assembly info.

The other thing you need to do is in the project properties-configuration properties you need to make sure that Regsiter for Com interop is set to true

Add the code then make sure you add a new unique GUID using Tools – Create GUID (create registry format). You need to change the catch domain and replace mailbox in the code which are hard coded as well you should set a unique x-header for the server.

Once you’ve done this you need to register your dll using regasm with the /codebase switch eg regasm onarrivalesink.dll /codebase

And then finally bind your sink using SMTPreg.vbs (which comes with the Exchange SDK there is also a copy in When your binding it I would make sure you bind it so it only fires on emails sent to your catch domain recipients so a registration like

cscript smtpreg.vbs /add 1 onarrival CatchallSink SMTPonarrival.Catchall "rcpt to=*"

If you want to debug your code (which you should only be doing on a dev server) because SMTP event sinks run in-process (of IIS) within Visual Studio to debug you need to select tools – debug process and then attach to the inetinfo.exe process (for CLR). The only quirk that I found was that Inetinfo needs to have successfully loaded your code to allow you to connect the debugger (eg the sink needs to have fired once first) or you just can’t connect. The other small quirk that I haven’t worked out yet is that I had to keep restarting the IISadmin service (and dependants) to make it release the DLL so I could make changes.

The code I’ve used is very low on testing so I wouldn’t trust it in anything other then a test environment.

I’ve put a downloadable copy of the code here

The code itself looks like

using System;
using System.Runtime.InteropServices;
using CDO;
using ADODB;
using SEO;

namespace SMTPonarrival
public class Catchall : ISMTPOnArrival , IEventIsCacheable
void ISMTPOnArrival.OnArrival(IMessage msg, ref CdoEventStatus EventStatus)
if (msg.Fields["urn:schemas:mailheader:X-catchall"].Value == null)
catch(Exception e)
System.IO.StreamWriter logfile = new System.IO.StreamWriter("c:\\SMTPEventerrorlog.txt",true);
logfile.WriteLine("Sink Fired : " + System.DateTime.Now);
logfile.WriteLine("Error : " + e.Message);
//Set Event Status to CDO_RUN_NEXT_SINK
EventStatus = CDO.CdoEventStatus.cdoRunNextSink;
void IEventIsCacheable.IsCacheable()
// This will return S_OK by default.

private void ProcessMessage(IMessage msg1)
string strFixedListlc;
string searchdomain = "";
string strreplaceaddr = ";";
string strFixedList = msg1.EnvelopeFields[RECIPLIST].Value.ToString();
while (strFixedList.IndexOf(searchdomain ,1) != -1 )
strFixedListlc = strFixedList.ToLower();
int nDomainPart = strFixedListlc.IndexOf(searchdomain,1);
int nNamePart = strFixedList.LastIndexOf(";",nDomainPart);
int nNextAddress = strFixedList.IndexOf("SMTP:",nDomainPart);
if (nNamePart == -1)
if (nNextAddress == -1)
strFixedList = strreplaceaddr;}
strFixedList = strreplaceaddr + strFixedList.Remove(0,nNextAddress);}
if (nNextAddress == -1)
strFixedList = strFixedList.Remove(nNamePart,strFixedList.Length-nNamePart) + ";" + strreplaceaddr;}
strFixedList = strFixedList.Remove(nNamePart,strFixedList.Length-nNamePart) + ";" + strreplaceaddr + strFixedList.Remove(0,nNextAddress);
msg1.EnvelopeFields[RECIPLIST].Value = strFixedList;
msg1.Fields["urn:schemas:mailheader:X-catchall"].Value = "Server-CatchALL";

Thursday, May 12, 2005

Changing the default permissions on a calendar to Reviewer

Updated 3/8/2005 to update local freebusy folder permissions to editor

Changing the default permissions on user’s calendars to reviewer is something that comes up now and again in different companies for different reasons. Automating this with script is relatively straight forward if you use the ACL.dll which you can get a copy of from the sample on CDOLive

Actually the day after I wrote the below script I found another utility that someone had created call setperm that gives you a nice GUI to this. As well as that it gives you the ability to change permissions on other folders other then the calendar. The original web site that hosted this utility seems to be gone now (which is a shame it because it had some good stuff) but you can still download the utility from . There's also a third party application from Symprex that can do this go here for details.

If your still interested in the script version here it is it mostly based around . Some ADSI has been added to select all the users from the GAL for a particular server in the servername variable and some file logging is done to track all the user that are updated. I’ve put a downloadable copy of the code here the script looks like

Public Const CdoDefaultFolderCalendar = 0
servername = wscript.arguments(0)
set conn = createobject("ADODB.Connection")
set com = createobject("ADODB.Command")
Set iAdRootDSE = GetObject("LDAP://RootDSE")
strNameingContext = iAdRootDSE.Get("configurationNamingContext")
strDefaultNamingContext = iAdRootDSE.Get("defaultNamingContext")
Conn.Provider = "ADsDSOObject"
Conn.Open "ADs Provider"
svcQuery = "<LDAP://" & strNameingContext &
">;(&(objectCategory=msExchExchangeServer)(cn=" & Servername &
Com.ActiveConnection = Conn
Com.CommandText = svcQuery
Set Rs = Com.Execute
while not rs.eof
GALQueryFilter = "(&(&(&(& (mailnickname=*)(!msExchHideFromAddressLists=TRUE)(|
(&(objectCategory=person)(objectClass=user)(msExchHomeServerName=" &
rs.fields("legacyExchangeDN") & ")) )))))"
strQuery = "<LDAP://" & strDefaultNamingContext & ">;" & GALQueryFilter &
com.Properties("Page Size") = 100
Com.CommandText = strQuery
Set Rs1 = Com.Execute
while not Rs1.eof
call dofreebusy(servername,rs1.fields("mailnickname"))
wscript.echo "Setting Permission on: " & rs1.fields("mailnickname")
set conn = nothing
set com = nothing
wscript.echo "Done"

function dofreebusy(servername,mailboxname)

Set objSession = CreateObject("MAPI.Session")
objSession.Logon "","",false,true,true,true,servername & vbLF & mailboxname
Set CdoInfoStore = objSession.GetInfoStore
Set CdoFolderRoot = CdoInfoStore.RootFolder
Set ACLObj = CreateObject("MSExchange.aclobject")
set cdocalendar = objSession.GetDefaultFolder(CdoDefaultFolderCalendar)
ACLObj.CDOItem = cdocalendar
Set FolderACEs = ACLObj.ACEs
getpermissions = mailboxname & ": "
For each fldace in FolderACEs
if fldace.ID = "ID_ACL_DEFAULT" then
fldace.rights = 1025
end if
getpermissions = getpermissions & fldace.ID & "-" & fldace.rights
'FreeBusy Update
Set objRoot = objSession.GetFolder("")
Set objFreeBusyFolder = objRoot.Folders.Item("FreeBusy Data")
ACLObj.CDOItem = objFreeBusyFolder
Set FolderACEs = ACLObj.ACEs
For each fldace in FolderACEs
if fldace.ID = "ID_ACL_DEFAULT" then
fldace.rights = 1123
end if

End function