Thursday, December 22, 2005

Postmaster NDR resubmit Tool

Hot on the heels of last weeks resubmit tool I decided to rework this a little so it could also be used to resubmit messages from an NDR notification that may have been wrongly addressed. If you have configured your SMTP virtual servers to send a copy of Non delivery Recipients to a mailbox you’ll get a copy of all the NDR’s your server produces. Now in a large environment this is probably next to useless because of the number of messages you may receive but in small companies it may be a reasonable thing to do to allow you catch mis-addressed emails. But once you have received a badly addressed email if you try to resubmit that email from the NDR using Outlook (Resend) the sender address will be set to your email address which may confuse replies. So what this tool does it firstly extracts the original message from the NDR (if possible) and then allows you to specify the proper email address you want to submit it to and it will then resubmit the email so when it arrives at the recipients mailbox it will have the correct sender address.

Technically this works the same as the post from last week it uses two ASP pages and Exoledb in the background to query a folder for NDR messages. And then it use CDOEX to extract the original message from the NDR and then it uses the stream interface to submit the message to the pick directory.

Using the tool

The two asp pages that make up this tool use Exoledb as the underlying mechanism to access the Exchange Store. This means that the pages must be run locally on an Exchange server where the postmaster or notifcation mailbox is located. To run the tool it needs to be put into a directory usually under the default website (eg create a directory under wwwroot called ndrresub) that has NTLM authentication enabled (via IIS admin). Because this was a really simple tool I’ve also hard coded the Pickup directory in the ASP file so if you have the pick directory set to any other directory other then “c:\Program Files\Exchsrvr\Mailroot\vsi 1\PickUp” you need to change the following line in expresub.asp file

dim rec,oCon,Href,msgobj,resub,Subject,Toaddress,pickupdirectory,rfcmsg
pickupdirectory = "c:\Program Files\Exchsrvr\Mailroot\vsi 1\PickUp"

This tool is desinged to be used against the postmasters mailbox so you should have the pages located on the server where the postmaster mailbox is and then to access the mailbox type the OWA url to the inbox eg http://servername/exchange/administrator/inbox. The main page of this application is called ndrmain.asp so if you have placed the files in a directory called ndrresub under wwwroot you should be able to access the tool by using http://servername/ndrresub/ndrmain.asp .

Download

I’ve put a donwloadable copy of the 2 asp pages here


The following code segment illustrates the main differences.

im rec,oCon,Href,msgobj,msgobj1,resub,Subject,Toaddress,pickupdirectory,rfcmsg
pickupdirectory = "c:\Program Files\Exchsrvr\Mailroot\vsi 1\PickUp"
Resub = request.form("resub")
Subject = request.form("Subject")
Toaddress = request.form("Toaddress")
set rec = createobject("ADODB.Record")
Set oCon = CreateObject("ADODB.Connection")
Href = request.querystring("Href")
oCon.ConnectionString = Href
oCon.Provider = "ExOledb.Datasource"
oCon.Open
set msgobj = createobject("CDO.Message")
msgobj.datasource.open Href,oCon,3
set objattachments = msgobj.attachments
for each objattachment in objattachments
if objAttachment.ContentMediaType = "message/rfc822" then
set msgobj1 = createobject("cdo.message")
msgobj1.datasource.OpenObject objattachment, "ibodypart"
exit for
end if
next
if Resub = "Yes" then
response.write "Message Resubmitted to :" & Toaddress
msgobj1.fields("urn:schemas:mailheader:subject") = Subject
msgobj1.fields.update
set stm = msgobj1.getstream
stm.type = 2
stm.Charset = "x-ansi"
rfcmsg = stm.readtext
rfcmsg = "x-sender: " & msgobj1.fields("urn:schemas:httpmail:fromemail") & vbcrlf & rfcmsg
rfcmsg = "x-receiver: " & Toaddress & vbcrlf & rfcmsg
stm.position = 0
stm.writetext = rfcmsg
Randomize ' Initialize random-number generator.
rndval = Int((20000000000 * Rnd) + 1)
stm.savetofile pickupdirectory & "\" & day(now) & month(now) & year(now) & hour(now) & minute(now) & rndval & ".eml"
else

Friday, December 16, 2005

Public folder Mail Re-Submit Tool

I came up with this little application this week to solve one annoying problem I have with a small client that uses an anti-spam application that submits any SPAM it finds to a public folder. As with everything the cheaper the anti-spam software the more chance there will be false positives so occasionally there is a need to go though this public folder and forward any false positives on to the intended receiver. Using the normal Outlook or OWA method will change the reply address’s and also the subject line which is usually also altered by the anti spam program. So what I came up with was two simple ASP pages that could be used to firstly query all the mail in a public folder and then allow an export (which is sometimes useful for checking the X-headers added by this spam program) and also resubmit an email to a desired email address (and allow any fix-up to the subject) buy firstly getting the RFC822 stream of the message via CDOEX and then saving this stream with an added x:sender and x:receiver field to the pickup directory on the server.

The Resubmit Trick

I picked up this resubmit trick by basically copying what the IMF and IMF archive managers do. If you have UCE Archiving enabled with IMF it saves a copy of any SPAM it detects to a directory and writes the sender and reciver address’s as x:sender and x:receiver at the top of the file. If these two fields are at the top of the file when you put a mail into the pickup directory Exchange will process these as the envelope fields and only attempt to deliver the message to the x:receiver address (and not the To and CC fields of the message). I started out thinking I could do this by adding the X-header’s via the fields collection in CDOEX like I usually do which works but created a problem that because using the procedure for some reason added another field to the top of the file before x:sender and x:receiver when exchange processed the email that was placed in the pickup directory it would also try and resend the email to any of the address’s that where in the To and CC field of the message (which is a really bad thing).To fix this problem I ended up getting the stream of the message appending to the front of the steam and then writing it to disk which made sure that these two fields where always at the top of the file

