Friday, April 27, 2007

Reporting on the Top 10 largest emails in each users mailbox in Exchange

Someone asked for a script last week to show the top 5 largest emails by size in each user’s mailbox being a Letterman fan (yep we get it here in Aus just no all the jokes) I tough I’d post it as a Top 10 script. You can expand on this concept to really look at the Top 10 of anything in your user's mailbox's (drum roll at meeting not included). The script is basically the mailbox content age script I posted a while ago modified to instead create a disconnected record-set of the largest 10 items in each folder in a mailbox and then basically sort this list at the end of the mailbox scan and write the top 10 largest items for each user to a separate htm file in the reports directory. To limit the query to only return the top 10 largest items in each folder the query uses the order by SQL verb to sort the result by the item size and to limit the number of results returned the range header is used in WebDAV eg

req.setRequestHeader "Range", "rows=0-9"

This means only the first 10 rows of the result is returned which cuts down the processing of the script greatly and means the execution time and load on the server is greatly reduced. The script uses the admin virtual root so it will run with delegated Exchange Admin Rights. The script requires 1 command-line parameter which in the servername of the server you wish to run it against eg something like

Cscript shotop10.vbs servername

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

On Error Resume next
Servername = wscript.arguments(0)
set shell = createobject("")
strValueName = "HKLM\SYSTEM\CurrentControlSet\Control\TimeZoneInformation\ActiveTimeBias"
minTimeOffset = shell.regread(strValueName)
toffset = datediff("h",DateAdd("n", minTimeOffset, now()),now())
treport = "<table border=""1"" width=""100%"">" & vbcrlf
treport = treport & " <tr>" & vbcrlf
treport = treport & "<td align=""center"" bgcolor=""#000080""><b><font color=""#FFFFFF"">Folder</font></b></td>"
& vbcrlf
treport = treport & "<td align=""center"" bgcolor=""#000080""><b><font color=""#FFFFFF"">Date
Recieved</font></b></td>" & vbcrlf
treport = treport & "<td align=""center"" bgcolor=""#000080""><b><font color=""#FFFFFF"">Mail
From</font></b></td>" & vbcrlf
treport = treport & "<td align=""center"" bgcolor=""#000080""><b><font color=""#FFFFFF"">Subject</font></b></td>"
& vbcrlf
treport = treport & "<td align=""center"" bgcolor=""#000080""><b><font color=""#FFFFFF"">Size
KB</font></b></td>" & vbcrlf
treport = treport & "</tr>" & vbcrlf
set req = createobject("microsoft.xmlhttp")
set com = createobject("ADODB.Command")
set conn = createobject("ADODB.Connection")
Set iAdRootDSE = GetObject("LDAP://RootDSE")
strNameingContext = iAdRootDSE.Get("configurationNamingContext")
strDefaultNamingContext = iAdRootDSE.Get("defaultNamingContext")
Conn.Provider = "ADsDSOObject"
Conn.Open "ADs Provider"
polQuery = "<LDAP://" & strNameingContext &amp; ">;(&(objectCategory=msExchRecipientPolicy)(cn=Default
svcQuery = "<LDAP://" & strNameingContext &amp; ">;(&(objectCategory=msExchExchangeServer)(cn="
& Servername & "));cn,name,legacyExchangeDN;subtree"
Com.ActiveConnection = Conn
Com.CommandText = polQuery
Set plRs = Com.Execute
while not plRs.eof
for each adrobj in plrs.fields("gatewayProxy").value
if instr(adrobj,"SMTP:") then dpDefaultpolicy =
wscript.echo dpDefaultpolicy
Com.CommandText = svcQuery
Set Rs = Com.Execute
while not rs.eof
GALQueryFilter = "(&(&(&(& (mailnickname=*)(!msExchHideFromAddressLists=TRUE)(|
(&(objectCategory=person)(objectClass=user)(msExchHomeServerName=" &
rs.fields("legacyExchangeDN") &amp; ")) )))))"
strQuery = "<LDAP://" & strDefaultNamingContext &amp; ">;" & GALQueryFilter &
com.Properties("Page Size") = 100
Com.CommandText = strQuery
Set Rs1 = Com.Execute
while not Rs1.eof
set conn1 = createobject("ADODB.Connection")
strConnString = "Data Provider=NONE; Provider=MSDataShape"
conn1.Open strConnString
set objParentRS = createobject("adodb.recordset")
" NEW adVarChar(255) AS MailDate, " & _
" NEW adVarChar(255) AS FolderName, " & _
" NEW adVarChar(255) AS MailFrom, " & _
" NEW adVarChar(255) AS Subject, " & _
" NEW adBigInt AS Size"
objParentRS.LockType = 3
objParentRS.Open strSQL, conn1
falias = "http://" & servername &amp; "/exadmin/admin/" & dpDefaultpolicy & "/mbx/"
for each paddress in rs1.fields("proxyaddresses").value
if instr(paddress,"SMTP:") then falias = falias & replace(paddress,"SMTP:","")

wscript.echo falias
call RecurseFolder(falias)
objParentRS.Sort = "Size DESC"
Set fso = CreateObject("Scripting.FileSystemObject")
set wfile = fso.opentextfile("c:\temp\" & rs1.fields("mail").value &
report = ""
For mrep = 1 To 10
report = report & "<tr>" & vbcrlf
report = report & "<td align=""center"">" & objParentRS.fields("FolderName") &amp;
" </td>" & vbcrlf
report = report & "<td align=""center"">" & objParentRS.fields("MailDate") &amp;
" </td>" & vbcrlf
report = report & "<td align=""center"">" & objParentRS.fields("MailFrom") &amp;
" </td>" & vbcrlf
report = report & "<td align=""center"">" & objParentRS.fields("Subject") &amp;
" </td>" & vbcrlf
report = report & "<td align=""center"">" &
formatnumber(cdbl(objParentRS.fields("Size"))/1024,2) &amp; " </td>" & vbcrlf
report = report & "</tr>" & vbcrlf
Set objParentRS = nothing
wfile.writeline treport
wfile.writeline report
wfile.writeline "</table>"
set wfile = Nothing
set conn = nothing
set com = nothing

Public Sub RecurseFolder(sUrl) "SEARCH", sUrl, False, "", ""
sQuery = "<?xml version=""1.0""?>"
sQuery = sQuery & "<g:searchrequest xmlns:g=""DAV:"">"
sQuery = sQuery & "<g:sql>SELECT """
sQuery = sQuery & "mapi/proptag/x0e080003"", ""DAV:hassubs"" FROM SCOPE "
sQuery = sQuery & "('SHALLOW TRAVERSAL OF """ & sUrl &amp; """') "
sQuery = sQuery & "WHERE ""DAV:isfolder"" = true and ""DAV:ishidden"" = false
and """" = 1"
sQuery = sQuery & "</g:sql>"
sQuery = sQuery & "</g:searchrequest>"
req.setRequestHeader "Content-Type", "text/xml"
req.setRequestHeader "Translate", "f"
req.setRequestHeader "Depth", "0"
req.setRequestHeader "Content-Length", "" & Len(sQuery)
req.send sQuery
Set oXMLDoc = req.responseXML
Set oXMLSizeNodes = oXMLDoc.getElementsByTagName("d:x0e080003")
Set oXMLHREFNodes = oXMLDoc.getElementsByTagName("a:href")
Set oXMLHasSubsNodes = oXMLDoc.getElementsByTagName("a:hassubs")
For i = 0 to oXMLSizeNodes.length - 1
call procfolder(oXMLHREFNodes.Item(i).nodeTypedValue,sUrl)
wscript.echo oXMLHREFNodes.Item(i).nodeTypedValue
If oXMLHasSubsNodes.Item(i).nodeTypedValue = True Then
call RecurseFolder(oXMLHREFNodes.Item(i).nodeTypedValue)
End If
End Sub

sub procfolder(strURL,pfname)
wscript.echo strURL
strQuery = "<?xml version=""1.0""?><D:searchrequest xmlns:D = ""DAV:""
strQuery = strQuery & "<D:sql>SELECT ""DAV:displayname"",
""urn:schemas:httpmail:subject"", "
strQuery = strQuery & """DAV:creationdate"", ""DAV:getcontentlength"", "
strQuery = strQuery & """urn:schemas:httpmail:fromemail"",
strQuery = strQuery & " FROM scope('shallow traversal of """
strQuery = strQuery & strURL &amp; """') Where ""DAV:ishidden"" = False AND
""DAV:isfolder"" = False Order by ""DAV:getcontentlength""
DESC</D:sql></D:searchrequest>" "SEARCH", strURL, false
req.setrequestheader "Content-Type", "text/xml"
req.setRequestHeader "Translate","f"
req.setRequestHeader "Range", "rows=0-9"
req.send strQuery
If req.status >= 500 Then
ElseIf req.status = 207 Then
set oResponseDoc = req.responseXML
set oNodeList = oResponseDoc.getElementsByTagName("a:displayname")
set oNodeList1 = oResponseDoc.getElementsByTagName("a:href")
set oSize = oResponseDoc.getElementsByTagName("a:getcontentlength")
set odatereceived = oResponseDoc.getElementsByTagName("d:datereceived")
set fromEmail = oResponseDoc.getElementsByTagName("d:fromemail")
set subject = oResponseDoc.getElementsByTagName("d:subject")
For i = 0 To (oNodeList.length -1)
set oNode = oNodeList.nextNode
set oNode1 = oNodeList1.nextNode
set oNode2 = oSize.nextNode
set oNode3 = odatereceived.nextNode
set onode4 = fromEmail.nextNode
set onode5 = subject.nextNode
wscript.echo onode4.text & " " & onode5.text
objParentRS("MailDate") =
&amp; " " & Mid(oNode3.text,12,8))
objParentRS("FolderName") = unescape(Replace(strURL,falias,""))
objParentRS("MailFrom") = onode4.text
objParentRS("Subject") = Right(onode5.text,255)
objParentRS("Size") = oNode2.Text
End If
end sub

Friday, April 20, 2007

Understanding the TimeZone setting with the GetUserAvailability Operation in Exchange Web Services in Exchange 2007

Updated 12/7/2007 with the proper method of using the registry thanks to Benjamin Spain from MSFT for pointing this out

Using TimeZones is very different in EWS then it is in other Exchange API’s like CDOEX where the time zone configuration itself is stored in the DLL (hence causing the need to patch the dll every time the government decides to change the date for Daylight savings). I’ve been working on some GetUserAvailibility code and it’s been a bit of a struggle to get my head around the new format and how to make this flexible (eg no hard coding Bias values or Daylight Saving settings) in the code I’m using so I thought I’d put together a post to share what I’ve learned.

To populate the TimeZone information in a GetUserAvailability Operation you need to set the properties in a GetUserAvailabilityRequestType object so the following nodes will be populated in the SOAP request.


Theses nodes should reflect the setting from the registry timezone keys under SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones\ for whatever timezone you want to make the request from. Not all timezones use DST but you still have to fill out both standardTime and daylightTime nodes in the case where you have no Daylight setting.

To read the timezone setting from the registry you need to deal with two different data structures which are documented on MSDN the first is the


Make sure you read the StandardDate section of this doco this is very important in relation to the rest of the code.



To find the correct node in the registry the method that I've used was to get the StandardName from TimeZone.CurrentTimeZone.StandardName which i found usually corresponds to the Key name under SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones\. Under that Key you need to get the TZI value which is a binary registry key which stores information in the TIME_ZONE_INFORMATION structure.

To help parse this information i used a few structs as the format defines the first 4 bytes relate to the default Bias the next 4 bytes are the standardTime Bias (usually always 0) and the next four bytes are the daylightTime Bias. The remaining bytes are comprised of 16bit words that define 2 SYSTIME structures one for standard time and one for daylight time. These structures basically tell you when DST and StandardTime starts and Ends. To use these in a GetUserAvailibility operation you need to translate them into a SerializableTimeZoneTime object.

So to put this together is some code it would look like the following there a downloadable version here

class Program
private struct SYSTEMTIME
public Int16 wYear;
public Int16 wMonth;
public Int16 wDayOfWeek;
public Int16 wDay;
public Int16 wHour;
public Int16 wMinute;
public Int16 wSecond;
public Int16 wMilliseconds;
public void getSysTime(byte[] Tzival,int offset) {
wYear = BitConverter.ToInt16(Tzival, offset);
wMonth = BitConverter.ToInt16(Tzival, offset + 2);
wDayOfWeek = BitConverter.ToInt16(Tzival, offset + 4);
wDay = BitConverter.ToInt16(Tzival, offset + 6);
wHour = BitConverter.ToInt16(Tzival, offset + 8);
wMinute = BitConverter.ToInt16(Tzival, offset + 10);
wSecond = BitConverter.ToInt16(Tzival, offset + 12);
wMilliseconds = BitConverter.ToInt16(Tzival, offset + 14);
private struct REG_TZI_FORMAT
public Int32 Bias;
public Int32 StandardBias;
public Int32 DaylightBias;
public SYSTEMTIME StandardDate;
public SYSTEMTIME DaylightDate;
public void regget(byte[] Tzival) {
Bias = BitConverter.ToInt32(Tzival, 0);
StandardBias = BitConverter.ToInt32(Tzival, 4);
DaylightBias = BitConverter.ToInt32(Tzival, 8);
StandardDate = new SYSTEMTIME();
StandardDate.getSysTime(Tzival, 12);
DaylightDate = new SYSTEMTIME();
DaylightDate.getSysTime(Tzival, 28);


static void Main(string[] args)
String tzString = @"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones\" + TimeZone.CurrentTimeZone.StandardName;
RegistryKey TziRegKey = Registry.LocalMachine;
TziRegKey = TziRegKey.OpenSubKey(tzString);
byte[] Tzival = (byte[])TziRegKey.GetValue("TZI");
GetUserAvailabilityRequestType fbRequest = new GetUserAvailabilityRequestType();
fbRequest.TimeZone = new SerializableTimeZone();
fbRequest.TimeZone.DaylightTime = new SerializableTimeZoneTime();
fbRequest.TimeZone.StandardTime = new SerializableTimeZoneTime();
fbRequest.TimeZone.Bias = rtRegTimeZone.Bias;
fbRequest.TimeZone.StandardTime.Bias = rtRegTimeZone.StandardBias;
fbRequest.TimeZone.DaylightTime.Bias = rtRegTimeZone.DaylightBias;
if (rtRegTimeZone.StandardDate.wMonth != 0)
fbRequest.TimeZone.StandardTime.DayOfWeek = ((DayOfWeek)rtRegTimeZone.StandardDate.wDayOfWeek).ToString();
fbRequest.TimeZone.StandardTime.DayOrder = (short)rtRegTimeZone.StandardDate.wDay;
fbRequest.TimeZone.StandardTime.Month = rtRegTimeZone.StandardDate.wMonth;
fbRequest.TimeZone.StandardTime.Time = String.Format("{0:0#}:{1:0#}:{2:0#}", rtRegTimeZone.StandardDate.wHour, rtRegTimeZone.StandardDate.wMinute, rtRegTimeZone.StandardDate.wSecond);
else {
fbRequest.TimeZone.StandardTime.DayOfWeek = "Sunday";
fbRequest.TimeZone.StandardTime.DayOrder = 1;
fbRequest.TimeZone.StandardTime.Month = 1;
fbRequest.TimeZone.StandardTime.Time = "00:00:00";

if (rtRegTimeZone.DaylightDate.wMonth != 0)
fbRequest.TimeZone.DaylightTime.DayOfWeek = ((DayOfWeek)rtRegTimeZone.DaylightDate.wDayOfWeek).ToString();
fbRequest.TimeZone.DaylightTime.DayOrder = (short)rtRegTimeZone.DaylightDate.wDay;
fbRequest.TimeZone.DaylightTime.Month = rtRegTimeZone.DaylightDate.wMonth;
fbRequest.TimeZone.DaylightTime.Time = "00:00:00";
else {
fbRequest.TimeZone.DaylightTime.DayOfWeek = "Sunday";
fbRequest.TimeZone.DaylightTime.DayOrder = 5;
fbRequest.TimeZone.DaylightTime.Month = 12;
fbRequest.TimeZone.DaylightTime.Time = "23:59:59";




Tuesday, April 17, 2007

Showing the percentage of Store being used by a User (or Users) on Exchange 2003 and 2007

If you haven’t been able to implement Mailbox quotas in your Exchange organization then you will probably find some users are using a large percentage of the used (or allocated space) in your Exchange database when compared to other users. If you work out this on a percentage basis this information can come in handy if you want to try to charge back to certain groups the cost of storing that information. E.g. you use this percentage of the resource so you pay this percentage of the cost (tell him he’s dreaming). I’ve put together a couple of scripts to help show this information as well as some other basic information about the user. The first couple of scripts show all the important information about a particular user. It takes one parameter which is the samaccountname and then does a AD Lookup and returns information about what Server the users mailbox is on what version of Exchange that mailserver is running what mailstore the mailbox is located in and what storage group that mailstore is located in. It then uses WMI to connect to the server in question and get the size of the database files on 2003 this includes the EDB and STM file which it adds together to get the combined file size. It then uses the Exchange_Mailbox class on Exchange 2003 to then retrieve the mailbox size of the user and the using a few divisions to get the percentage of store space the user is using. It then displays the results back to the user. The Exchange 2007 version does pretty much the same thing except that it uses the Exchange Powershell cmdlets from the Exchange Management Shell to get the same information. Most of the information can be returned with the get-mailboxstatistics cmdlet so the size of this script is a little smaller when compared to the Exchange 2003 version.

I’ve also put together a version of the script that returns the percentage used for every user in the domain that has a mailbox. This is where you can see a clear difference in why its easier to script against 2007 compared to 2003. The same script which took 50 lines in powershell in 2003 (okay I could have drop the GUID stuff and just used the legdn but that would have only saved about 4 lines) I could do the same thing in 10 lines in 2007 using the Exchange 2007 cmdlets with a big reduction in the complexity of the script.

In both these scripts I’ve used hashtables to store information and objects which can later be referenced as needed when trying to access the information across a number of servers and mail stores. In 2003 you need to do a bit of backtracking in AD to get the information about the mailboxes you are after. Eg on the user account you have DN of the mailstore the mailbox is located in the homemdb property you can take this and get the mailstore object from AD. On the mailstore object you have the msExchOwningServer property which has the DN of the server object in the configuration partition which you can then retrieve the netbios server name from which you can then use in your WMI query to get the mailbox size. Because you don’t really want to query the mailserver for each mailbox its easier to just grab all the mailbox sizes first and store then in a Hashtable and the just grab the size from the hashtable when needed.

I’ve put a downloadable copy of all the scripts here to show the difference in complexities between the 2003 version and 2007 version for the all users version the following is the 2007 version

$mbStores = @{ }
Get-mailboxdatabase | foreach {
$ffEdbFileFilter = "name='" + $_.edbfilepath.ToString().Replace("\","\\") + "'"
$mbEdbSize = get-wmiobject CIM_Datafile -filter $ffEdbFileFilter -ComputerName $_.ServerName

get-mailboxstatistics | foreach{
$divval = $mbStores[$_.Database]/100
$pcStore = ($_.TotalItemSize.Value/$divval)/100
$_.DisplayName + "," + $_.TotalItemSize.Value.ToMB() + "," + "{0:P1}" -f $pcStore

And this is the 2003 version

$snServerNames = @{ }
$mbSizes = @{ }
$mbStores = @{ }
$root = [ADSI]'LDAP://RootDSE'
$cfConfigRootpath = "LDAP://" + $root.ConfigurationNamingContext.tostring()
$configRoot = [ADSI]$cfConfigRootpath
$searcher = new-object System.DirectoryServices.DirectorySearcher($configRoot)
$searcher.Filter = '(objectCategory=msExchExchangeServer)'
$searcher1 = $searcher.FindAll()
#Get Server Objects
foreach($server in $searcher1){
$soServerObject = $server.getDirectoryEntry()
#Get Mailbox Sizes
$qrQueryresults = get-wmiobject -class Exchange_Mailbox -Namespace ROOT\MicrosoftExchangev2 -ComputerName $soServerObject.Name
foreach ($mbMailbox in $qrQueryresults){
$searcher.Filter = '(objectCategory=msExchPrivateMDB)'
$searcher2 = $searcher.FindAll()
foreach ($mailstore in $searcher2){
$moMailStoreObject = $mailstore.getDirectoryEntry()
$soServer = $snServerNames[[String]$moMailStoreObject.msExchOwningServer]
$ffEdbFileFilter = "name='" + $moMailStoreObject.msExchEDBFile.ToString().replace("\","\\") + "'"
$ffStmFileFilter = "name='" + $moMailStoreObject.msExchSLVFile.ToString().replace("\","\\") + "'"
$mbEdbSize =get-wmiobject CIM_Datafile -filter $ffEdbFileFilter -ComputerName $soServer.Name
$mbStmSize =get-wmiobject CIM_Datafile -filter $ffStmFileFilter -ComputerName $soServer.Name
[int64]$csCombinedSize = [double]$mbEdbSize.FileSize + [int64]$mbStmSize.FileSize

$dfDefaultRootPath = "LDAP://" + $root.DefaultNamingContext.tostring()
$dfRoot = [ADSI]$dfDefaultRootPath
$gfGALQueryFilter = "(&(&amp;(&(& (mailnickname=*)(objectCategory=person)(objectClass=user)(msExchHomeServerName=*)))))"
$dfsearcher = new-object System.DirectoryServices.DirectorySearcher($dfRoot)
$dfsearcher.Filter = $gfGALQueryFilter
$searcher2 = $dfsearcher.FindAll()
foreach ($uaUsers in $searcher2){
$uaUserAccount = New-Object System.DirectoryServices.directoryentry
$uaUserAccount = $uaUsers.GetDirectoryEntry()
$gaGuidArray = $uaUserAccount.msExchMailboxGuid.value
$adGuid = "{" + $gaGuidArray[3].ToString("X2") + $gaGuidArray[2].ToString("X2") + $gaGuidArray[1].ToString("X2") + $gaGuidArray[0].ToString("X2") + "-" +
$gaGuidArray[5].ToString("X2") + $gaGuidArray[4].ToString("X2") + "-" + $gaGuidArray[7].ToString("X2") + $gaGuidArray[6].ToString("X2") + "-" +
$gaGuidArray[8].ToString("X2") + $gaGuidArray[9].ToString("X2") + "-" + $gaGuidArray[10].ToString("X2") + $gaGuidArray[11].ToString("X2") +
$gaGuidArray[12].ToString("X2") + $gaGuidArray[13].ToString("X2") + $gaGuidArray[14].ToString("X2") + $gaGuidArray[15].ToString("X2") + "}"
$mbsize = [double]$mbSizes[$adGuid].Size
$divval = ($mbStores[$uaUserAccount.HomeMDB][0]/1024)/100
$pcStore = ($mbsize/$divval)/100
$uaUserAccount.Name.ToString() + "," + "{0:#.00}" -f ($mbsize/1KB) + "," + "{0:P1}" -f $pcStore

Thursday, April 05, 2007

WebService to Find Room and Equipment Mailboxes in Exchange 2007

In Exchange 2007 one of the new features is resource mailboxes out of the box you have two types of these mailboxes Room mailbox and Equipment mailbox. I’m rewriting some Intranet Meeting availability pages at the moment to work with Exchange Web Services and one thing you can’t do in EWS is run a query to find all these type of objects in your Exchange Organization. There are also a bunch of new features such as being able to set the resource capacity (eg how many people a room can hold) and custom resource properties (eg what type of things are in the room such as a whiteboard, projector etc). As this information is all stored in Active Directory you need to use LDAP to query this information. Because this is the type of thing I might want to use in multiple applications I thought I’d put together a little WebService that I could consume that would query this information on behalf of the requesting application and return information about the resource mailboxes firstly their email address's so i could then use this in a GetUserAvailability EWS operation and also where they are located and the extra resource capacity and resource custom properties.

The code to do this is pretty simple it’s just your standard System.DirectoryServices searching code the Ldap filter I used was to filter on the msExchRecipientDisplayType property which seems to get set to 7 for a Room Mailbox and 8 for a Equipment Mailbox. The WebService needs the rights to make these queries into the directory which you will need to solve with Impersonation and Delegation on your server. Alternatively you can hard code the alternate credential in your code (or web.config file) this is what I’m actually doing so If left that code in and just commend it out.

You can have a bit more fun with this if you expand your code eg you can use one of the Exchange custom attributes to store the Ip ranges of the local Ip subnets that are in close proximity to the Meeting Room's physical location. And then grab the IP of the client at the Intranet and use this information to return the meeting rooms close to where the client is making the request from (this depends a lot on your local setup).

I’ve put a downloadable copy of the Webserivce code here the code itself looks like.

using System;
using System.Web;
using System.Web.Services;
using System.Xml;
using System.Web.Services.Protocols;
using System.DirectoryServices;
using System.IO;

[WebService(Namespace = "")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class Service : System.Web.Services.WebService
public Service () {

public XmlDocument FindRooms() {
return FindMailboxes(7);
public XmlDocument FindEquipment()
return FindMailboxes(8);

private XmlDocument FindMailboxes(int MailboxType) {
string sqSearchQuery = "";
string mtMailboxType = "";
case 7:
sqSearchQuery = "(&(&(&(mailNickname=*)(objectcategory=person)(objectclass=user)(msExchRecipientDisplayType=7))))";
mtMailboxType = "Room Mailbox";
case 8:
sqSearchQuery = "(&(&(&(mailNickname=*)(objectcategory=person)(objectclass=user)(msExchRecipientDisplayType=8))))";
mtMailboxType = "Equipment";
XmlDocument rdReturnResult = new XmlDocument();
StringWriter xsXmlString = new StringWriter();
XmlWriter xrXmlWritter = new XmlTextWriter(xsXmlString);
xrXmlWritter.WriteAttributeString("type", mtMailboxType);
SearchResultCollection srSearchResults;
string roRootDSE = dsDirectorySearcher.SearchRoot.Path;
//string roRootDSE = "LDAP://dcName/DC=e2007dev,DC=domain,DC=com,DC=au";
//DirectoryEntry deDirectoryEntry = new DirectoryEntry(roRootDSE,
@"e2007dev\username", "password");
DirectoryEntry deDirectoryEntry = new DirectoryEntry(roRootDSE);
DirectorySearcher dsDirectorySearcher = new DirectorySearcher(deDirectoryEntry);
dsDirectorySearcher.SearchScope = SearchScope.Subtree;
dsDirectorySearcher.Filter = sqSearchQuery;
srSearchResults = dsDirectorySearcher.FindAll();
foreach (SearchResult srSearchResult in srSearchResults)
WriteAttributeValue(xrXmlWritter, srSearchResult, "msExchResourceCapacity");
WriteAttributeValue(xrXmlWritter, srSearchResult, "msExchResourceDisplay");
WriteAttributeValue(xrXmlWritter, srSearchResult, "displayName");
WriteAttributeValue(xrXmlWritter, srSearchResult, "co");
WriteAttributeValue(xrXmlWritter, srSearchResult, "department");
WriteAttributeValue(xrXmlWritter, srSearchResult, "physicalDeliveryOfficeName");
WriteAttributeValue(xrXmlWritter, srSearchResult, "postalCode");
WriteAttributeValue(xrXmlWritter, srSearchResult, "postOfficeBox");
WriteAttributeValue(xrXmlWritter, srSearchResult, "st");
WriteAttributeValue(xrXmlWritter, srSearchResult, "streetAddress");
WriteAttributeValue(xrXmlWritter, srSearchResult, "telephoneNumber");
return rdReturnResult;
private void WriteAttributeValue(XmlWriter xrXmlWritter, SearchResult
srSearchResult, String atAttribute)
if (srSearchResult.Properties.Contains(atAttribute))

Tuesday, April 03, 2007

Tracking Permission Changes to Mailbox rights and Send As – Receive As ACE’s in Exchange

First up this script is not designed to audit who is making changes to permissions in Exchange. Tracking who is making changes to Active Directory objects in a distributed Network with multiple DC’s is not an easy thing to do my advice is if you need to do this hit the Google trail or look at third party products.

What this script does is give visibility of permission changes to users mailboxes/accounts made via ADUC via Exchange Mailbox Rights and Send-As/Receive as rights on the user object. It basically works by creating an XML file that is a snapshot of the current explicitly added rights on an Exchange Mailbox and any ACE’s that grant the extended rights for send and receive as. The next time the script runs it takes another snapshot of the rights and then using a couple of Scripting Dictionary object s does a comparison and finds Add ,Deletes or Permission changes and then produces a HTML report of these changes. To extend the usefulness of this script I’ve created a version that creates a WMI watcher on any ds_user modification events on a Domain controller. When a modification is detected it uses the IADstools to check the metadata in the directory to see if any of the security attributes have been modified recently an if so then does a snapshot compare to see if picks up any ACE’s changes. Any ACE changes that are found are then emailed to an emailaddress as a Htmlreport. This starts to get close to real-time reporting of changes. Now in a large network maybe you don’t want to be doing this because of the number of ds_user modification events you might receive could possible have a detrimental performance effect on the DC is question (eg don’t know really haven’t tested it)

The script itself is split up into a few different sections the first section does file management. To maintain an audit path every snap that is taken is stored so when the script has been run more then 2 times the 1st snap that is taken is then copied to an archive directory. The second section of the script handles opening, searching and comparing the different snapshots that are taken. I’ve used the Microsoft Dom object to do this although not the easiest thing in the world to use it is good for this type of data retrieval. The last section of the script is dedicated to writing the snapshot because the format of the xml was pretty simple I’ve just used the FSO item to write the XML manually.

To use the script you should first check the following 4 variables and make sure the directories that variable refers to exists.

csCurrentSnapFileName = "c:\temp\currentSnap.xml"
psPreviousSnapFileName = "c:\temp\prevSnap.xml"
adArchieveDirectory = "c:\temp\SnapArchive\"
rfReportFileName = "c:\temp\ACLChangeReport-"

I’ve created two versions of the script the first is a simple version that is designed to be run from a scheduler and just looks for changes against the last snap every time its run and then writes a HTML report to the configured directory if any changes are detected. The second version is WMI watcher script that watches for changes to the ds_user class on the server and then emails a configured address with a report of any changes.
For this version of the script there are three variables that need to be configured first for email functionality section.

SenderAddress = ""
RecieverAddress = ""
EmailServername = “servername”

The script itself takes one commandline parameter which is the netbios name of the DC you want to run it against. The WMI version also requires that you have the Windows 2000/3 Support Tools installed so you will have the iadstools.dll registered. The other things to be aware of is there a 15 minute change detection period depending on the replication latency in your network you may need to make this greater.

If DateDiff("n",reformatdate(objIadsTools.MetaDataLastWriteTime(count)),Now()) < 15 Then fsnap = 1

I’ve put a downloadable copy of the script here the wmi version of the script looks like

Const RIGHT_DS_DELETE = &H10000
Const RIGHT_DS_READ = &H20000
Const RIGHT_DS_CHANGE = &H40000

csCurrentSnapFileName = "c:\temp\currentSnap.xml"
psPreviousSnapFileName = "c:\temp\prevSnap.xml"
adArchieveDirectory = "c:\temp\SnapArchive\"
rfReportFileName = "c:\temp\ACLChangeReport-"

SenderAddress = ""
RecieverAddress = ""
EmailServername = "mailserver"

strComputer = wscript.arguments(0)

Set fso = CreateObject("Scripting.FileSystemObject")
set objIadsTools = CreateObject("IADsTools.DCFunctions")
Set objWMIService = GetObject("winmgmts://" & strComputer & "/root/directory/LDAP")
strWQL = "SELECT * " & _
"FROM __InstanceModificationEvent " & _
"WITHIN 2 " & _
"WHERE TargetInstance ISA 'ds_user'"
WScript.Echo "Waiting User Modifications"
Set objEventSource = objWMIService.ExecNotificationQuery(strWQL)
Set objEventObject = objEventSource.NextEvent()
WScript.Echo "User Modified : " & objEventObject.TargetInstance.DS_displayName
Call getUserData(strComputer,objEventObject.TargetInstance.DS_distinguishedName)

sub getUserData(dcDomainController,dnUserDN)
fsnap = 0
intRes = objIadsTools.GetMetaData(Cstr(dcDomainController),Cstr(dnUserDN),0)
if intRes = -1 then
Wscript.Echo objIadsTools.LastErrorText
end if
wscript.echo "User" & dnUserDN
wscript.echo Now()
for count = 1 to intRes
Select Case objIadsTools.MetaDataName(count)
Case "nTSecurityDescriptor" wscript.echo "nTSecurityDescriptor LastModified : "
& objIadsTools.MetaDataLastWriteTime(count)_
& " Time Diff : " &
If DateDiff("n",reformatdate(objIadsTools.MetaDataLastWriteTime(count)),Now()) <
15 Then fsnap = 1
Case "msExchMailboxSecurityDescriptor" wscript.echo "msExchMailboxSecurityDescriptor
Last Modified : " _
& objIadsTools.MetaDataLastWriteTime(count) & " Time Diff : " &
If DateDiff("n",reformatdate(objIadsTools.MetaDataLastWriteTime(count)),Now()) <
15 Then fsnap = 1
End Select
If fsnap = 1 Then CheckACLs

end Sub

Sub CheckACLs()

rrRightReport = 0
If fso.FileExists(csCurrentSnapFileName) Then
wscript.echo "Snap Exists"
If fso.FileExists(psPreviousSnapFileName) Then
fso.movefile csCurrentSnapFileName, psPreviousSnapFileName
set xdXmlDocument = CreateObject("Microsoft.XMLDOM")
Set xnSnaptime = xdXmlDocument.selectNodes("//SnappedACLS")
For Each exSnap In xnSnaptime
oldSnap = exSnap.attributes.getNamedItem("SnapDate").nodeValue
wscript.echo "Snap Taken : " & oldSnap
afFileName = adArchieveDirectory &
","") & ".xml"
wscript.echo "Archiving Old Snap to : " & afFileName
fso.copyfile psPreviousSnapFileName, afFileName
set xdXmlDocument1 = CreateObject("Microsoft.XMLDOM")
Set ckCurrentPerms = CreateObject("Scripting.Dictionary")
Set pkPreviousPerms = CreateObject("Scripting.Dictionary")
Set xnCurrentPermsUsers = xdXmlDocument1.selectNodes("//User")
For Each xnUserNode In xnCurrentPermsUsers
unUserName = xnUserNode.attributes.getNamedItem("SamaccountName").nodeValue
For Each caACLs In xnUserNode.ChildNodes
ckCurrentACL = unUserName & "|-|" &
ckCurrentPerms.add ckCurrentACL,
Set xnPrevPermsUsers = xdXmlDocument.selectNodes("//User")
For Each xnUserNode1 In xnPrevPermsUsers
unUserName1 = xnUserNode1.attributes.getNamedItem("SamaccountName").nodeValue
For Each caACLs1 In xnUserNode1.ChildNodes
pkPrevACL = unUserName1 & "|-|" &
pkPreviousPerms.add pkPrevACL,
rem Do a Check for Any Deleted or Changed Permisssions
If ckCurrentPerms.exists(pkPrevACL) Then
If ckCurrentPerms(pkPrevACL) <>
caACLs1.attributes.getNamedItem("Right").nodeValue Then
rrRightReport = 1
wscript.echo "Found Changed ACL "
wscript.echo "Old Rights : " & pkPrevACL & " " &
wscript.echo "New Rights : " & pkPrevACL & " " & ckCurrentPerms(pkPrevACL)
hrmodHtmlReport = hrmodHtmlReport & "<tr><td><font face=""Arial""
color=""#000080"" size=""2"">" & unUserName1 & " </font></td>" & vbcrlf
hrmodHtmlReport = hrmodHtmlReport & "<td><font face=""Arial"" color=""#000080""
size=""2"">Old Rights: " & caACLs1.attributes.getNamedItem("User").nodeValue _
& " " & caACLs1.attributes.getNamedItem("Right").nodeValue & " </font></td>" &
hrmodHtmlReport = hrmodHtmlReport & "<td><font face=""Arial"" color=""#000080""
size=""2"">New Rights: " _
& caACLs1.attributes.getNamedItem("User").nodeValue & " " &
ckCurrentPerms(pkPrevACL) & " </font></td></tr>" & vbcrlf
End if
rrRightReport = 1
hrDelHtmlReport = hrDelHtmlReport & "<tr><td><font face=""Arial""
color=""#000080"" size=""2"">" & unUserName1 & " </font></td>" & vbcrlf
hrDelHtmlReport = hrDelHtmlReport & "<td><font face=""Arial"" color=""#000080""
size=""2"">" & caACLs1.attributes.getNamedItem("User").nodeValue _
& " " & caACLs1.attributes.getNamedItem("Right").nodeValue & "
</font></td></tr>" & vbcrlf
Wscript.echo "Found Deleted ACL : " & pkPrevACL & " " &
End if
rem Do forward check of ACL's
For Each dkCurrenPermKey In ckCurrentPerms.keys
If Not pkPreviousPerms.exists(dkCurrenPermKey) Then
rrRightReport = 1
dknewpermarry = Split(dkCurrenPermKey,"|-|")
hrnewHtmlReport = hrnewHtmlReport & "<tr><td><font face=""Arial""
color=""#000080"" size=""2"">" & dknewpermarry(0) & " </font></td>" & vbcrlf
hrnewHtmlReport = hrnewHtmlReport & "<td><font face=""Arial"" color=""#000080""
size=""2"">" & dknewpermarry(1) _
& " " & ckCurrentPerms(dkCurrenPermKey) & " </font></td></tr>" & vbcrlf
Wscript.echo "Found new ACL : " & dkCurrenPermKey & " " &
End if
wscript.echo "No current permissions snap exists taking snap"
Call TakeSnap
End If
If rrRightReport = 1 Then
wscript.echo "Writing Report"
hrHtmlReport = "<html><body>" & vbcrlf
NewSnapDate = WeekdayName(weekday(now),3) & ", " & day(now()) & " " &
Monthname(month(now()),3) & " " & year(now()) & " " & formatdatetime(now(),4) &
hrHtmlReport = hrHtmlReport & "<p><font size=""4"" face=""Arial Black""
color=""#008000"">Change To Mailbox Rights Report for Snaps Taken Between -
</font>" & oldSnap & " and "_
& NewSnapDate & "</font></p>" & vbcrlf
If hrnewHtmlReport <> "" Then
hrHtmlReport = hrHtmlReport & "<p><font face=""Arial"" color=""#000080""
size=""2"">ACL's Added</font></p>"
hrHtmlReport = hrHtmlReport & "<table border=""1"" width=""100%"" id=""table1""
cellspacing=""0"" cellpadding=""0"" bordercolor=""#000000"">"
hrHtmlReport = hrHtmlReport &
Replace(Replace(hrnewHtmlReport,"-exra-",""),"-exsa-","") & "</table>"
End If
If hrmodHtmlReport <> "" Then
hrHtmlReport = hrHtmlReport & "<p><font face=""Arial"" color=""#000080""
size=""2"">ACL's Modified</font></p>"
hrHtmlReport = hrHtmlReport & "<table border=""1"" width=""100%"" id=""table1""
cellspacing=""0"" cellpadding=""0"" bordercolor=""#000000"">"
hrHtmlReport = hrHtmlReport &
Replace(Replace(hrmodHtmlReport,"-exra-",""),"-exsa-","") & "</table>"
End If
If hrDelHtmlReport <> "" Then
hrHtmlReport = hrHtmlReport & "<p><font face=""Arial"" color=""#000080""
size=""2"">ACL's Deleted</font></p>"
hrHtmlReport = hrHtmlReport & "<table border=""1"" width=""100%"" id=""table1""
cellspacing=""0"" cellpadding=""0"" bordercolor=""#000000"">"
hrHtmlReport = hrHtmlReport &
Replace(Replace(hrDelHtmlReport,"-exra-",""),"-exsa-","") & "</table>"
End If
hrHtmlReport = hrHtmlReport & "</body></html>" & vbcrlf
Set objEmail = CreateObject("CDO.Message")
objEmail.From = SenderAddress
objEmail.To = RecieverAddress
objEmail.Subject = "ACL Change Report"
objEmail.htmlbody = hrHtmlReport
= 2
= EmailServername
= 25
wscript.echo "Email Sent"
End If

End Sub

Sub TakeSnap

set wfile = fso.opentextfile(csCurrentSnapFileName,2,true)
wfile.writeline("<?xml version=""1.0""?>")
wfile.writeline("<SnappedACLS SnapDate=""" & WeekdayName(weekday(now),3) & ", "
& day(now()) & " " & Monthname(month(now()),3) & " " & year(now()) & " " &
formatdatetime(now(),4) & ":00" & """>")

Set objSystemInfo = CreateObject("ADSystemInfo")
strdname = objSystemInfo.DomainShortName
set conn1 = createobject("ADODB.Connection")
strConnString = "Data Provider=NONE; Provider=MSDataShape"
conn1.Open strConnString
set conn = createobject("ADODB.Connection")
set com = createobject("ADODB.Command")
Set iAdRootDSE = GetObject("LDAP://RootDSE")
strNameingContext = iAdRootDSE.Get("defaultNamingContext")
Conn.Provider = "ADsDSOObject"
Conn.Open "ADs Provider"
Query = "<LDAP://" & strNameingContext & ">;(&(&(& (mailnickname=*) (|
Com.ActiveConnection = Conn
Com.CommandText = Query
Com.Properties("Page Size") = 1000
Set Rs = Com.Execute
While Not Rs.EOF
dn = "LDAP://" & replace(rs.Fields("distinguishedName").Value,"/","\/")
set objuser = getobject(dn)
Set oSecurityDescriptor = objuser.Get("msExchMailboxSecurityDescriptor")
Set oUserSecurityDescriptor = objuser.Get("ntSecurityDescriptor")
Set oUserdacl = oUserSecurityDescriptor.DiscretionaryAcl
Set oUserace = CreateObject("AccessControlEntry")
Set dacl = oSecurityDescriptor.DiscretionaryAcl
Set ace = CreateObject("AccessControlEntry")
fwFirstWrite = 0
For Each ace In dacl
if ace.AceFlags <> 18 then
if ace.Trustee <> "NT AUTHORITY\SELF" Then
If fwFirstWrite = 0 Then
wfile.writeline(" <User SamaccountName=""" & rs.fields("samaccountname") &
fwFirstWrite = 1
End if
wfile.writeline("<ACE User=""" & ace.Trustee & """ Right=""" &
getRights(ace.AccessMask) & """></ACE>")
end if
end if
For Each oUserace In oUserdacl
if lcase(oUserace.ObjectType) = "{ab721a54-1e2f-11d0-9819-00aa0040529b}" and
oUserace.AceType = 5 Then
if oUserace.Trustee <> "NT AUTHORITY\SELF" and oUserace.AceFlags <> 6 Then
if fwFirstWrite = 0 Then
wfile.writeline(" <User SamaccountName=""" & rs.fields("samaccountname") &
fwFirstWrite = 1
End If
wfile.writeline("<ACE User=""" & oUserace.Trustee & "-exsa-" & """ Right=""Send
end if
end if
if lcase(oUserace.ObjectType) = "{ab721a56-1e2f-11d0-9819-00aa0040529b}" and
oUserace.AceType = 5 Then
if oUserace.Trustee <> "NT AUTHORITY\SELF" and oUserace.AceFlags <> 6 then
If fwFirstWrite = 0 Then
wfile.writeline(" <User SamaccountName=""" & rs.fields("samaccountname") &
fwFirstWrite = 1
End If
wfile.writeline("<ACE User=""" & oUserace.Trustee & "-exra-" & """
Right=""Recieve As""></ACE>")
end if
end if

If fwFirstWrite = 1 then
End if
wscript.echo "New Snap Taken"

End Sub

Function getRights(hvHexValue)
If (hvHexValue And RIGHT_DS_SEND_AS) Then
getRights = "Send As"
End If
If (hvHexValue And RIGHT_DS_CHANGE) Then
getRights = "Modify user attributes"
End If
If (hvHexValue And RIGHT_DS_DELETE) Then
getRights = "Delete mailbox store"
End If
If (hvHexValue And RIGHT_DS_READ) Then
getRights = "Read permissions"
End If
getRights = "Take Ownership"
End If
getRights = "Mailbox Owner"
End If
getRights = "Mailbox Primary Owner"
End If
End Function

Function reformatdate(dtDatetoTransform)
dtarry = split(dtDatetoTransform,"/")
tmArry = split(dtarry(2)," ")
rdReturnDate = formatdatetime(dateserial(tmArry(0),dtarry(0),dtarry(1)) & " " &
tmArry(1) & " " & tmArry(2))
reformatdate = rdReturnDate
End function