Thursday, November 24, 2005

Using Monad and WMI with Exchange 2003

I’ve been playing around a bit more this week with the MSH beta and decided I’d share some of the stuff you can do using the WMI functionality in Monad. To start off here’s some one-liners that can be used to get information from the Exchange 2003 WMI classes. Learning how to do things in monad is a little tricky at the moment other peoples blog’s tend to be the best source of information I worked out the one-liners using a post from Adam Barr’s blog
http://www.proudlyserving.com/archives
/2005/08/monad_and_wmi.html
also the following blog was really useful as well as it has a whole bunch of samples that helped when I got stuck. http://mow001.blogspot.com/

Display the SMTP Message Queues

get-wmiobject -class Exchange_SMTPQueue -Namespace ROOT\MicrosoftExchangev2 -ComputerName servername | select-object LinkName,MessageCount,Size

Display Mailbox sizes

get-wmiobject -class Exchange_Mailbox -Namespace ROOT\MicrosoftExchangev2 -ComputerName servername | select-object MailboxDisplayName,TotalItems,Size

Display Users that are logged onto OWA

get-wmiobject -class Exchange_Logon -Namespace ROOT\MicrosoftExchangev2 -ComputerName servername -filter "ClientVersion = 'HTTP' and LoggedOnUserAccount != 'NT AUTHORITY\\SYSTEM'" | select-object LoggedOnUserAccount,MailboxDisplayname

Display all public folder sizes and message counts

get-wmiobject -class Exchange_Publicfolder -Namespace ROOT\MicrosoftExchangev2 -ComputerName servername | select-object name,messagecount,totalmessagesize

The last one is a sample of listening to the Exchange_Logon class modification events this one still needs a little bit of tweaking

$objConn = New-Object System.Management.ConnectionOptions
$objconn.Impersonation = [System.Management.ImpersonationLevel]::Impersonate
$objconn.EnablePrivileges = 1
$tspan = New-Object System.TimeSpan(0, 0, 10)
$exmangescope = New-Object System.Management.ManagementScope("\\servername\root\MicrosoftExchangeV2", $objconn);
$query = New-Object System.Management.WqlEventQuery("__InstanceModificationEvent",$tspan, "TargetInstance isa `"Exchange_Logon`" and TargetInstance.ClientVersion = `"HTTP`" and TargetInstance.LoggedOnUserAccount != `"NT AUTHORITY\\SYSTEM`"")
$watcher = New-Object System.Management.ManagementEventWatcher($exmangescope,$query)
$e = $watcher.WaitForNextEvent()
$des = 1
while($des -eq 1){
$e.TargetInstance.LoggedOnUserAccount
$e = $watcher.WaitForNextEvent()
Trap{Break}
}

Sending a SysLog Message using Monad

I’m a bit fan on using Syslog for monitoring and logging and after putting the new version of MSH on my machine that works with the release version of 2.0 .NET framework (make sure you uninstall any old versions of the monad beta before you install the new framework or your going to be in for some problems) I thought I’d have a go at seeing if I could use my C# code from this post in a monad script. After a little bit of translating and lot of learning I managed to get something that works. It’s about six lines so it’s nice and lean. The good part is you could throw it into a command-let and then use it as a one-liner in another script. The code itself looks like this. This sample sends a USER:Warning Priorty message