Using the tool

The two asp pages that make up this tool use Exoledb as the underlying mechanism to access the Exchange Store. This means that the pages must be run locally on an Exchange server that has an instance of the public folder you want to access. To run the tool it needs to be put into a directory usually under the default website (eg create a directory under wwwroot called resub) that has NTLM authentication enabled (via IIS admin). Because this was a really simple tool I’ve also hard coded the Pickup directory in the ASP file so if you have the pick directory set to any other directory other then “c:\Program Files\Exchsrvr\Mailroot\vsi 1\PickUp” you need to change the following line in expresub.asp file

dim rec,oCon,Href,msgobj,resub,Subject,Toaddress,pickupdirectory,rfcmsg
pickupdirectory = "c:\Program Files\Exchsrvr\Mailroot\vsi 1\PickUp"

This tool is not just limited to public folders if you have a mailbox you want to re-submit messages from it will also work just enter in the OWA url to the inbox you want (eg: http://servername/exchange/username/inbox ) and press submit. The main page of this application is called resubmain.asp so if you have placed the files in a directory called resub under wwwroot you should be able to access the tool by using http://servername/resub/resubmain.asp .

Limitations

One limitation is that you can only submit a message to one email address and you should only resubmit messages to internal address resubmitting to external address may cause unexpected results.

Changing Export format

Currently the export format is set to .txt files this is mainly because I wanted to be able to view the message in notepad. If you wanted to export the message as a .eml just change the extension in the following line in resubmail.asp and you will then be able to open the message in Mail client such as Outlook Express

Response.AddHeader "Content-Disposition","attachment;filename=export.txt"

Change to

Response.AddHeader "Content-Disposition","attachment;filename=export.eml"

Download

I’ve put a download of the ASP pages here

Sunday, December 11, 2005

Mind the Overlap –2006 Commonwealth Games timezone patch for Daylight Saving and how it affects Exchange and Outlook

This is one for the Aussie’s and anyone who is managing a server on the East coast of Australia. Because of the Commonwealth games next year the date daylight saving starts on is being brought forward 1 week. If anyone remembers back a similar thing was done for the Olympics in 2000. First thing first is what is the overlap period? this is the period between when the new daylight saving starts and the period where it usually starts. This is between the dates 26/03/2006 and 2/04/2006. The long and short of it is that if your users currently have appointments created within this period including recurring appointments (no patches applied) these appointments are going to be out by 1 hour during this overlap period. Microsoft has released a patch that adds a new timezone to the registry but this patch doesn’t retrospectively fix appointments that already exist only those created after the patch has been applied. Because Timezone information isn’t year specific you now run into the problem if your users create appointment in the overlap period in 2007 with the patch applied.

The current KB and Advice from Microsoft can be found at

http://www.microsoft.com/australia/timezone/2005.aspx
http://support.microsoft.com/kb/907494/
http://support.microsoft.com/kb/909915

Here’s my take on this which is absolutely unofficial based on my testing and knowledge, I strongly encourage you to do your own testing to test for yourself the affects of these patches and setting, Also any of the scripts mentioned in this post have had very little real testing. Because I’m a Sydney sider this affects me directly I’ll try to keep this page up to date with any corrections as I learn more.
Depending on your environment you have four areas you may need to worry about with this patch. Desktop, OWA, locally on Exchange Server and Handhelds. Additionally if your using WebDAV or CDOEX to do any appointment creation/modifications you need to apply a hotfix and check your code especially if you’re using hard coded CDO timezone enums.

Desktops- Getting all the desktops your users may be using to access Outlook and OWA updated with this patch (and then removing) it after provides the greatest logistical challenge. The desktop patch affects both Outlook and OWA.
How does the Desktop Patch effect OWA – This is an interesting one because you would think that OWA would only really be affected by the patches on the server well no. When a user tries to create an appointment in Exchange via OWA the browser uses a client side control that pulls information from the time zone on the local machine and the date and the time that is posted to the server is actually the UTC time for the appointment based on the time zone offset from the client. So this means although you may have everything in your company patched if a user walked up to a kiosk machine that isn’t patched and creates an appointment via OWA during the overlap period this appointment would be created 1 hour out no matter what the time zone settings where in OWA. The server-side patch’s for OWA is still equally important however because OWA still does a lot of server side actions that are going to require the right Timezone data. These include free-busy time calculation and also expansion of reoccurring appointments. One example of this is if you have patched your desktops and patched your servers and even applied the hot fix for CDO and OWA if you haven’t updated the timezone for the user in OWA options when OWA goes to expand a reoccurring appointment that has an occurrence in the overlap period it will expand 1 hour out. So to get OWA working properly in the Overlap period with Exchange 2003

1. Update any Desktops that are going to be used to connect to Outlook or OWA
2 Update the Server with the new timezone and enable
3. Apply the hotfix from http://support.microsoft.com/default.aspx?scid=kb;en-us;909933&sd=rss&spid=1773 . Note if you don’t apply the hotfix and try changing the Timezone in OWA my experience was that OWA would no longer create appointments at all so this hotfix is import.
4. Adjust the timezone in OWA – Options

Doing a Bulk Update of OWA Timezone Options
Once you have updated the timezone on the server and applied the hotfix another task that must be done is to update the user’s OWA options. The first time a user connect to OWA its sets the http://schemas.microsoft.com/exchange/timezone property. This is the timezone you see if you go into options in OWA. If you don’t want to rely on your users going in and manually changing this setting you might want to look at a script to do a bulk update of this property. The timezone property itself lives on the root mailbox folder I’ve come up with a ADSI / WebDAV script that will connect to every mailbox on a server and update the timezone setting if it is currently set to one of the 3 affected timezones. This script is written for 2003 so I’ve used the email address in the WebDAV connection string which I find the most reliable way for 2003. If you want to run the script on 2000 you could try using the mailnickname instead. This script takes the name of the server you want to run it against as a commandline parameter and then does a ADSI query to find all the users on that server (not hidden from the addresslist) and then constructs a URL to connect to each user's mailbox and does a propfind on the http://schemas.microsoft.com/exchange/timezone property to determine what the current timezone is and then does a proppatch with the new value if needed. The script needs to be run with a user that has full rights to all mailboxes on a server see http://support.microsoft.com/default.aspx?scid=kb;en-us;262054 for more details. I’ve included the script in the download for this post here

CDOEX /Exoledb and WebDAV

Because CDO maintains an internal table to track time zone information you need a hotfix (for 2003 see) to make sure that any of your CDOEX applications will handle date and times correctly during the overlap period. The CdoTimeZoneId Enum id is stored with any appointments created via OWA and CDO and used by CDO to render the time correctly. What that hotfix does is creates some new CdoTimeZoneId Enum values (and supporting code) to cater for the overlap period. For example the CdoTimeZoneId Enum for Sydney normally is 57 the CdoTimeZoneId Enum the hotfix introduces for Sydney during this period is 78. Again having the correct CdoTimeZoneid Enum set is going to be important if you do an expansion query on a reoccurring appointment with Exoledb or when you go to display an appointment use CDOEX during the overlap period.

Auditing currently affected Appointments

What about appointments that have already been created without that patch being applied in the overlap period. Currently Microsoft recommends that you do an export and import for any affected appointments. Before you do this you might want to identify which users are affected eg those that have appointments created in the Overlap period. For this some Exoldb or WebDAV code can come in handy. I’ve wrote a script that takes the servername or the server you want to analyze as a command-line parameter then uses Exoledb to query that server for any appointments in the overlap period. Because this script uses Exoledb it must be run locally on a Exchange server and must be run with an account that has rights to the users calendar see kb . This script produces a CSV file that lists all the users that have appointments in the overlap period and the details of those appointments. To use the script there is one change you must make before using it which is you must add your primary SMTP domain name in the domainname variable in the script this is used for the Exoledb Connection string. I’ve included a copy of the script in the download for this post here.

Can you identify if an appointment has been created with the patch? Yes and no if the appointment is created with Outlook you can check the following Mapi property http://schemas.microsoft.com/mapi/id/{00062002-0000-0000-C000-000000000046}/0x8234 to see what timezone the appointment was created with. If the appointment was created with OWA then it’s a little more difficult. If the timezone patch and hotfix has been applied to the server and the OWA timezone is set then you can use the urn:schemas:calendar:timezoneid property. For example if your using the new Sydney 2006 timezone when the patch is applied the timezoneid property should be set to 78. The problem is if you have the desktop patch applied but the server unpatched an appointment may be created okay in the overlap period but the CdoTimeZoneId will still be set to 57. Also expanded reoccurring appointments may show nothing depending on the method that has been use to expand them.

Can you update the appointment in the overlap period programmatically? I haven’t quite got to that yet as I’m still in the detection and update period. But in theory it should be possible you’d want to make sure that you where only going to affect appointments created in the overlap period. For example if you wanted to adjust reoccurring appointments that may have been expanded wrong in the overlap period you would need to update the timezone information and them make a change that would cause the appointment to be re-expanded.

Friday, December 02, 2005

Auditing Send-as and Receive As rights via script

A while ago I wrote this script to audit the Mailbox DACL in a reverse fashion so instead of the normal way where you would see these people have rights to this mailbox it did this person has rights to the following mailbox’s. The one thing this script didn’t check was for Send-as and receive-as rights. These particular rights are extended rights there a good description of what an extended right is here. The bit that is needed for this script is (quoted from that article)

“Extended rights are not defined by an access mask. Instead, each extended right is identified by a globally unique identifier (GUID). This GUID corresponds to a controlAccessRight object that is stored in the Extended-Rights container within a forest's Configuration container. An ACE that grants an extended right specifies a GUID corresponding to a particular controlAccessRight object.”

The ACE’s themselves that pertain to send-as and receive-as are added to the Active Directory User object Security descriptor and not the mailbox’s security descriptor (msExchMailboxSecurityDescriptor).
So adapting the script that is used before was pretty easy it’s a matter of changing it from checking the mailbox security descriptor via CDOEXM to just using ADSI to check the user’s security descriptor. Go though all the ACE’s on the DACL of that user and look for any GUID’s that match the send-as and receive as rights see this for a list of all the extended right’s GUIDS. Look for allows which have an AceFlags enum of 5 (6 is for deny) then using the same data-shaping logic I used previously display the result to the commandline.
I’ve put a download of this script here the script itself looks like

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=*) (|
(&(objectCategory=person)(objectClass=user)(|(homeMDB=*)(msExchHomeServerName=*)))

))));samaccountname,displayname,distinguishedName;subtree"
Com.ActiveConnection = Conn
Com.CommandText = Query
Com.Properties("Page Size") = 1000
set objParentRS = createobject("adodb.recordset")
set objChildRS = createobject("adodb.recordset")
strSQL = "SHAPE APPEND" & _
" NEW adVarChar(255) AS UOADDisplayName, " & _
" NEW adVarChar(255) AS UOADTrusteeName, " & _
" ((SHAPE APPEND " & _
" NEW adVarChar(255) AS MRmbox, " & _
" NEW adVarChar(255) AS MRTrusteeName, " & _
" NEW adVarChar(255) AS MRRights, " & _
" NEW adVarChar(255) AS MRAceflags) " & _
" RELATE UOADTrusteeName TO MRTrusteeName) AS rsUOMR"
objParentRS.LockType = 3
objParentRS.Open strSQL, conn1