$SL = New-Object System.Net.Sockets.UdpClient("192.168.1.115", 514)
$Message = "<12>date=" + [DateTime]::get_Now().tostring("yyyy-MM-dd") + ",time=" + [DateTime]::get_Now().tostring("hh:mm:ss")
$Message = $Message + ",msg=`"The Sky is Falling`""
[byte[]] $rawMSG = $(new-object System.Text.ASCIIEncoding).GetBytes($message)
$SL.Send($rawMSG, $rawMSG.Length);
$SL.close()

Building a Better Link Monitor using WMI – Exchange 2003

Someone asked me a question last week about using the Queue class’s in Exchange 2003 and this got me thinking about link monitors. Link monitors are a little bit old terminology these days although I really used to like the old site and link monitors in Exchange 5.5 especially the visual representation. Now there are monitors on Exchange 2003 and while useful they are really lacking in being able to tell you if there actually is a problem and what that problem might be.

So I decided to see if I could build a better link monitor that would one tell me when there is a problem and also let me know in the warning email what that problem might be. Eg if there are lot of messages queuing up send me a dump of what the message queues looks like and tell me what messages are in the queue. Then with any luck I can tell from the email if there really is a problem that I might need to look at or if its just a temporarily large volume of email being sent. Eg the first thing you do when you get a warning about a problem with mail queues is to go and check what’s in them so I was trying to cut this step out.

So the solution I put together was a script that would listen for modification events on the Exchange_SMTPQueue Wmi class with a filter so it would only take action when the number of message in any of the queues went over a configured threshold. When the threshold is reached it would query all queues on the box and build a html table of the results and it would also then enumerate the messages in the queues that were over threshold and create a html table of the result of this enumeration. The html tables would then form the body of an alert email which would be sent. To stop the script sending email every update period which is about every 15 seconds or so the script tracks the last time an alert was sent so it will only send 1 alert per hour if the queues are still over threshold.

The script itself uses 3 WMI queries the first query listens for the Queue modification events. The second query enumerates the queues the third query enumerates the messages within a queue that are over threshold. A mail is then sent over SMTP using CDOEX/CDOSYS. The script itself can be run locally or remotely as long as there is CDOEX or CDOSYS installed on avaible on the machine to send the message.

To use the script you need to configure four things within the script the first is the computer name the second is the email address its sending from the email address its sending to and the server its sending through so the following 4 lines needs to be customised.

cComputerName = "."
objEmail.From = "Queuewarnings@yourdomain.com"
objEmail.To = "somebody@yourdomain.com"
objEmail.Configuration.Fields.Item("http://schemas.microsoft.com/cdo/configuration/smtpserver") = "Servername"
I’ve put a download copy of the script here the script itself looks like

cComputerName = "."
MessageThreshold = 5
LastAlertSent = dateadd("h",-1,now())
Set objWMIService = GetObject("winmgmts:" _
& "{impersonationLevel=impersonate}!\\" & _
cComputerName & "\root\MicrosoftExchangeV2")
Set colMonitoredEvents = objWMIService.ExecNotificationQuery _
("SELECT * FROM __InstanceOperationEvent WITHIN 10 WHERE " _
& "Targetinstance ISA 'Exchange_SMTPQueue' and TargetInstance.MessageCount >= "
& MessageThreshold)
Do
Set objLatestEvent = colMonitoredEvents.NextEvent
Wscript.echo now() & " " & objLatestEvent.TargetInstance.LinkName & " " &
objLatestEvent.TargetInstance.MessageCount & " " &
objLatestEvent.TargetInstance.Size
if LastAlertSent < dateadd("h",-1,now()) then
call EnumSMTPQueues()
LastAlertSent = now()
end if
Loop


sub EnumSMTPQueues()
Const cWMINameSpace = "root/MicrosoftExchangeV2"
Const cWMIInstance = "Exchange_SMTPQueue"
HtmlMsgbody = "<table border=""1"" width=""100%"" cellpadding=""0"" bordercolor=""#000000""><tr><td
bordercolor=""#FFFFFF"" align=""center"" bgcolor=""#000080"">" _
& "<b><font color=""#FFFFFF"">Queue Name</font></b></td><td bordercolor=""#FFFFFF""
align=""center"" bgcolor=""#000080""<b><font color=""#FFFFFF"">Message
Count</font></b></td>" _
& "<td bordercolor=""#FFFFFF"" align=""center"" bgcolor=""#000080""><b><font
color=""#FFFFFF"">Queue Size</font></b></td></tr>"
strWinMgmts = "winmgmts:{impersonationLevel=impersonate}!//"& _
cComputerName&"/"&cWMINameSpace
Set objWMIExchange = GetObject(strWinMgmts)
If Err.Number <> 0 Then
WScript.Echo "ERROR: Unable to connect to the WMI namespace."
Else
Set listExchange_PublicFolders = objWMIExchange.InstancesOf(cWMIInstance)
For Each objExchange_SMTPQueue in listExchange_PublicFolders
HtmlMsgbody = HtmlMsgbody & "<tr><td>" & objExchange_SMTPQueue.LinkName &
"</td><td>" & objExchange_SMTPQueue.MessageCount _
& "</td><td>" & objExchange_SMTPQueue.size & "</td></tr>"
WScript.echo objExchange_SMTPQueue.LinkName & " " &
objExchange_SMTPQueue.MessageCount & " " & objExchange_SMTPQueue.size
if objExchange_SMTPQueue.MessageCount >= MessageThreshold then
wql ="Select * From Exchange_QueuedSMTPMessage Where LinkId='" &
objExchange_SMTPQueue.LinkID
wql = wql & "' And LinkName='" & objExchange_SMTPQueue.Linkname & "' And
ProtocolName='SMTP' And "
wql = wql & "QueueId='" & objExchange_SMTPQueue.QueueID & "' And QueueName='" &
objExchange_SMTPQueue.Queuename &"' And"
wql = wql & " VirtualMachine='" & objExchange_SMTPQueue.VirtualMachine & "'"
wql = wql & " And VirtualServerName='" & objExchange_SMTPQueue.VirtualServerName
& "'"
quehtml = quehtml & getmess(wql)
end if
next
End If
HtmlMsgbody = HtmlMsgbody & "</table><BR><B>Message Queues</B><BR>" & quehtml
Set objEmail = CreateObject("CDO.Message")
objEmail.From = "Queuewarnings@yourdomain.com"
objEmail.To = "somebody@yourdomain.com"
objEmail.Subject = "Queue Threshold Exceeded"
objEmail.HTMLbody = HtmlMsgbody
objEmail.Configuration.Fields.Item("http://schemas.microsoft.com/cdo/configuration/sendusing")
= 2
objEmail.Configuration.Fields.Item("http://schemas.microsoft.com/cdo/configuration/smtpserver")
= "Servername"
objEmail.Configuration.Fields.Item("http://schemas.microsoft.com/cdo/configuration/smtpserverport")
= 25
objEmail.Configuration.Fields.Update
objEmail.Send
wscript.echo "message sent"
End sub

function getmess(wql)
quehtml = "<table border=""1"" width=""100%""><tr><td bgcolor=""#008000""
align=""center""><b><font color=""#FFFFFF"">Date Sent</font></b></td>" _
& "<td bgcolor=""#008000"" align=""center""><b><font color=""#FFFFFF"">Sent
By</font></b></td>"_
& " <td bgcolor=""#008000"" align=""center""><b><font color=""#FFFFFF"">Recipients</font></b></td>"_
& " <td bgcolor=""#008000"" align=""center""><b><font color=""#FFFFFF"">Subject</font></b></td>"_
& " <td bgcolor=""#008000"" align=""center""><b><font color=""#FFFFFF"">Size</font></b></td></tr>"
Const cWMINameSpace = "root/MicrosoftExchangeV2"
strWinMgmts = "winmgmts:{impersonationLevel=impersonate}!//" & cComputerName &
"/" & cWMINameSpace
Set objWMIExchange = GetObject(strWinMgmts)
Set listExchange_MessageQueueEntries = objWMIExchange.ExecQuery(wql)
For each objExchange_MessageQueueEntries in listExchange_MessageQueueEntries
recieved =
dateadd("h",toffset,cdate(DateSerial(Left(objExchange_MessageQueueEntries.Received,
4), Mid(objExchange_MessageQueueEntries.Received, 5, 2),
Mid(objExchange_MessageQueueEntries.Received, 7, 2)) & " " &
timeserial(Mid(objExchange_MessageQueueEntries.Received, 9,
2),Mid(objExchange_MessageQueueEntries.Received, 11,
2),Mid(objExchange_MessageQueueEntries.Received,13, 2))))
Wscript.echo recieved & " " & objExchange_MessageQueueEntries.Sender & " " &
objExchange_MessageQueueEntries.Subject _
& " " & objExchange_MessageQueueEntries.size & " " &
replace(replace(objExchange_MessageQueueEntries.Recipients(0),vbcrlf,""),"Envelope
Recipients:","")
quehtml = quehtml & "<tr><td>" & recieved &"</td><td>" &
objExchange_MessageQueueEntries.Sender & "</td><td>" &
replace(replace(objExchange_MessageQueueEntries.Recipients(0),vbcrlf,""),"Envelope
Recipients:","") & "</td><td>" _
& objExchange_MessageQueueEntries.Subject & "</td><td>" &
objExchange_MessageQueueEntries.size & "</td></tr>"
next
quehtml = quehtml & "</table>"
getmess = quehtml
end function

Thursday, November 17, 2005

Allowing someone to update their own Active Directory Contact details via OWA

Modifying your own user details via GALMOD or one of it derivates has been around for sometime. I wrote this little app a few years ago at the bequest of a secretary to allow users with non admin rights to modify other people’s details using a WSC Com object. A question came up last week about being able to do this in OWA via a public folder for those users that might be outside your network that may only have access to OWA though a front backend setup etc. Exchange does give you the ability in OWA to register and run your own custom forms (do a search on WSS.forms in the Exchange SDK for more details). So one way of achieving what this person asked to do was to register a custom ASP WSS form and include some ADSI to do the work of modifying the user’s account and phone details. If you’re not running Exchange on a Domain controller (which should be the case for most people bar SBS users) you have to consider delegation issues when your page is going to request the changes be made on behalf of the user. A couple a ways to work around this is to specify alternate credentials in your code to make the changes with. Or use a COM+ wrapper and a com object (or a WSC com object like I did here). Security wise it’s usually better to use a COM+ wrapper to store your username and password instead of storing it as clear text in the asp file if your going to be storing your asp file in the Exchange store where someone may be able to get easy access to the source. I decided to put together a real simple example of a WSS.From that could be used to change a user’s phone number detail’s using ADSI and some hard coded credentials.

The code itself is pretty simple installing it and getting it to work can be a little challenging for the uninitiated. I’ve used the method that is prescribed in the Exchange SDK which involves a few steps.

Before you start you need to make sure that you enable scripts for ASP pages on the Exchange server or you will receive a 403 permissions error when you try and view the folder. To enabled scripts you use Exchange System Manager go to Servers-Protocols-HTTP-Exchange Virtual Directory. Select the Public virtual directory and then right click and select properties. Go to the access table under execute permission select scripts then save and exit.

Hard coded script bits the following script has 2 hardcoded bits you need to change to use it the first is the servername of a Domain controller where the changes are going to be made. The second is the username and password of a user with rights to make changes to the properties that you are modifying. Both these setting are in the following line

Set oUser = Dirobj.OpenDSObject("LDAP://servername/"& sysinfo.UserName, "domain\user", "password", 0)

Installation I’ve created a script call forminst.vbs and included it in the download of this post that performs the following steps for you it will prompt you for the name of the root folder to create and it will also upload the asp page from d:\ into the public folder

The first step is to create the folder you want the form to be registered on.
The next step is to create a hidden folder that will contain the form registration and the ASP page itself.
The next step is to set the urn:schemas-microsoft-com:exch-data:schema-collection-ref property on the parent folder to point to the folder that was created in step two.
The next step is to create the default form registration on the child folder that will tell exchange to display the ASP page for any requests made for the parent folder in OWA.
The last step is then to upload the asp page to the child folder.

That’s it if you receive a 403 error when you try and view the folder in OWA make sure you have set scripting rights in Exchange System Manager (the rights should also be reflected in IIS admin). From a security point of view you should consider building a com object and using a COM+ wrapper if you where going to use this in production.

I’ve put a downloadable copy of the code from this post here the Form itself looks like

<%
Set oForm = Server.CreateObject("WSS.Form")
dim sysinfo
dim oUser
Set sysinfo = CreateObject("ADSystemInfo")
set Dirobj = GetObject("LDAP:")
Set oUser = Dirobj.OpenDSObject("LDAP://DCservername/"& sysinfo.UserName,
"domain\user", "password", 0)
If Request.ServerVariables("REQUEST_METHOD") = "GET" Then
oForm.fields("FName").value = oUser.GivenName
oForm.fields("LName").value = oUser.sn
oForm.fields("Bphone").value = oUser.TelephoneNumber
oForm.fields("Mphone").value = oUser.mobile
oForm.fields("Hphone").value = oUser.homephone
oForm.fields.update
oForm.Render
else
If Request.ServerVariables("REQUEST_METHOD") = "POST" Then
if oForm.Fields("FName").Value = "" then
oForm.Elements("FName").ErrorString = "<span style=""COLOR:red"">First Name
Required<span>"
oForm.Render
elseif oForm.Fields("LName").Value = "" then
oForm.Elements("LName").ErrorString = "<span style=""COLOR:red"">Last Name
Required<span>"
oForm.Render
else
oUser.GivenName = oForm.Fields("FName").Value
oUser.sn = oForm.Fields("LName").Value
oUser.TelephoneNumber = oForm.Fields("BPhone").Value
if oForm.Fields("BPhone").Value = "" then
oUser.putex 1,"TelephoneNumber", vbNull
else
oUser.TelephoneNumber = oForm.Fields("BPhone").Value
end if
if oForm.Fields("MPhone").Value = "" then
oUser.putex 1,"mobile", vbNull
else
oUser.mobile = oForm.Fields("MPhone").Value
end if
if oForm.Fields("HPhone").Value = "" then
oUser.putex 1,"homephone", vbNull
else
oUser.homephone = oForm.Fields("HPhone").Value
end if
oUser.setinfo
oForm.Elements("Result").ErrorString = "<span style=""COLOR:red"">Updated<span>"

oForm.Render
end if
else
end if
end if
%>

<HTML>
<HEAD>
<BASE TARGET="_top">

</HEAD>

<BODY><!The Data URL macro is expanded at runtime by
the renderer so that the Submit
button will post back to the item itself.>
<FORM action="" id=FORM1 method=post name="FORM1"
target="_self">

<H1> Phone Number Details
<INPUT class="field" name="result"
style="HEIGHT: 25px; WIDTH: 0px"></H1>
<br><br>

<b>First Name:</b> <INPUT class="field" name="FName"
style="HEIGHT: 25px; WIDTH: 200px"> <br><br>
<b>Last Name:</b> <INPUT class="field" name="LName"
style="HEIGHT: 25px; WIDTH: 200px"> <br><br>
<b>Business PhoneNumber:</b> <INPUT class="field"
name="Bphone" style="HEIGHT: 23px; WIDTH: 200px">
<br><br>
<b>Mobile PhoneNumber:</b><INPUT class="field"
name="Mphone" style="HEIGHT: 23px; WIDTH: 200px">
<br><br>
<b>Home Phone:</b> <INPUT class="field" name="Hphone"
style="HEIGHT: 25px; WIDTH: 200px"> <br>
<br><br>
&nbsp;<INPUT id=submit1 name=submit1 type=submit
value=Submit>

</FORM>

</BODY>
</HTML>
 

Show which users have been delegated an Exchange Administrator role via the ESM delegation wizard by script.

When users are delegated exchange Administrator rights via Exchange System Manager’s delegation wizard these users are assigned specific rights depending on the role that is selected in Active Directory to Exchange objects that are in the configuration partition and the Exchange System objects container in the domain partition. If you want to show which users have been delegated rights via this method its a matter of checking the Access Control Entries in one of the DACL’s associated with one of these containers. Because these are defined roles the accessmask on the ACE will be consistent depending on the role you select. So after a little trial and error the following script can be used to check and display the users that have been assigned rights via the ESM delegation wizard. This script checks the root of the Exchange configuration container in the Active directory Configuration partition

Set iAdRootDSE = GetObject("LDAP://RootDSE")
strNameingContext = iAdRootDSE.Get("configurationNamingContext")
sUserADsPath = "LDAP://CN=Microsoft Exchange,CN=Services," & strNameingContext
Set objadlist = GetObject(sUserADsPath)
Set oSecurityDescriptor = objadlist.Get("ntSecurityDescriptor")
Set dacl = oSecurityDescriptor.DiscretionaryAcl
Set ace = CreateObject("AccessControlEntry")
For Each ace In dacl
if ace.AceFlags = 2 then
select case ace.AccessMask
case 131220 Wscript.echo ace.Trustee & " Exchange View Only Adiministrator"
case 197119 Wscript.echo ace.Trustee & " Exchange Administrator"
case 983551 Wscript.echo ace.Trustee & " Exchange Full Administrator"
end select
end if
Next
wscript.echo
wscript.echo "Done viewing descriptor"

Thursday, November 10, 2005

Displaying a Public Folders Creator and Folder Contacts via WebDAV

It seems I’m on a bit of a public folder theme of late. This script came up when I was working on the public folder audit log script unfortunately it was a little wasted but its still a useful sample of how you might go about finding the creator and public folder contacts of a public folder via WebDAV (or it should also work with Exoledb) using the XML security descriptors provided though the http://schemas.microsoft.com/exchange/security/ namespace. Documentation for the XML security descriptor can be found here . Also the appendix of the documentation that comes with Pfdavadmin is also very good if you trying to decode the descriptor.

Getting the Folder Creator

To get the folder creator via webdav this involves doing a propfind on the http://schemas.microsoft.com/exchange/security/creator property. You should then be able to parse the nt4_compatible_name from the XML that is returned to display the account that created the public folder. If you can’t get the nt4_compatible_name then you’ll have to work with the SID instead and do a resolution of this.

Get the Folder Contacts

This was the most challenging part of the script to determine if someone is a folder contact or not you have to check the DACL on the public folder itself. Although the folder_contact is not a security right in itself it still is part of the DACL acessmask. So to get this information via WebDAV you do a propfind on the http://schemas.microsoft.com/exchange/security/descriptor property which will return a XML representation of the DACL for that folder. You can use Pfdavadmin to have a look at what this will look like. The DACL contains the acessmasks that determine what rights a user has to a folder. Exchange DACL’s aren’t that straightforward however a really good description of the format that is used can be found in the pfdavadmin documentation. But basically you have 3 parts to the DACL usually the effective and subcontainer rights are the same and these make up the folder rights. And you also have the subitem rights. To work out if someone is a folder contact all you need to do is check on one of the folder rights parts. But within each of these parts there is a Allow access mask and a deny access mask which complicates things a little more. So you must remember to check both the allow and deny masks to come up with the right answer. When you retrieve the ACE’s themselves via WebDAV you get a Access Mask which represents that rights a user has on the folder. The format of an access mask is explained here . I never seen an explanation of what each of the bit values are for Exchange permissions but from what I can work out the bit that controls whether are user is a contact on the folder is 1000000000000000. So what I’ve made the script do is first convert the accessmask back into a 32 binary representation and then treating it like a string check to see if this bit is set in this string. The Deny access mask is also checked to see if this bit is set which would override any allow. This method is a little bit out there but does seem to work I would never however recommend you go about trying to set a DACL via this method which could spell very bad things for your store. You can use the XML descriptor to set permission but always use the documented access masks.

So the script to check the folder creator and contacts look like this I’ve put a downloadable copy here


set req = createobject("microsoft.xmlhttp")
folderurl = "http://servername/public/folder"

xmlreqtxt = "<?xml version='1.0'?><a:propfind xmlns:a='DAV:' xmlns:s='http://schemas.microsoft.com/exchange/security/'><a:prop><s:creator/></a:prop
></a:propfind>"
req.open "
PROPFIND", folderurl, false, "", ""
req.setRequestHeader "Content-Type", "text/xml; charset=""UTF-8"""
req.setRequestHeader "Depth", "0"
req.setRequestHeader "Translate", "f"
req.send xmlreqtxt
If req.status >= 500 Then
ElseIf req.status = 207 Then
set oResponseDoc = req.responseXML
set oNodeList = oResponseDoc.getElementsByTagName("S:nt4_compatible_name")
For i = 0 To (oNodeList.length -1)
set oNode = oNodeList.nextNode
wscript.echo "Folder Created By : " & oNode.text
next
end if

xmlreqtxt = "<?xml version='1.0'?><a:propfind xmlns:a='DAV:'
xmlns:s='http://schemas.microsoft.com/exchange/security/'
><a:prop><s:descriptor/></a:prop
></a:propfind>"
req.open "
PROPFIND", folderurl, false, "", ""
req.setRequestHeader "Content-Type", "text/xml; charset=""UTF-8"""
req.setRequestHeader "Depth", "0"
req.setRequestHeader "Translate", "f"
req.send xmlreqtxt
set oResponseDoc = req.responseXML
set oNodeList = oResponseDoc.getElementsByTagName("S:effective_aces")
set oNode = oNodeList.nextnode
set oNodeList1 =
oNode.selectnodes("S:access_allowed_ace/S:sid/S:nt4_compatible_name")
set oNodeList2 = oNode.selectnodes("S:access_allowed_ace/S:access_mask")
For nl = 1 To oNodeList1.length
set oNode1 = oNodeList1.nextnode
set oNode2 = oNodeList2.nextnode
binmask = getbinval(oNode2.Text)
if len(binmask) > 16 then
if mid(right(binmask,16),1,1) = 1 then
Contacts = Contacts & oNode1.Text & ","
end if
end if
Next
set oNodeList3 =
oNode.selectnodes("S:access_denied_ace/S:sid/S:nt4_compatible_name")
set oNodeList4 = oNode.selectnodes("S:access_denied_ace/S:access_mask")
For nl1 = 1 To oNodeList3.length
set oNode3 = oNodeList3.nextnode
set oNode4 = oNodeList4.nextnode
binmask = getbinval(oNode4.Text)
if len(binmask) > 16 then
if mid(right(binmask,16),1,1) = 1 then
Contacts = replace(Contacts,oNode3.Text & ",","")
end if
end if
Next
wscript.echo "Folder Contacts : " & Contacts
function getbinval(mask)
binval = " "
for bv = 1 to len(mask)
select case mid(mask,bv,1)
case "f" binval = binval & "1111"
case "e" binval = binval & "1110"
case "d" binval = binval & "1101"
case "c" binval = binval & "1100"
case "b" binval = binval & "1011"
case "a" binval = binval & "1010"
case "9" binval = binval & "1001"
case "8" binval = binval & "1000"
case "7" binval = binval & "0111"
case "6" binval = binval & "0110"
case "5" binval = binval & "0101"
case "4" binval = binval & "0100"
case "3" binval = binval & "0011"
case "2" binval = binval & "0010"
case "1" binval = binval & "0001"
case "0" binval = binval & "0000"
end select
next
getbinval = binval
end function

Firing an event whenever a new Public folder is created

I was talking with someone this week about an issue where they wanted to maintain the functionality that existed pre going into native mode on Exchange 2003 that would allow public folders to be mail enabled and hidden from the Address list by default. Now there are a lot of good reasons why this is no longer the default action within Exchange but there are also some reasons why you may want this to still happen on a branch of your public folder tree. An event sink might be an obvious way to go about this but because an event sink can only have a shallow match scope on the default public folder tree there not a good choice for this job unless you only want this to work on one folder and not child folders.

What you can do however using the Exchange_PublicFolder WMI class is setup something that will listen for Instance creation events which will happen when a new public folder is created then check the path where the folder is being created and then if you want to have that folder mail-enabled use the WMI IsMailEnabled and PublishInAddressBook properties which are read/write. You could also monitor the _InstanceOperationEvent which would pickup every operation in a public folder but the number of events on busy exchange server would be excessive and you may would pay a large performance penalty. The creation event if setup correctly should only every catch new folder creations. A sample script to do this would look like the following the script checks to see if the folder is being created in the help desk branch of the public folder tree and then mail enables and hides the folder from the Gal via WMI.

strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
& "{impersonationLevel=impersonate}!\\" & _
strComputer & "\root\MicrosoftExchangeV2")
Set colMonitoredEvents = objWMIService.ExecNotificationQuery _
("SELECT * FROM __InstanceCreationEvent WITHIN 10 WHERE " _
& "Targetinstance ISA 'Exchange_PublicFolder' ")
Do
Set objLatestEvent = colMonitoredEvents.NextEvent
path = objLatestEvent.TargetInstance.path
if instr(path,"/help desk/") then
objLatestEvent.TargetInstance.PublishInAddressBook = false
objLatestEvent.TargetInstance.IsMailEnabled = true
objLatestEvent.TargetInstance.Put_()
wscript.echo "Mail Enabled Folder : " & path
end if
Loop

Displaying deleted public folder tracking information via script in Exchange 2003 sp2 – Extended Edition

Last week I posted this script that displayed the deleted public folder audit logs. At the time I was working on a extended version that would also show information about the size of the folder that was deleted and number of items, who owned it and who the folder contacts where. I’ve had a little bit of mixed success with this script I was able to do a shallow deleted traversal query using webdav to show the size and number of items of a deleted public folder based on the information in the event log. But this only worked if the parent of the deleted folder still existed. Further more when I tried to do a propfind to work out the owner and folder contacts using the http://schemas.microsoft.com/exchange/security/ xml descriptors. (even though I spent a fair bit of time coming up with some script to work out the contacts) this didn't work. It seems that you can't access the soft deleted folder item directly with a propfind but you can delete and copy them. So the final result was a little bit disappointing but still useful none the less basically it works the same as last weeks script but also does a webdav query to find the size of the soft deleted folder and the number of items within that folder. This version is not setup for Form Based Authentication but that would be easy to add if you needed it. I’ve put a downloadable copy of the script here the script itself looks like


days = wscript.arguments(1)
servername = wscript.arguments(0)
SB = 0
Set fso = CreateObject("Scripting.FileSystemObject")
set wfile = fso.opentextfile("c:\PfDeletes.csv",2,true)
wfile.writeline("DateDeleted,FolderName,FolderID,DeletedBy-MailboxName,DeletedBy-UserName,FolderSize,NumberofItems")
dtmStartDate = CDate(Date) - days
dtmStartDate = Year(dtmStartDate) & Right( "00" & Month(dtmStartDate), 2) &
Right( "00" & Day(dtmStartDate), 2)
Set objWMIService = GetObject("winmgmts:{impersonationLevel=impersonate}!\\" &
servername & "\root\cimv2")
Set colLoggedEvents = objWMIService.ExecQuery("Select * from Win32_NTLogEvent
Where Logfile='Application' and Eventcode = '9682' and TimeWritten >= '" &
dtmStartDate & "' ",,48)
wscript.echo "Date Deleted FolderName FolderID Deleter-Mbox Deleter-UserName"
For Each objEvent in colLoggedEvents
SB = 1
Time_Written = cdate(DateSerial(Left(objEvent.TimeWritten, 4),
Mid(objEvent.TimeWritten, 5, 2), Mid(objEvent.TimeWritten, 7, 2)) & " " &
timeserial(Mid(objEvent.TimeWritten, 9, 2),Mid(objEvent.TimeWritten, 11,
2),Mid(objEvent.TimeWritten,13, 2)))
FolderName = Mid(objEvent.Message,8,instr(objEvent.Message,"with folder")-9)
FolderID = Mid(objEvent.Message,instr(objEvent.Message,"with folder
ID")+15,(instr(objEvent.Message,"was deleted by")-(instr(objEvent.Message,"with
folder ID")+16)))
MailboxName = Mid(objEvent.Message,instr(objEvent.Message,"was deleted
by")+15,instr(objEvent.Message,", user account")-(instr(objEvent.Message,"was
deleted by")+15))
UserName = Mid(objEvent.Message,instr(objEvent.Message,", user
account")+15,(instr(objEvent.Message,chr(13))-(instr(objEvent.Message,", user
account")+16)))
retarry = showdeletails(servername,FolderName)
wscript.echo Time_Written & " " & FolderName & " " & FolderID & " " &
MailboxName & " " & UserName & " " & retarry(0) & " " & retarry(1)
wfile.writeline(Time_Written & "," & FolderName & "," & FolderID & "," &
MailboxName & "," & UserName & "," & retarry(0) & "," & retarry(1))
next
wfile.close
set wfile = nothing
if SB = 0 then queryeventlog = "No Public Folder Deletes recorded in the last "
& days & "Days"

function showdeletails(servername,pfpath)
dim retarry(2)
retarry(0) = " "
retarry(1) = " "
if instr(2,pfpath,"/") then
lastslash = ""
for i = 2 to len(pfpath)
if mid(pfpath,i,1) = "/" then
lastslash = i
end if

next
rfolder = "http://" & servername & "/public" & left(pfpath,int(lastslash))
else
rfolder = "http://" & servername & "/public/"
end if

strQuery = "<?xml version=""1.0""?><D:searchrequest xmlns:D = ""DAV:"" >"
strQuery = strQuery & "<D:sql>SELECT ""DAV:displayname"",
""http://schemas.microsoft.com/mapi/proptag/x669B0014"", "
strQuery = strQuery & """http://schemas.microsoft.com/mapi/proptag/x66400003"",
""http://schemas.microsoft.com/exchange/permanenturl"" FROM scope('SOFTDELETED
traversal of """
strQuery = strQuery & rfolder & """') Where ""DAV:isfolder"" = True and "
strQuery = strQuery & """http://schemas.microsoft.com/mapi/proptag/x6707001E"" =
'" & pfpath & "'</D:sql></D:searchrequest>"
set req = createobject("microsoft.xmlhttp")
req.open "SEARCH", rfolder, false
req.setrequestheader "Content-Type", "text/xml"
req.setRequestHeader "Translate","f"
req.send strQuery
If req.status >= 500 Then
ElseIf req.status = 207 Then
set oResponseDoc = req.responseXML
set oNodeList = oResponseDoc.getElementsByTagName("d:x669B0014")
set oNodeList1 = oResponseDoc.getElementsByTagName("d:x66400003")
set oNodeList2 = oResponseDoc.getElementsByTagName("a:href")
For i = 0 To (oNodeList.length -1)
set oNode = oNodeList.nextNode
set oNode1 = oNodeList1.nextNode
set oNode2 = oNodeList2.nextNode
permurl = oNode2.text
if instr(2,pfpath,"/") then
rfold = left(pfpath,int(lastslash)-1)
else
rfold = ""
end if
wscript.echo permurl
if oNode.Text <> 0 then
retarry(0) = formatnumber(oNode.text / 1024 /1024)
retarry(1) = oNode1.text
else
retarry(0) = oNode.text
retarry(1) = oNode1.text
end if
Next
Else

End If
showdeletails = retarry
end function

 

Thursday, November 03, 2005

Displaying deleted public folder tracking information via script in Exchange 2003 sp2

One of the new features introduced in ex 2003 sp2 is the ability to track public folder deletes. To turn this on you need to turn diagnostic logging on the public store general category to at least medium. For more details on this see the F1 help and look for the topic “Track Public Folder Deletions”. Once this option is turned on, when someone deletes a public folder Exchange then logs an event of type 9682 into the Application log on the server which tells you which folder was deleted and by whom it was deleted. Having this information in the event log is useful but for reporting purposes and proactive management (eg tracking folders that shouldn’t have been deleted) it’s a little impractical. So what I’ve come up with is a script that queries the eventlog on a server for a specified number of days retrieves any public folder delete entries parses all the values out and creates a CSV file with the result. It also outputs the result to the command line

The script itself is very simple it takes two commandline parameters which is the name of the server you want to query and the time period in days you want to query for. Then it does a semi-sync query of the Application log via WMI for any 9682 messages logged within the specified time period. It then parses the folder name, folder id, the mailbox that deleted the folder (displayed as the LegacyDN value) and also the username of the user that deleted the mailbox. The parser works by parsing the log message based on the static elements in the log format. The results are finally echoed to the console and then written to a csv file called pfdeletes.csv.

I’m working on a extended version of this script that uses the information retrieved from the event log to then query the folder dumpster of the parent folder that was deleted to get extra information about the folder such as how big it was, how many items where in the folder, who created it and maybe who the folder contacts are.

I’ve put a downloadable copy of the script here the script itself looks like the following to run the script requires two command-line parameters the servername and the number of days to query eg to query the log for the last 7 days it would look like

Cscript showpfdeletes.vbs servername 7


days = wscript.arguments(1)
servername = wscript.arguments(0)
SB = 0
Set fso = CreateObject("Scripting.FileSystemObject")
set wfile = fso.opentextfile("c:\PfDeletes.csv",2,true)
wfile.writeline("DateDeleted,FolderName,FolderID,DeletedBy-MailboxName,DeletedBy-UserName")
dtmStartDate = CDate(Date) - days
dtmStartDate = Year(dtmStartDate) & Right( "00" & Month(dtmStartDate), 2) & Right( "00" & Day(dtmStartDate), 2)
Set objWMIService = GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & servername & "\root\cimv2")
Set colLoggedEvents = objWMIService.ExecQuery("Select Timewritten,Message from Win32_NTLogEvent Where Logfile='Application' and Eventcode = '9682' and TimeWritten >= '" & dtmStartDate & "' ",,48)
wscript.echo "Date Deleted FolderName FolderID Deleter-Mbox Deleter-UserName"
For Each objEvent in colLoggedEvents
SB = 1
Time_Written = cdate(DateSerial(Left(objEvent.TimeWritten, 4), Mid(objEvent.TimeWritten, 5, 2), Mid(objEvent.TimeWritten, 7, 2)) & " " & timeserial(Mid(objEvent.TimeWritten, 9, 2),Mid(objEvent.TimeWritten, 11, 2),Mid(objEvent.TimeWritten,13, 2)))
FolderName = Mid(objEvent.Message,8,instr(objEvent.Message,"with folder")-9)
FolderID = Mid(objEvent.Message,instr(objEvent.Message,"with folder ID")+15,(instr(objEvent.Message,"was deleted by")-(instr(objEvent.Message,"with folder ID")+16)))
MailboxName = Mid(objEvent.Message,instr(objEvent.Message,"was deleted by")+15,instr(objEvent.Message,", user account")-(instr(objEvent.Message,"was deleted by")+15))
UserName = Mid(objEvent.Message,instr(objEvent.Message,", user account")+15,(instr(objEvent.Message,chr(13))-(instr(objEvent.Message,", user account")+16)))
wscript.echo Time_Written & " " & FolderName & " " & FolderID & " " & MailboxName & " " & UserName
wfile.writeline(Time_Written & "," & FolderName & "," & FolderID & "," & MailboxName & "," & UserName)
next
wfile.close
set wfile = nothing
if SB = 0 then wscript.echo "No Public Folder Deletes recorded in the last " & days & " Days"