Set Rs = Com.Execute
While Not Rs.EOF
dn = "LDAP://" & replace(rs.Fields("distinguishedName").Value,"/","\/")
set objuser = getobject(dn)
Set oSecurityDescriptor = objuser.Get("ntSecurityDescriptor")
Set dacl = oSecurityDescriptor.DiscretionaryAcl
Set ace = CreateObject("AccessControlEntry")
objParentRS.addnew
objParentRS("UOADDisplayName") = rs.fields("displayname")
objParentRS("UOADTrusteeName") = strdname & "\" & rs.fields("samaccountname")
objParentRS.update
Set objChildRS = objParentRS("rsUOMR").Value
For Each ace In dacl
if lcase(ace.ObjectType) = "{ab721a54-1e2f-11d0-9819-00aa0040529b}" and
ace.AceType = 5 then
if ace.Trustee <> "NT AUTHORITY\SELF" and ace.AceFlags <> 6 then
objChildRS.addnew
objChildRS("MRmbox") = rs.fields("displayname")
objChildRS("MRTrusteeName") = ace.Trustee
objChildRS("MRRights") = "Send As"
objChildRS("MRAceflags") = ace.AceFlags
objChildRS.update
end if
end if
if lcase(ace.ObjectType) = "{ab721a56-1e2f-11d0-9819-00aa0040529b}" and
ace.AceType = 5 then
if ace.Trustee <> "NT AUTHORITY\SELF" and ace.AceFlags <> 6 then
objChildRS.addnew
objChildRS("MRmbox") = rs.fields("displayname")
objChildRS("MRTrusteeName") = ace.Trustee
objChildRS("MRRights") = "Recieve As"
objChildRS("MRAceflags") = ace.AceFlags
objChildRS.update
end if
end if
Next
rs.movenext
Wend
wscript.echo "Number of Mailboxes Checked " & objParentRS.recordcount
Wscript.echo
objParentRS.MoveFirst
Do While Not objParentRS.EOF
Set objChildRS = objParentRS("rsUOMR").Value
crec = 0
if objChildRS.recordcount <> 0 then wscript.echo objParentRS("UOADDisplayName")
Do While Not objChildRS.EOF
wscript.echo " " & objChildRS.fields("MRmbox")
wscript.echo " -" & objChildRS.fields("MRRights")
objChildRS.movenext
loop
objParentRS.MoveNext
loop

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"

Wednesday, October 26, 2005

Reporting on SMTP Protocol Log Settings and Log Directories on all Exchange Server in a Domain

Apart from message tracking logs one of the most useful things you can have when trying to track down message transfer problems are SMTP protocol logs. This is one thing I usually always enable on any Exchange server I’m managing. One of the things that is missing is a way in which you can manage these files over a long period of time if you leave logging enabled (something like the badmail detection and archiving would be nice actually most of the work is already done its just a matter of adaptation).

So what I’ve decided to do was put together a script that would first enumerate all the SMTP virtual servers on all Exchange servers within a domain and then report on if logging was enabled, what type of logging is being used, how many logfiles are in the logging directory, how much space the log files are taking up and what the oldest log file in the directory is. Because the log settings for a SMTP VS are stored in the metabase the next part of the script uses the ADSI IIS provider to connect to the metabase on the Exchange server and then retrieve information about logging for that Virtual server. To determine first if logging is enabled the Logtype property is checked if the value is 1 then logging has been enabled on the VS if its 0 it hasn’t. The next value that is checked is the logpluginclsid property this property will tell you what type of logging is being used via which clsid is stored in the property. With Exchange this will be one of four types W3C , NCSA, IIS or ODBC. If its ODBC logging then there will be no log files so the script will drop out here. If a log file format is being used the next property that is checked is the logfiledirectory property. By default if you turn on logging and don’t configure the logging directory this property will be null under the VS node and the Logfile directory from the parent node will be used. The next part of the script formats the log file directory into a format that can be used in the rest of the script. I’ve created two versions of this script the first is a fso version which uses the WSH File Scripting Object to connect to the administrative shares on the Exchange Server and then access the logfile directory. The second version is a WMI version which uses WMI to query the CIM_DATAFILE class this will mean the script will still work even if the administrative shares have been disabled for security reasons. The rest of the script then connects to the logging directory via FSO or WMI and then counts the number of log files in the directory and works out how much space they are using and what the oldest file in the directory is. The result is then output to the commandline and also they are written to a CSV file on the c:\ called STMPVSSetting.csv

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

lfcount = 0
lfsize = 0
lfoldatenum = ""
Set fso = CreateObject("Scripting.FileSystemObject")
set wfile = fso.opentextfile("c:\SMTPVSSettings.csv",2,true)
wfile.writeline("Servername,Virtual Server Name,Logging Type,Log File Dir,Number
of LogFiles,Space Used(MB),Oldest Log File")
set conn = createobject("ADODB.Connection")
set com = createobject("ADODB.Command")
Set iAdRootDSE = GetObject("LDAP://RootDSE")
strNameingContext = iAdRootDSE.Get("configurationNamingContext")
strDefNamingContext = iAdRootDSE.Get("defaultNamingContext")
Conn.Provider = "ADsDSOObject"
Conn.Open "ADs Provider"
Com.ActiveConnection = Conn
Com.ActiveConnection = Conn
Wscript.echo
Wscript.echo "SMTP Virtual Servers Logfile Setting and Resources"
vsQuery = "<LDAP://" & strNameingContext & ">;(objectCategory=protocolCfgSMTPServer);name,distinguishedName;subtree"
Com.ActiveConnection = Conn
Com.CommandText = vsQuery
Set Rs = Com.Execute
While Not Rs.EOF
strstmsrv = "LDAP://" & rs.fields("distinguishedName")
set svsSmtpserver = getobject(strstmsrv)
wscript.echo
wscript.echo "ServerName:" &
mid(svsSmtpserver.distinguishedName,instr(svsSmtpserver.distinguishedName,"CN=Protocols,")+16,instr(svsSmtpserver.distinguishedName,",CN=Servers")-(instr(svsSmtpserver.distinguishedName,"CN=Protocols,")+16))
call
getSTMPstatus(mid(svsSmtpserver.distinguishedName,instr(svsSmtpserver.distinguishedName,"CN=Protocols,")+16,instr(svsSmtpserver.distinguishedName,",CN=Servers")-(instr(svsSmtpserver.distinguishedName,"CN=Protocols,")+16)),svsSmtpserver.adminDisplayName)
rs.movenext
wend
wfile.close

sub getSTMPstatus(servername,vsname)
Set SMTPVSS = GetObject("IIS://" & Servername & "/SMTPSVC")
for each SMTPVS in SMTPVSS
if SMTPVS.KeyType = "IIsSmtpServer" then
if SMTPVS.ServerComment = vsname then
wscript.echo "SMTP Server : " & SMTPVS.ServerComment
if SMTPVS.logtype = 0 then
wscript.echo "Logging not enabled"
wfile.writeline(servername & "," & SMTPVS.ServerComment & "," & "Logging not
enabled,,,,")
else
select case SMTPVS.logpluginclsid
case "{FF160663-DE82-11CF-BC0A-00AA006111E0}" Wscript.echo "Logging Type : W3C
Extended Log File Format"
lfiles = 1
ltype = "W3C Extended Log File Format"
case "{FF16065B-DE82-11CF-BC0A-00AA006111E0}" Wscript.echo "Logging Type : ODBC
Logging"
wfile.writeline(servername & "," & SMTPVS.ServerComment & "," & "ODBC
Logging,,,,")
case "{FF16065F-DE82-11CF-BC0A-00AA006111E0}" Wscript.echo "Logging Type : NCSA
Log File Format"
lfiles = 1
ltype = "NCSA Log File Format"
case "{FF160657-DE82-11CF-BC0A-00AA006111E0}" Wscript.echo "Logging Type :
Microsoft IIS Log File Format"
lfiles = 1
ltype = "Microsoft IIS Log File Format"
end select
if lfiles = 1 then
if isnull(SMTPVS.logfiledirectory) then
wscript.echo "Log File Directory : " & SMTPVSS.logfiledirectory & "SMTPSVC" &
SMTPVS.name
lfiledir = SMTPVSS.logfiledirectory & "SMTPSVC" & SMTPVS.name
logfileunc = "\" &
mid(SMTPVSS.logfiledirectory,3,len(SMTPVSS.logfiledirectory)-2) & "SMTPSVC" &
SMTPVS.name
else
if mid(SMTPVS.logfiledirectory,len(SMTPVS.logfiledirectory),1) = "\" then
lfiledir = SMTPVS.logfiledirectory & "SMTPSVC" & SMTPVS.name
wscript.echo "Log File Directory : " & SMTPVS.logfiledirectory & "SMTPSVC" &
SMTPVS.name
logfileunc = "\" & mid(SMTPVS.logfiledirectory,3,len(SMTPVS.logfiledirectory)-2)
& "SMTPSVC" & SMTPVS.name
else
lfiledir = SMTPVS.logfiledirectory & "\" & "SMTPSVC" & SMTPVS.name
wscript.echo "Log File Directory : " & SMTPVS.logfiledirectory & "\" & "SMTPSVC"
& SMTPVS.name
logfileunc = "\" & mid(SMTPVS.logfiledirectory,3,len(SMTPVS.logfiledirectory)-2)
& "\" & "SMTPSVC" & SMTPVS.name
end if
end if
drive = left(SMTPVS.logfiledirectory,1) & ":"
lfilepath = replace(logfileunc,"\","\\")
lfilepath = right(lfilepath,(len(lfilepath)-2)) & "\\"
Set objWMIService = GetObject("winmgmts:" _
& "{impersonationLevel=impersonate}!\\" & servername & "\root\cimv2")
Set lfiles = objWMIService.ExecQuery _
("select * from CIM_DataFile where path = """ & lfilepath & """ and extension =
""log"" and drive = """ & drive & """")
for each lfile in lfiles
lfcount = lfcount + 1
lfsize = lfsize + lfile.filesize
if lfcount = 1 then lfolddatenum = cdate(DateSerial(Left(lfile.lastmodified, 4),
Mid(lfile.lastmodified, 5, 2), Mid(lfile.lastmodified, 7, 2)) & " " &
timeserial(Mid(lfile.lastmodified, 9, 2),Mid(lfile.lastmodified, 11,
2),Mid(lfile.lastmodified,13, 2)))
if lfolddatenum > lfile.LastModified then
lfolddatenum = cdate(DateSerial(Left(lfile.lastmodified, 4),
Mid(lfile.lastmodified, 5, 2), Mid(lfile.lastmodified, 7, 2)) & " " &
timeserial(Mid(lfile.lastmodified, 9, 2),Mid(lfile.lastmodified, 11,
2),Mid(lfile.lastmodified,13, 2)))
end if
next
wscript.echo "Number of Log files in Directory : " & lfcount
wscript.echo "Disk Space being used : " & formatnumber(lfsize/1048576,2,0,0,0) &
" MB"
wscript.echo "Oldest Log file in this Directory : " & lfolddatenum
wfile.writeline(servername & "," & SMTPVS.ServerComment & "," & ltype & "," &
lfiledir & "," & lfcount & "," & formatnumber(lfsize/1048576,2,0,0,0) & "," &
lfolddatenum)
lfcount = 0
lfsize = 0
lfolddatenum = ""
ltype = ""
lfiledir = ""

end if
lfiles = 0
end if
end if
end if
next

end sub
 

Thursday, October 20, 2005

Parsing SMTP Protocol log files with Monad

Now that SP2 is out in the wild and I’m starting to look more at what’s happening with SenderID (by the way if you haven't seen this already there is a great post on SenderID on the Exchange Team Blog). I wanted a way I could aggregate the information that is stored in the SMTP protocol logs so I could see for each domain that is sending me email what are the IP address’s of the mail servers and how many emails do have i recieved from each IP (and do this for a time period say the last 1-2 hours). I’ve had the beta of Monad which is the next version of the Windows Shell that will be in Vista (maybe) and E12 (downloadable for here) installed on my machine for a while and this seemed like a good task to take it out for a test drive. The main advantage of Monad from my point of view is being able to get access to all the objects in the .NET framework so this means you can finally get access to hashtables in your scripts (Perl users have had this for years). Hashtables are very versatile objects to use in scripts and are perfect for the sort of aggregation I wanted to do. Adam Barr has posted a very good example of using nested hash-tables that helped a lot with working out how to get this to work.

The script is relatively simple it takes two command-line parameters the first is the directory where the logs file are (can be network drive although be careful if you have really large log files) and the second parameter is the number of hours you want to look back. So the first couple of lines deal with inputting the parameters and next couple looks in the directory for any files that where modified within the time period imputed. The next part of the script does a line by line parse of the log file, the split method is used to break the log file into an array so each element can be processed as needed. Because I’m only interested in Inbound traffic there is an if statement to drop any outbound connections. And because I’m only interested in the “FROM” lines in the log file there’s some further if statements and also finally some code that does a time comparison so only the events within the inputted time period are processed. Because the time used in the log files is in UTC there’s some code that does the UTC conversion (this is a really cool compared to how you do this in VBS). The next part of script basically handles aggregating the domains into one hashtable and then creating a nested hashtable table to handle storing each of the IP address’s that are sending for that domain using a key derived from the IP-address’s and Domain name it also counts the number of email sent from each IP address. The last part to the script handles going back though the hashtable and displaying the data in a hierarchal format.

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

param([String] $LogDirectory = $(throw "Please specify path for a Log for Directory"),
[int32] $timerange = $(throw "Please specify a Time Range in Hours"))
$reqhash1 = @{ }
$Di = New-Object System.IO.DirectoryInfo $LogDirectory
foreach($fs in $Di.GetFileSystemInfos()){
if ($fs.LastWriteTime -gt [DateTime]::get_Now().AddHours(-$timerange) ){
foreach ($line in $(Get-Content $fs.Fullname)){
if ($line.Substring(0,1) -ne "#"){
$larry = $line.split(" ")
if ($larry[3] -ne "OutboundConnectionCommand"){
if ($larry[8] -eq "MAIL"){
$ltime = [System.Convert]::ToDateTime($larry[0] + " " + $larry[1])
if($ltime -gt [DateTime]::get_UtcNow().addhours(-$timerange)){
$femail = $larry[10].Substring($larry[10].IndexOf("<")+1,$larry[10].IndexOf(">")-$larry[10].IndexOf("<")-1)
$fdomain = $femail.Remove(0, $femail.IndexOf("@")+1)
if($reqhash1.ContainsKey($fdomain)){
$hashtabedit = $reqhash1[$fdomain]
if($hashtabedit.ContainsKey($larry[2] + "/" + $fdomain)){
$hashtabedit[$larry[2] + "/" + $fdomain] = $hashtabedit[$larry[2] + "/" + $fdomain] + 1
}
else{
$hashtabedit.Add($larry[2] + "/" + $fdomain,1)
}
}
else{
$reqhash2 = @{ }
$reqhash2.Add($larry[2] + "/" + $fdomain,1)
$reqhash1.Add($fdomain,$reqhash2)
}
}
}
}
}
}
}
}
foreach ($htent in $reqhash1.keys){
$htent
$reqhash2 = $reqhash1[$htent]
foreach ($htent1 in $reqhash2.keys){
" " + $htent1.Substring(0,$htent1.IndexOf("/")) + " " + $reqhash2[$htent1]
}
}

Mail Enabling a Public folder via MAPI and WebDAV

I’ve had a few questions from people recently asking about the different methods you can mail enable a folder when your running Exchange 2003 in Native Mode. The standard method for mail enabling objects programmatically is to use CDOEXM and the IRecipient Interface . To mail enable a public folder you need to combine this with a little CDOEX like this

FolderURL = "http://server/public/folder/"
set objFolder1 = createobject("CDO.Folder")
objFolder1.DataSource.Open FolderURL, , 3,-1
Set objRecip1 =
objFolder1.Getinterface("IMailRecipient")
objRecip1.MailEnable
objFolder1.DataSource.Save

Now because this uses CDOEX which runs over Exoledb it will only work locally on an Exchange server. To do this remotely you can use the Exchange_PublicFolder WMI class I posted a sample of doing the here previously.

I was curious at the way Exchange System Manger went about mail enabling folders when you where using this on a remote machine as this obviously wasn’t using CDOEX. I did some network captures on a remote machine and the only conversation that the remote machine was having with any server when mailenabling a folder was that it was doing a proppatch on two properties on the public folder via webdav. There was no discernable LDAP traffic at all. I decided to give that go in script myself on a non enabled public folder to see if this would mail enable it and it seemed to work just fine and the RUS seemed to be happy enough to populate the email address’s and also deal with the directory object in Active Directory. I tried the same thing with a CDO script that created a new public folder in the root and mail enabled it via setting these two mapi properties and it seemed to work equally as well.

Now wether this method would be a supported way to mail enable public folders I don’t know it does seem to be how ESM is doing it. There’s also a little more discussion in a recent post on the Exchange Team blog and also in the following KB .

The two properties in question are

PR_PUBLISH_IN_ADDRESS_BOOK = &H3FE6000B
PR_PF_PROXY_REQUIRED = &H671F000B

I’ve only tested this on Exchange 2003 in Native mode as I said this is probably unsupported but the WebDAV to do this would look like

server = "server"
folderpath = "folder"
sDestinationURL = "http://" & server & "/public/" & folderpath & "/"
xmlstr = "<?xml version=""1.0"" ?><a:propertyupdate xmlns:a=""DAV:"" xmlns:b=""urn:schemas-microsoft-com:datatypes""
" _
& "xmlns:c=""xml:"" xmlns:d=""http://schemas.microsoft.com/mapi/proptag/"" " _
& "xmlns:e=""http://schemas.microsoft.com/exchange/events/"" xmlns:f=""http://schemas.microsoft.com/exchange/"">
" _
& "<a:set><a:prop>" _
& "<d:0x3FE6000B b:dt=""boolean"">1</d:0x3FE6000B>" _
& "<d:0x671F000B b:dt=""boolean"">1</d:0x671F000B>" _
& "</a:prop></a:set></a:propertyupdate>"
Set XMLreq = CreateObject("Microsoft.xmlhttp")
XMLreq.open "PROPPATCH", sDestinationURL, False
XMLreq.setRequestHeader "Content-Type", "text/xml;"
XMLreq.setRequestHeader "Translate", "f"
XMLreq.setRequestHeader "Content-Length:", Len(xmlstr)
XMLreq.send(xmlstr)
If (XMLreq.Status >= 200 And XMLreq.Status < 300) Then
Wscript.echo "Success! " & "Results = " & XMLreq.Status & ": " &
XMLreq.statusText
ElseIf XMLreq.Status = 401 then
Wscript.echo "You don't have permission to do the job! Please check your
permissions on this item."
Else
Wscript.echo "Request Failed. Results = " & XMLreq.Status & ": " &
XMLreq.statusText
End If


A CDO example would look like.

MailServer = "servername"
Mailbox = "mailbox"
PR_PUBLISH_IN_ADDRESS_BOOK = &H3FE6000B
PR_PF_PROXY_REQUIRED = &H671F000B
set objSession = CreateObject("MAPI.Session")
strProfile = MailServer & vbLf & Mailbox
objSession.Logon "",,, False,, True, strProfile
Set objInfoStores = objSession.InfoStores
set objInfoStore = objSession.GetInfoStore
Set objpubstore = objSession.InfoStores("Public Folders")
Set objTopFolder = objSession.GetFolder(objpubstore.Fields(&H66310102),objpubstore.ID)
Set objFolder = objTopFolder.Folders.Add("YellowCab1")
Set objFields = objFolder.Fields
objfields.add PR_PUBLISH_IN_ADDRESS_BOOK, true
objfields.add PR_PF_PROXY_REQUIRED, true
objFolder.Update

Thursday, October 13, 2005

Reporting on forwarding rules in Mailboxes and Public Folders via a script

Reporting on rules in mailboxes and public folders can get a little challenging for the humble sysadmin. Because most the time the setting of these rules is out of your control knowing where some rules are forwarding can be a little scary (and in some cases can be against company policy or breach privacy laws). There are a few ways to manipulate rules one of the most often used is the rule.dll which can be used to create and enumerate rules while this is useful it still only offers you the hex value of the binary action property which contains the list of the recipient addresses for a forwarding rule. An active rule in a mailbox or public folders is a special message in a mailbox of type IPM.Rule.Message. In CDO 1.2 you can access these messages by using the hidden messages collection on a folder. If a rule is a forwarding rule then the email address’s the rule is forwarding to gets stored in the PR_RuleMsgActions (0x65EF0102) Mapi Property on that IPM.Rule.Message. Unfortunately this property is a binary property whose format is undocumented. Reverse engineering a complete decode for this property is a little hard and time consuming so what I did was just concentrate on matching the patterns within the binary property (its kind of like doing a jigsaw in the dark) but armed with my trusty ASCII chart I came up with some formulas that can separate out the text address’s from the rest of the information that is stored in the property and produce some meaning results (well it works for me anyway).

I’ve created two versions of this script the first loops though all the mailboxes on a server and looks at rules in the inbox of each mailbox. The second script loops though all the public folders on a server and reports on any forwarding rules in those public folders. The results are output to the screen and also a CSV file is created on the c: drive with the results.

The flow of the mailbox script is it takes in the name of the server as a command line parameter and then does a query of active directory for all the visible mailboxes on this server. Its then opens all the mailboxes and checks the hidden collection for any IPM.Rule.Message rule messages and then checks the PR_RuleMsgActions property on any rule messages it finds. If any address objects are found these are output to the command line and also stored in an array so they can be written to a CSV file at the end of the iteration. Once all rules have been checked for a mailbox the result is written in to a CSV file and it moves on to the next mailbox. The public folders script works similarly except instead of querying for mailboxes via Active Directory it uses CDO to loop though the folders collection and any subfolder therein.

To run the mailbox script you need to add the name of the server you want it to run against as a command-line parameter eg cscript mbxruleloop.vbs servername .With the public folders script you need to configure the servername and the name of mailbox on the server you want to report against (this is so the script can logon to the server the actually mailbox content isn’t accessed).

I’ve posted a downloadable copy of the script here the Mailbox script looks like

servername = wscript.arguments(0)
PR_HAS_RULES = &H663A000B
PR_URL_NAME = &H6707001E
PR_CREATOR = &H3FF8001E
Set fso = CreateObject("Scripting.FileSystemObject")
set wfile = fso.opentextfile("c:\mbxforwardingRules.csv",2,true)
wfile.writeline("Mailbox,FolderPath,Creator,AdressObject,SMTPForwdingAddress")
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 & "));cn,name,legacyExchangeDN;subtree"
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 & ";distinguishedName,mailnickname;subtree"
com.Properties("Page Size") = 100
Com.CommandText = strQuery
Set Rs1 = Com.Execute
while not Rs1.eof
call procmailboxes(servername,rs1.fields("mailnickname"))
wscript.echo rs1.fields("mailnickname")
rs1.movenext
wend
rs.movenext
wend
rs.close
wfile.close
set fso = nothing
set conn = nothing
set com = nothing
wscript.echo "Done"


Sub procmailboxes(servername,mailboxname)

set objSession = CreateObject("MAPI.Session")
objSession.Logon "","",false,true,true,true,servername & vbLF & mailboxname
Set objInfoStores = objSession.InfoStores
set objInfoStore = objSession.GetInfoStore
set Inbox = objSession.Inbox
if inbox.fields.item(PR_HAS_RULES) = true then
Set objMessages = inbox.HiddenMessages
for Each objMessage in objMessages
if objMessage.type = "IPM.Rule.Message" then
call procrule(objMessage,mailboxname,inbox.fields.item(PR_URL_NAME).value)
end if
next
end if

end sub

sub procrule(objmessage,MailboxName,folderpath)
frule = false
splitarry = split(hextotext(objmessage.fields.item(&H65EF0102)),chr(132),-1,1)
if ubound(splitarry) <> 0 then
wscript.echo
wscript.echo "Mailbox Name :" & MailboxName
wscript.echo "Folder Path :" & folderpath
wscript.echo "Rule Created By : " & objmessage.fields.item(PR_CREATOR).value
mbname = MailboxName
fpath = folderpath
creator = objmessage.fields.item(PR_CREATOR).value
frule = true
end if
tfirst = 0
addcount = 1
for i = 0 to ubound(splitarry)
addrrsplit = split(splitarry(i),chr(176),-1,1)
for j = 0 to ubound(addrrsplit)
addrcontsep = chr(3) & "0"
if instr(addrrsplit(j),addrcontsep) then
if tfirst = 1 then addcount = addcount + 1
wscript.echo
wscript.echo "Address Object :" & addcount
redim Preserve resarray(1,1,1,1,1,addcount)
resarray(1,0,0,0,0,addcount) = mbname
resarray(1,1,0,0,0,addcount) = fpath
resarray(1,1,1,0,0,addcount) = creator
if instr(addrrsplit(j),"0/o=") then
resarray(1,1,1,1,0,addcount) = mid(addrrsplit(j),(instr(addrrsplit(j),"0/o=")+1),len(addrrsplit(j)))
WScript.echo "ExchangeDN :" & mid(addrrsplit(j), (instr(addrrsplit(j),"0/o=")+1),len(addrrsplit(j)))
else
WScript.echo "Address :" & mid(addrrsplit(j),3, len(addrrsplit(j)))
resarray(1,1,1,1,0,addcount) = mid(addrrsplit(j),3,len(addrrsplit(j)))
end if
tfirst = 1
end if
smtpsep = Chr(254) & "9"
if instr(addrrsplit(j),smtpsep) then
slen = instr(addrrsplit(j),smtpsep) + 2
elen = instr(addrrsplit(j),chr(3))
Wscript.echo "SMTP Forwarding Address : " & mid(addrrsplit(j),slen,(elen-slen))
resarray(1,1,1,1,1,addcount) = mid(addrrsplit(j),slen,(elen-slen))
end if
next
next
if frule = true then
for r = 1 to ubound(resarray,6)
wfile.writeline(resarray(1,0,0,0,0,r) & "," & resarray(1,1,0,0,0,r) & "," &
resarray(1,1,1,0,0,r) & "," & resarray(1,1,1,1,0,r) & "," &
resarray(1,1,1,1,1,r))
next
end if

end sub


Function hextotext(binprop)
arrnum = len(binprop)/2
redim aout(arrnum)
slen = 1
for i = 1 to arrnum
if CLng("&H" & mid(binprop,slen,2)) <> 0 then
aOut(i) = chr(CLng("&H" & mid(binprop,slen,2)))
mid(binprop,slen,2)))
end if
slen = slen+2
next
hextotext = join(aOUt,"")
end function