Technical Articles

Here is a collection of Technical Articles I've written over the last few years. To keep this list short, I've started sorting ideas under similar topics. For example, if you are looking for a script to dump AD account info, check under the script to dump AD groups.

If that isn't working, check out the tech cloud on the bottom of the right side-bar. I try to categorize all my articles by content, script type, etc. This should quickly limit your search from the 100+ items, to just what you are looking for.

A Cool Firefox Add-in

I was just introduced to a cool new add-in for Firefox, called GooglePedia. This cool little tool splits Google's search results screen into two windows, the left side containing your search results, the right side containing a Wikipedia screen containing your search string.

If you use Firefox, then take a look, very cool! If you don't, what's wrong? Download Firefox now and get on the bandwagon. The inline spell check of web forms (like blog posts) alone is worth it.

Google Search Gadget

After I got my new PC, the first thing I wanted to do was replace some of the functionality of my dynamic desktop. Primarily this meant a useful Google Search box. My search found 3 gadgets already in play, but to my dismay, they all link to adwords sponsored websites.

So, using Darren's example, I found that gadgets are really just little tiny websites. Hmm, I can code websites. Even really tiny ones.

I would like to present, my basic Google Search Gadget. No encoded text, no adwords, no fancy graphics, or crazy cool search technology. It simply provides a form to start searchs.

Future revisions:

  • Adding a Google logo?
  • Search History?
  • Configuration page
  • Better background image = transparent background!

Build your own website for only pennies a day

Nowadays, it seems almost everyone has their own website. You can shop for your groceries, order a pizza and even buy a home all from the comforts of your arm-chair. So, why don't you have your own website all about you. I do!

What? No technical expertise? Bah. No dedicated connection to the Internet? Bah. I know, you don't have the slightest idea of where to get started. Ah! That I can help with.

Here are my very easy steps to get you running with your own website in just a couple minutes work. (The hard part comes later, when you try keeping the content fresh!)

  1. Start a blog. Go to Wordpress, Blogger or one of the millions of blog sites and create an account. You don't need to be too creative naming it, just something. Take note of the address to link to it for later.
  2. Register a domain name. Ah! the expensive part. (Expensive I mean $6 to $15 a year). Go to one of the multitude of domain name registrars and get yourself a domain name. (Sorry EricWoodford.com is already taken.) I personally use dotdnr as they have a fairly easy site to navigate and they provide a multitude of domains (ever want a .tv domain?), but don't be afraid of sites like GoDaddy.com (and use SCOTT1 to save 10%) if the price is right for you. NOTE:Don't have them create a site for you. You already did that in the last step.
  3. Setup your DNS server. When you registered your domain name, they should have asked for a name server (like NS1.dotdnr.com). These are the server's that will eventually host your (domain) name. Go to zoneedit.com and create yourself a free DNS account. You will need to enter the domain name that you registered and they will provide you two name servers. (almost done!)
  4. Update your name servers. Now that you have your name servers from ZoneEdit, go back to your domain name registrar and enter them in their interface. Now people who look up your domain, where to find you (at ZoneEdit).
  5. (Finally) Redirect your web traffic. So far, when people type in your domain name, it will go nowhere. It may go to a psuedo "Under Contruction" site setup by the domain registrar, but not likely (or very useful). ZoneEdit has a great feature called WebForward. Using this you can silently redirect people to the blog site we setup in step 1. Simply click the 'webforward' link for your new domain, type in the blog address into the destination field and select the cloaked? checkbox. This will hide the actual blog domain name, and only show yours.
  6. Now you just need to wait. Typically domain name registrar's will replicate information every few hours out to the Internet. I'd give it 24 hours (and test it) before broadcasting your new website to all your friends.

So, there's nothing stopping you now. Get creating that new content and let the world know who you are. Leave me a comment here with a link to your site, and if I agree with your content, I'll post it here for others to review. Note: Spammers, I will just delete your comment and not look back.

Chain mail, A Spammers Best Friend!

I just received the latest joke/hoax/chain-mail from a family/friend/co-worker regarding some new issue, scam or sentiment. When this person forwarded along the message, I had to skim through pages upon pages of valid e-mail addresses to get to the actual message, which ended with "forward to all your family and friends".

Thanks to this person, I made $$$ with very little work. How? I extracted out all 200+ (validated and real) e-mail addresses, then forwarded the file off to a website that collects e-mail addresses. (Spammers pay extra for validated e-mail address like these, you know!!)

With a little effort, the friend/family/co-worker could have avoided the pending spam.

  1. Remove all extra mail headers. Open the embedded messages down to the one containing the content you are forwarding out.
  2. Remove all other e-mail addresses Often chain-mail will include all the former recipients,
  3. BCC all recipients. This hides the e-mail addresses of your inteded recipients. You can spoof the TO address using something you know won't work. "Eric@Example.com"

Working together we can help decrease the amount of spam that we all get. On the other hand, keep sending me your messages, and I'll keep making a few bucks in the process.

Computer Slow? How about these?

Now that you've been using that computer for awhile now, it is now slowing down, to a crawl, and you don't know what to do.. I've put together this short article to cover some of the basics that I check for with slow computers.

Computer slowness can often be attributed to too many applications running in memory. When you first start up Windows, it has a series of programs that automatically launch at startup. These programs add functionality to your computer, behind-the-scenes. Most are begnign like your anti-virus solution or functional like Google Desktop, but there are also programs on your computer that you might not want.

Convert and configure Exchange 2007 conference room mailbox

Having recently migrated all of our department to Exchange 2007, we were excited to take advantage of the new conference room functionality! No more Outlook Resource configuration, no more faulty Auto-Accept script! Microsoft has FINALLY programmed in full-fledged resource mailbox functionality into Exchange.

Our dilemna. After migration from Exchange 2003, our resource mailboxes were showing up in the Exchange Management console as LinkedMailbox type.

get-mailbox "resource mailbox" | select RecipientTypeDetails

To convert them to UserMailbox type, Microsoft informed us that we'd need to disconnect the mailbox (from the AD account, then reconnect it. Afterwards, we can run the powershell cmdlet to assign the conference room calendar settings.

Sounds easy enough.

Disable-Mailbox -identity "Resource Mailbox"
Connect-Mailbox -identity "Resource Mailbox" -database (it's db) -user "domain\rsrmbxacct"

Right?? Nope.

  1. To connect a domain account to a mailbox, the account needs to be enabled. We've disabled all our resource accounts.
  2. When you enable an account, it needs to have a valid password based on your security standards.
  3. You have to wait for replication, which may not be immediately.

My following code, will read a straight list of mailbox names, and convert them from LinkedMailbox type mailboxes, to RoomMailbox type, then configure it as a conference room based on the following criteria.


cls
#The following file contains list of mailbox display names, seperated by a carriage return.
$Rooms = Get-Content -path ".\AllConferenceRooms.txt"

#Set this variable to $true for normal conference rooms
#set this variable to $false for director conference rooms
#---------------------------
$NormalCRoom = $True
$Delegates = @("delegate1@example.com","delegate2@example.com")

#If this will be a 'director-type' conference room.
if ($NormalCRoom -eq $False){
$Delegates += "Delegate3@example.com"
}

foreach ($MyMailbox in $Rooms) {
$MyMailbox = $MyMailbox.Trim()

if (($MyMailbox -ne $null) -and ($mymailbox -ne '')){
#Gathering some information
#---------------------------
$Mbx = get-mailbox $MyMailbox
$MyMailbox = $Mbx.DisplayName
"Working on '" + $MyMailbox + "'"
"-------------------------------------"

$mbxUPN = $mbx.UserPrincipalName
$DB = $Mbx | select database| % {Get-MailboxDatabase -Identity $_.database}

if ($Mbx.RecipientTypeDetails -eq "LinkedMailbox") {
"Linked mailbox - converting to UserMailbox"
Disable-mailbox $MyMailbox -erroraction stop -outvariable Result

#Pausing script while waiting for the Disable-Mailbox command to process.
do {
$isDisabled = get-user -identity $mbxUPN
} while ( $isDisabled.recipienttype -eq "UserMailbox")

#Create a new password for the account based on the UPN field.
"Setting account password to :'"+$mbxUPN + "'"
$pwd = new-object Security.SecureString
$mbxUPN.ToCharArray() | foreach { $pwd.AppendChar($_)}
Set-QADUser -Identity $mbxUPN -UserPassword $pwd

"Reconnecting mailbox to domain account"
## found I needed to enble the AD account to get next step to work.
enable-qaduser -identity $mbxUPN
Connect-mailbox -identity $MyMailbox -database $DB -user $mbxUPN
disable-qaduser -identity $mbxUPN

#Pausing script to wait for AD replication
do {
$isDisabled = get-user -identity $mbxUPN
} while ( $isDisabled.recipienttype -ne "UserMailbox")
$Mbx = get-mailbox $MyMailbox
}

#Convert standard mailbox to conference room resource
if ($Mbx.RecipientTypeDetails -eq "UserMailbox") {
"Converting to Conference Room mailbox"
Set-Mailbox -identity $MyMailbox -Type room

do {
$isDisabled = get-user -identity $mbxUPN
} while ( $isDisabled.RecipientTypeDetails -ne "RoomMailbox")
$Mbx = get-mailbox $MyMailbox
}

#configure conference room.
if ($Mbx.RecipientTypeDetails -eq "RoomMailbox") {
if ($NormalCRoom -eq $true) {
"Configuring the conference room to auto accept meeting requests"
Set-MailboxCalendarSettings -Identity $MyMailbox -AddOrganizerToSubject $true -AllBookInPolicy $true -AllowConflicts $false -AllowRecurringMeetings $true -AutomateProcessing AutoAccept -BookingWindowInDays 366 -Confirm -DeleteAttachments $true -DeleteComments $true -DeleteNonCalendarItems $true -DisableReminders $true -EnableResponseDetails $true -EnforceSchedulingHorizon $true -MaximumDurationInMinutes 720 -AddAdditionalResponse $false -OrganizerInfo $true -RemoveForwardedMeetingNotifications $true -RemoveOldMeetingMessages $true -RemovePrivateProperty $true
} else {
"Configuring the conference room to as a 'Director-type' mailbox"

Set-MailboxCalendarSettings -Identity $MyMailbox -AddOrganizerToSubject $true -AllBookInPolicy $false -AllowConflicts $false -AllowRecurringMeetings $true -AutomateProcessing AutoAccept -BookingWindowInDays 366 -Confirm -DeleteAttachments $true -DeleteComments $true -DeleteNonCalendarItems $true -DisableReminders $true -EnableResponseDetails $true -EnforceSchedulingHorizon $true -MaximumDurationInMinutes 720 -AddAdditionalResponse $false -OrganizerInfo $true -RemoveForwardedMeetingNotifications $true -RemoveOldMeetingMessages $true -RemovePrivateProperty $true -AllRequestInPolicy $true -ForwardRequestsToDelegates $true
}

foreach ($Person in $Delegates) {
"Assigning "+$Person + " mailbox rights on this resource"
Add-MailboxPermission -AccessRights FullAccess -Identity $MyMailbox -User $Person
}

"Assiging "+$delegates + " as Delegates to this resource."
Set-MailboxCalendarSettings -Identity $MyMailbox -ResourceDelegates $Delegates
}

"Completed."
" "
" "
}
}

Creating a fancy Outlook signature

Sure creating a signature in Outlook is easy. Open Outlook, type in some information, change a few settings and you'll have a decent looking signature. Now, if you want to include a graphic (like a company logo), or use HTML, you have to get a bit more fancy.

That is why I developed this script. This script makes it rather easy to build a fancy signature using various colors, fonts and font sizes. In addition, I have coded functionality to include the graphic (if it is on an external webserver). To make it easier, I have even coded the script to pull information from Active Directory (thanks to a script I found). Drawback being it doesn't work if it cannot access a local domain controller. I am going to work on that. I'd like to use the script at home..

Dynamic Interesting Desktops

One of my first contract jobs was for a small IT company in the area. I worked with them for about 4 months before a permanent, salaried position came along. (Sorry, I like having someone else pay for the benifits) One of the most memorable things the owner shown me was his background.

Now, he was a very private person, so I don't mean his personal background. I mean the 'desktop' image that he had on his computer. It consisted of virtually hundreds of links to websites and resources that he used regularly. Over the last few years, I have attempted various methods to recreate it. It's a really easy concept, create a webpage, add links to it, and set it as your background.

That brings me to my latest script. First it reads the contents of a special folder, containing several desktop sized images. I have been enjoying the art by this artist. http://www.stevetruett.com/wallpaperpages (These are especially great because he has one specific to my widescreen monitor resolution 1680x1050. ) The script randomly picks one for the background. Next it reads a short CSV file on my local computer containing URLs and titles for various resources I connect to regularly. The script creates links to these resources in the HTML. (They can be several layers deep if you want too! Personally, I put similar resources on the same line (gmail, yahoo mail, etc.) The final step overwrites an existing HTML file on my computer. I run the script as part of my logoff routine, so it's available next time I logon.

Variants of this background file have included live feeds from web cameras, weather maps and even graphs of system performance. I have also added portions to this script to have it set the background to the newly created HTML file, but since it never changes, it isn't quite necessary. Since I am still using a table to layout the page, the latest revision of this script defines the left and right columns using the screen's actual width, not percentages.

Have Fun!

My (Fixed - term) Mortgage Calculator

My wife and I were recently considering the purchase of a new home in our area. We went through a million what-if scenarios about the new home, laying out the pros and cons of the house, or current place and all the financial concerns involved.

While running all the numbers, I was constantly faced with numerous calculations. Going out to a mortgage calculator wasn't always the favorable solution. That is why I worked up this Excel spreadsheet to calculate various scenarios (what if our house sold of 10% more? What if we qualified for a 45year fixed loan? what if we paid off the car? )

The attached spreadsheet is the result of that process. You will need to populate the various 'yellow' fields with:

  • The price of your new home
  • A comparable price you can sell your existing home for
  • How much you still owe on it
  • Interest rates and terms for your market
  • Appropriate taxes and fees for your new neighborhood (like Mello Roos).

Our final deal-breaker on this house was a suggestion from a Realtor that we trust. She suggested waiting for the local market to improve so that our house would sell for a bit more. That would help us put down a larger down payment on the new place and lower the monthly bills.

Outlook Trick for Easy Mailbox Management

One thing every corporate employee has to deal with is email. Go on vacation, and you get tons of it. Stay home with a sick kid, and you get a ton more. The work does not stop, even if you can't go. In turn, we either don't take time off, even when needed, or we read our email all the time (Curse you Blackberry!).

As an IT administrator, I would get alerts. Lots of them. Backup alerts, server alerts, email slowness alerts, spam filter alerts, and even alerts that the alerts didn't work! To combat this ever growing corporate 'spam', I created a simple set of rules in Outlook to manage them.

Using Outlook's own auto-archiving functionality to clean-up messages, I was able to maintain a mailbox well within the corporate standards. The attached PDF document details how to duplicate these settings.

Note: A good portion of performance of Outlook in an Exchange environment depends on the local configuration and settings of the Outlook client. Check out this wonderful document created by a former co-worker regarding various settings Microsoft Support recommended.
It can be found here.

Outlook: Create Search Folders to find Read Receipts

I had an interesting request. A user wanted to search their mailbox for all read/not read/delivery/non-delivery receipts in their mailbox. You would think that it should be as easy as searching for "READ:" in the subject line. Unfortunately, Outlook does a wild-card type search on what you enter. This means you may get read:, bread: and read:s.

What about using forms? Based on my previous Outlook-form-coding experience, I knew that Outlook used different standard forms for the various types of messages. IPM.Note for email messages, IPM.Contact for (well, gee) contacts, etc.. As long as the message didn't traffic the Internet, the default form should be used. So, what is the default form for Read Receipts??

What form? Opening up a read receipt, I checked the properties tab. Here I found that the message type was report. Cool, now I went back to my query and tried to search on the form type of "report", but when you add the form type "report" to your search query, it returns NONE as the options. Now I know that this means there are no readable fields on this form.

Final solution: Doing a quick Google search on Message Class, I determined that this was the field I needed. Common message classes are IPM.Note and IPM.Contact. So, I created a query for all messages, where the message class contains report.

  1. From inside Outlook, right click Search Folders and create a new search folder
  2. Choose the type Custom Search Order
  3. Give it a name, then click the Criteria button
  4. From the Advanced tab, click the Field button, select All Mail Fields then Message Class
  5. Type report into the Value field and click Add to List
  6. Click OK to accept, then OK to close the Custom Search folder configuration screen.

My search folder returned several read receipts and an undeliverable response in my Inbox. I have not tested it with other response type messages, but believe it will find all message responses. Meeting responses will probably require a slightly different query.

Have fun!

Script to Simplify life

During my work day, I probably send a 20-30 email messages. Each time I typically look up someone's email address, copy it to the (keyboard) buffer (CTRL+C), click on a shortcut I have to open a new email, then paste the email address into the new message. I thought, what if my new email shortcut, would test the keyboard buffer and then automatically paste the address for me?

Save the following VBS to your local computer. Drag it into your quick launch bar, then change the icon to something useful (like Outlook's new message icon)! One click new emails that will be pre-populated with the last email address you copied.


'==========================================================================
' VBScript Source File -- Created with SAPIEN Technologies PrimalScript 3.1
'
' NAME: Send_Email.VBS - Read the keyboard buffer (aka clipboard)
' and send an email if read email address.
'
' AUTHOR: eric Woodford
' DATE : 2/13/2008
'
' COMMENT:
' clipboard idea: http://forums.vandyke.com/showthread.php?t=597
' mailto: http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=2149800&SiteID=1
'==========================================================================

Set objHTML = CreateObject("htmlfile")
ClipboardText = trim(objHTML.ParentWindow.ClipboardData.GetData("text"))
Set objHTML = Nothing
if instr(ClipboardText,"@") and instr(clipboardtext," ")=0 Then 'possibly a email address
Set objShell = WScript.CreateObject("shell.application")
str = "mailto:" & ClipboardText
objShell.ShellExecute str, "", "", "open", 1
Else
Set objShell = WScript.CreateObject("shell.application")
str = "mailto:"
objShell.ShellExecute str, "", "", "open", 1
End If
set objshell = nothing

Digital Camera, I Love You!

Back when my daughter was born, we were taking pictures of everything. "What a cute smile!" (click) "Oh she is biting that toy" (click) "She's finally asleep!" (click) (click) (click) (click) (click). This ended up to be one roll of standard 35mm film each day. Two months later, we were spending more on film developing, than on diapers! We started doing research on the multitude of options. The day before my sister-in-law's wedding in June 2002, we bought a Sony digital camera.

This camera, along with a 128mb memory stick (MS) allowed us to take approximately 60 pictures at a time. During the wedding, one of us would run upstairs in the hotel, download pictures, and come back. If I remember right, we took close to 600 pictures this way. According to family members, some of our pictures turned out better than the professional photographers. :) We were able to share our pictures out on Ofoto, and anyone could view them within a few days after the wedding. (The photographer took almost 2 months.) Plus, they could order copies for themselves, and we did not have to pay for the processing!!

Now 3 years later, we still have that camera, and have invested in second, smaller Sony camera (DSC-W5). This camera, has 32mb on-board and can use the Sony PRO Memory Sticks! With our 4GB PRO memory stick, we can take 1,660 (5 megapixel) pictures on a single outing! We never transfered pictures to the laptop the entire 8-days in Maui (over 750 pictures and videos). Additionally, the camera and 4Gb MS allow us to record up to 3 hours of continuous DVD quality video! No longer need to bring along our bulky Mini-DV video camera either!!

Unfortunately with digital cameras there are a number of technical considerations. If my computer where I am storing all the images dies, catches fire, or is stolen, I will lose 3 years of pictures. Also, with the simplicity of taking pictures and considering the low cost of viewing them, we now take multiples of each image (horizontal, vertical, with smiles, with a pose, candid, standing in front of the car, behind the car, etc.). In essence, there is virtually no limitations to how many pictures we can take on a single outing.
Technically, what does all this mean?

To use the attached file, you will need a unZIPing program like WinZip or 7-Zip. Open the file and extract the two files to your My Pictures folder. I'd suggest creating a shortcut on your desktop. (Hold down right-mouse button, drag to your desktop, release file and select "Create Shortcut".)

Copy and paste the following code into a file called MoveFiles.BAT. You will need to edit the script to reference the drive your memory stick connects to. Change the line Set MyMS= to point to your drive.

@echo off
Set MyMS=I:

call :GETDATE
Echo Transfering Pictures to:
Set FILEPATH="c:\
If %OS%==Windows_NT Set FILEPATH="%HOMEDRIVE%%HOMEPATH%\My Documents\
Echo ------ %FILEPATH%
c:
cd\
cd %FilePath%\My Pictures"
IF Not exist %YEAR%\ MD %YEAR%
CD %YEAR%
If not exist %TODAY%\ Md %TODAY%
cd %TODAY%
Start explorer %FILEPATH%My Pictures\%YEAR%\%TODAY%\"

dir %MyMS%\*.jpg /b /s > c:\pictures.txt
for /f %%a in (c:\pictures.txt) DO move %%a c:
if not exist c:index.htm copy "%HOMEDRIVE%%HOMEPATH%\My Documents\my pictures\index.htm" c:
for /f "delims=\ tokens=3,4,5" %%a in (c:\pictures.txt) DO Echo %%b >> index.htm
GOTO :VIDEOS

:GETDATE
REM Function to set TODAY equal to todays date in MM.DD.YYYY format
set GETDATE=%DATE%
set DOTDATE=%GETDATE:/=.%
set TODAY=%DOTDATE:~4,10%
Set YEAR=%TODAY:~6%
goto :EOF

:theend
cls
echo nothing to move
echo.
Goto :Done

:VIDEOS
Set Movies=%FILEPATH%\My Videos\"
cd %Movies%
IF Not exist %YEAR%\ MD %YEAR%
CD %YEAR%
Md %TODAY%
cd %today%
REM Also need to modify this line to use your correct memory stick location.
dir %MyMS%\*.mpg /b /s > c:\videos.txt
for /f %%a in (c:\videos.txt) DO move %%a c:
Echo Complete

:finished
explorer %FILEPATH%My Pictures\%YEAR%\%TODAY%\
notepad %FILEPATH%My Pictures\%YEAR%\%TODAY%\index.htm"

:Done

The attached VBScript is a work in progress. It needs a file set up on your removable media to read from. Then you can run the script to move files either direction. I use the same script to move MP3s onto my SD Ram chip.

My Home Disaster Recovery Plan

I was just reading through another news article about the fires in Southern California. The article was discussing the displaced families that were returning home for the first time. The focus family found that they had unfortunately returned to a burnt out shell of a home.

Among the possessions the a 56-year-old photographer lost were his transparencies, melted inside a fire-resistant box, and a photograph of his father.

"I've lost my history," Sanders said. "All the work I've done for the past 30 years, it's all destroyed."
- Yahoo News

This got me thinking. First, my computer has every digital picture (over 5 years and 20gb) ever taken of my kids on the hard drive. Secondly, a fire-safe box is not going to protect a stack of DVDs. What if my house burnt down while I was at work. I have visions of heroically racing into the den, grabbing my PC and running out of the house, but it is unlikely that my house will be burning while I am at home. "Hey, what's that burnning smell and why are the fire alarms going off? I am not cooking..."

Digital Disaster Recovery for the Home. With this, I have been implementing a backup methodology similar to those implemented in a corporate environment. This includes backing up all my digital files to removable media then shipping them to relatives. I currently have two stacks of 8 DVDs with 4GB of pictures and videos on each. Approximately every 2 months, I create 2 new DVDs containing the new pictures taken (one for me, one for off-site).

Next steps? A digital inventory of the house. As I stated earlier, digital images are virtually free to take and easy to keep. I plan to start taking pictures of household items, and their serials. I plan on starting with the electronic items (like the TV, TiVo, and digital camera). Then take pictures of the kitchen table and chairs (all wood), the bedroom set (also wood), the sofa and love seat, and well just about everything.

Insurance will cover replacing the material items. I can buy myself a new computer, stock full of all the applications I use. Unfortunately, there is no amount of money that can replace the happy moments we've recorded.

Email Troubleshooting 101: Introduction

In the last 10 years, email has evolved from the uber-geeky tool into the corporate mainstream solution that no one can live without. At the base, Internet email is a 40+ year old technology that was originally designed to provide a reliable (not time sensitive) solution for communications between colleagues. As it has gained popularity corporations have started to rely on it for business critical communications with their customers and vendors. Nowadays, IT departments learn about their outages by how well the email solution is performing.

In the rest of this article, I plan on covering a few of the troubleshooting techniques I use to resolve Internet email issues and what may have caused them.

The Parts of an Email
Every email message delivered across the Internet has 2 basic parts, a header and data. The header consists of the delivery information (To, From) and routing information (what servers processed the message when). The data portion consists of the subject, message body and any attachments. (Note: Since Internet email cannot directly handle files, these attachments are [w:uuencoding|converted] to text and placed inline with the message body. This encoding process can add an additional 30% to the attachment size when completed.)

Message Delivery
The delivery of an email message is not obvious. A message may bounce around a dozen times (or more) before it is delivered to the intended recipient. This entire transaction can be found in the message headers.

Here is a great graphic from Wikipedia:



The message header would look something like:

1: Delivered-To: bob@b.org
2: Received: by 10.1.2.15 with SMTP id ;
Mon, 29 Jan 2007 13:55:16 -0800 (PST)
3: Return-Path:
4: Received: from a.org (a.org [123.45.67.89])
by b.org with ESMTP id ;
Mon, 29 Jan 2007 13:55:16 -0800 (PST)
5: Mon, 29 Jan 2007 16:56:18 -0500
6: Subject: Dear Bob,
7: Date: Mon, 29 Jan 2007 16:56:18 -0500
8: From: Alice
9: To: Bob

If you read the header from bottom to top, you see that Alice sent the message at 16:56 -5hrs (or EST) (line #7) and it was delivered at 13:55 -8hrs (or PST) (line #2), meaning it officially delivered before it was sent! (or the two email servers are using different time synchronization servers. You'll also notice that this header includes the IP Address of the servers it touched (line 2 & 4). This is helpful when trying to troubleshoot errors later on.

  1. Open a command window (Start | Run | "CMD" click OK> and trying to PING each of these IP addresses. If you get a positive response, the servers are a least on the network.
  2. Now try to TELNET to the server on port 25 and see if the SMTP server responds.

    c:\Telnet 10.1.1.1 25

    You should get a positive response from the server. A failed response depends on the response.

Here are the possible SMTP Response codes that you may see. Please keep in mind that in general a 2XX indicates success, a 3XX indicates that the server has understood the request but requires further information to complete it, a 4XX error is temporary and a 5XX error is fatal. Float over the for my take on the error for my take on the error.

SMTP Code SMTP / ESMTP Message Description
211 System status, or system help reply
214 Help message
220Start sending your message. Service ready
221 Service closing transmission channel
250Success! Message Sent! Requested mail action okay, completed
251Rerouting message to administrator or alternate address for recipient. User not local; will forward to (other address)
354Message did not follow standard for SMTP mail. Try sending the message in plain-text format and remove any extra spaces at the end of the message. Start mail input; end with .
421The recipient's SMTP server is not responding on port 25. Chcek their DNS records and see if the server is available via PING. Service not available, closing transmission channel
450 Requested mail action not taken: mailbox unavailable
451The recipient may be trying to open your attachment and failing (virus scanning). Try resnding the message using a different encoding method (UUEncoding vs MIME). Requested action aborted: local error in processing
452The message will queue on your server and send when the server is available. Requested action not taken: insufficient system storage
500Your server may be trying to send EHLO commands to a standard SMTP server. Make sure your settings are not hard set. Syntax error, command unrecognized
501Not that common unless you are trying to communicate with a foreign system. Syntax error in parameters or arguments
502Your server may be trying to send EHLO commands to a standard SMTP server. Make sure your settings are not hard set. Command not implemented
503If the recipient server is very busy it may have not received all the information. Try resending the message. Bad sequence of commands
504Your server may be trying to send EHLO commands to a standard SMTP server. Make sure your settings are not hard set. Command parameter not implemented
550This happens when the server attempts to verify all recipients. It could be due to misconfiguration (like server timeout), mailbox deleted, or even spam filtering. Requested action not taken: mailbox unavailable
551The user is configured to only accept messages from certain users. This sender is not one of them. User not local; please try
552The recipient's mailbox is full and exceeded the receive limits imposed on them. Contact this recipient using other means. Requested mail action aborted: exceeded storage allocation
553If you get this error, most likely the mailbox no longer exists. Requested action not taken: mailbox name not allowed
554The recipient's server is blocking you. This could be due to limitations on the server (only accept from specific users) or their spam filter is too tight. Transaction failed

Exporting Exchange 2003 Mailbox Information

One of my peer departments is working on spamming the corporation. This internal spam, aka Corporate Newsletter, is to go out to everyone with a mailbox in the corporation. Unfortunately, a number of these people have abused their rights and their mailboxes are limited to receiving e-mail from only their managers.

In Exchange 5.5, you could create a simple CSV file and run the Admin tool to gather this information. Unfortunately, in Exchange 2003, mailbox settings are now integrated into Active Directory (AD). AD maintains all the mailbox information and settings, whereas the Exchange Admin tool is simply for server management (mail routing and global limits, etc.).

Then, how do you gather this information? I being the lazy, programmer-at-heart, that I am, wrote a VBScript to export the information. This worked great as I could set variables in the script to export the information I needed. This only worked for about a month, as I wanted pass this tool along to the internal employee for use (no way I was going to run this each time someone new was hired!!). That's why I put an HTA front-end on it!

To Use The Script

  1. Download attached ZIP and decompress to your hard drive.
  2. Modify the code to match your environment. Using Notepad without word-wrap turned on, will allow you to use CTRL+G to Go to specific line numbers.
    • Line 321 - modify LDAP query for your environment. We have seperate OU's for each affiliate office. The dropdown includes an option for each OU.

      All OUs
      Users OU Only

    • Line 99 - Modify the Select Case statement to include each of the OU's.

      Select Case ClassesPulldown.Value
      Case "LDAP://ou=users,dc=corp,dc=ent"
      strExcelPath="Mailbox_Export_"&today&"_Users.xls"
      Case else
      strExcelPath="Mailbox_Export_"&today&"_All.xls"
      End Select

  3. Make sure you have Excel installed on the local computer.
  4. Double-click HTA file and run. My export of the entire environment takes about 5-10 minutes for 3,500 mailboxes.

NOTE: We have set Custom Attribute 1 (aka ExtensionAttribute1) to a value (1, 2 or 3) for group or resource mailboxes. For example, the Postmaster mailbox has a value of 1,Conference rooms have a value of 2. Line 121 of the code is designed to filter on these values. Remove the line completely to get all mailboxes, or modify it for your environment.

Update August 16, 2007: I've modified the code to query the local AD for it's structure, so the above modifications are not necessary. After I made the changes, it started timing out when running the query, which shouldn't be related to the script changes. I suspect slowness in my AD.

Assign Secondary Account Permissions to Distribution Lists in Exchange 2003

The company I am working for has a slightly different security model than I've seen before. It is probably because the largest Exchange server I've supported previously only had 3,000 mailboxes. This one supports 10x that number, with plans to grow to over 100,000 mailboxes by this time next year.

With that, they use a seperate "security" domain for logon and authentication than the "resource" domain where their mailboxes reside. This means that the account you logon to your computer, may not be the same account you read your email from. That account will need to be granted secondary permissions to that mailbox, aka "Associated Exernal Account".

Along that same strain, you can no longer simply assign permissions to a user via the Managed By tab. Their mailbox is assigned permissions, but you need to grant their security account permissions also if the group owner wants to add/remove members.

That's why I developed this script. This script will assign Manager permissions to a distribution list. It assigns the selected name rights on the Managed By tab, then assigns the "Send As" and "Write Members" permissions on the Security tab to the selected user's Associated External Account.

There are a few limitations/warnings:

  1. It doesn't work with DLs that contain commas in their display name. It will present this groups in a popup box when you query the OUs. This may be resolved as of my latest release.
  2. If you attempt to query all DLs in an OU, it may return only a portion of them. Use the Contains functionality to filter the results. It works similar to a SQL LIKE command, in that it will return DLs that contain the phrase anywhere in it. (no wild cards) Found it was having issues with punctuation, like back slashes in names.
  3. It does not remove the old owner's permissions from the security tab. Heck, this could be useful when assigning additional owners to a single DL.
  4. I have not tested the script yet against a new owner without an associated external account. It will attempt to assign to the NT_DOMAIN\SELF account of the user.

I've cleaned up the code, so it no longer asks for a specific OU. It will query your entire domain for the specified DL. To run, double-click on the downloaded HTA and have fun!

Note: I have recently (3/26) updated this script to filter on the entries, instead of search each one. This makes the overall process rather quick.

Update (6/6/2008): I have added code to allow you to remove people from the security settings on a DL also. Currently working on an issue that generates an error when dealing with an apostrophe in the distinguished name (for example ldap://cn=Eric's Big List,ou=groups,ou=ericwoodford,ou=local). VBScript is taking it as the end of string and breaking..

Grant External Account Permissions to Modify Delegates

This HTA applet allows you to grant extended AD permissions to a specific user. I use it to assign permissions to the Associated External Account of an AD user rights to modify their own delegates.

I found what values I needed by configuring a single user with permissions, then using Richard's DACL export script to dump that user. I then modify the script (see line 248) to match the permissions I want to grant.

' Template: AddAce(TrusteeName, gAccessMask, gAceType, gAceFlags, gFlags, gObjectType, gInheritedObjectType)

The applet runs faster on the DC, but is usable on my local workstation.

Changing the Exchange Organization after Installation.

Today I had the honor of working with a company after a catastrophic failure of their Exchange server. One day their hardware was working just fine, the next the server will no longer boot and Exchange is down.

As part of the repair, I installed Exchange 2000 on a new server, and attempted to mount the databases from the mirrored drive. After a few manual uninstalls of Exchange and the the typical passes with ESEUTIL to repair the dirty shutdown the database was coming up clean, but still would not mount.

Checking the Application event log, I found that the server was built in the wrong Exchange Organization. Ugh! As Microsoft states repeatedly, to change the Org, one must reinstall Exchange. In Exchange 5.5 it was so bad, that we would worry about the case of the Organization name when adding new servers to an Exchange site.

That was COMPANY and not Company for the Organization name, right??

In 2000 and 2003, I've always understood that this had not changed. Plan ahead and do it right the first time. So, I started an uninstall of Exchange 2000 off this client's server while scouring Technet and Google for a better solution. Of course, as soon as I started the uninstall, I found that Exchange will NOT uninstall because it sees mailboxes are still on the server. Despite not successfully mounting, it must have partially mounted the database and Exchange now thinks they are on the server.

Using the Manual uninstall procedure again, I remove Exchange one more time, do another reinstall, but this time it also fails. Exchange never asked me to change the Organization as it is still registered in AD. Oh well. That's when I found this tool. LegacyDN.

Like RegEdit, this tool has tons of disclaimers:

Never use [name of tool] on a production database, never use unless you have backups, never use on unless you have PSS on hold on the phone and we tell you to run it..

So, I closed my eyes, crossed my fingers and double clicked on it. By default it comes up in read-only mode. I immediately see that it found the First Administrative Group on the server. Clicking on the fields do nothing, but I can see I have hope. So I close it, and reopen it in /FORCEWRITE mode. Click and the Organization field populates. I replace the text with the correct entry, click Update, wait 30 seconds and "The Organization has been updated successfully.". I imagine that my 30 second wait was in part to only having 8 mailboxes in this information store. If this had been a store with a couple hundred or thousand mailboxes, this process may have taken much longer.

Now the database mounted successfully and all is happy. The users can send and receive email without issue. Next, migrate them to Exchange 2003...

Some useful references.

DST: Get DN for Exchange Calendar Update tool

When running through the latest version of the Exchange calendar update process, it has you assign permissions to all mailboxes on the server. To modify all the mailboxes, you need to provide a list of distinguished names (DN) for each mailbox. Unfortunately this is not simply the DistiguishedName field that you would query from AD, but the LegacyDN entry. You need to query the Exchange environment to get this information.

The following script, I pulled from various sources (primarily my Advanced VBScript book off my desk). It echos the DN to the screen, and also creates a CSV file in the folder it was run from. I did have to run it from the Exchange server, but it may also run from a workstation running the Exchange system admin tools.




dim strComputer
dim objwmiservice
dim propvalue
dim SWBEMLocator
Dim UserName
Dim password
Dim ColItems

strLog="MailboxReport.csv"
Set objFSO=CreateObject("Scripting.FileSystemObject")
Set objFile=objFSO.CreateTextFile(strLog,True)

strComputer = inputbox("What is the name of your Exchange server?")
wscript.echo "Querying " & strComputer

username=""
password=""

Set SWBemlocator = CreateObject("WbemScripting.SWbemLocator")
Set objWMIService = SWBemlocator.ConnectServer(strComputer,"\root\MicrosoftExchangeV2",UserName,Password)
Set colItems = objWMIService.ExecQuery("Select * from Exchange_Mailbox",,48)

for each objitem in colitems
wscript.echo objitem.legacydn
objFile.writeline objitem.legacydn
next
objfile.close


Lookup email addresses from CSV in AD

Scenario: I was given a list of 15,000 email addresses and asked if they were still valid in our Exchange environment.

Easy method: I ran a simple VBScript that does an LDAP query against each email address. This worked great, except that it took close to 5 seconds per email address to query our environment. (~20 hrs!) The over-all run time was going to be too extreme.

Next method: Read all entries into a datalist using the tricks from Microsoft Scripting Guys. The script would read each object (primary;proxy) into seperate records, then search each one. This ran for 2 hours before the server logged me off. Even if I deleted entries that I found, it still took too long.

Final method: Read each email address and append it to a string for each letter (for example "administrator@example.com;author@example.com" went in A) accessible via an array. Now I have a 50ish (a..z,0..9,!#$%^) row array containing unsorted lists. The script just needs to find the correct row, and see if the email address exists there.

The entire process took approximately 14-15 minutes. This inclides 12 minutes to read in all 250k email addresses from Active Directory(AD) and then sort them. Processing it creates a NEW csv containing the original line, then appends TRUE or FALSE. If it finds the email address, it puts a TRUE, otherwise it puts FALSE. It does very little clean-up of the email addresses it reads from the CSV, but that could be improved rather easily.

Modify Mailbox Alias field

We recently finished a project updating the naming standard for all our Windows AD accounts. Once completed, I found that approximately 3% of the accounts had the alias field set to this old naming standard. Googled high-and-low, I could not find a simple script to set the alias field. So...

This script reads a text file of account distinguished names, then modifies the mailbox alias (aka mailnickname) field to match the SAMAccountName field. Added protection for accounts without mailboxes associated with them. Found that by setting a mailnickname, mail-enabled the account.


' Script to modify the mailbox alias of all accounts in text file to match SamAcct.
' Written by Eric Woodford
' Date 11/27/2007
'
Dim mbxAlias
dim samAcct
Dim objfso, tf, ef
Dim strUser, arUser
Dim objUser, strDN
Dim testrun

set objfso = CreateObject("Scripting.FileSystemObject")
'reads from this file
set tf = objfso.OpenTextFile("c:\useraccounts.txt",1)
' creates this log file
set ef = objfso.CreateTextFile("c:\User-Updates.log",vbtrue)

'set this boolean value to TRUE to it will not modify accounts. Good for testing results.
testrun = true

if testrun Then
ef.writeline "Starting TEST RUN at: " & Now()
Else
ef.writeline "Starting Account Conversion at: " & Now()
End If

' Loop through file
While tf.AtEndOfStream <> True
strDN= tf.readline

' cleanup values from export.
strDN = trim(replace(replace(Replace(strDN,"\\","\"),"""",""),"''","'"))
If Left(strDN,1) = "'" then strDN=Mid(strdn,2,Len(strDN))
If Right(strDN,1) = "'" then strDN = Mid(strdn,1, len(strdn)-1)

'don't bother processing if not a distinguished name.
If instr(strDN,"CN=")>0 Then
ef.writeline now() & ":" & strDN
'ef.writeline "Wanting to set to:" & arUser(1)
Set objUser = GetObject("LDAP://"&strDN)
samacct = objUser.sAMAccountName
mbxAlias = objUser.mailnickname

' skip if already set, or no mailbox associated with it
If (LCase(trim(mbxAlias))<> LCase(samacct)) And (mbxAlias<>"") Then
' If these don't match, then the account needs updating.
' a little formatting for the report.
sp = vbtab
If Len(mbalias) < 6 Then sp = sp & vbtab
If Len(mbalias) < 12 Then sp = sp & vbtab

ef.WriteLine "Alias: "&mbxAlias&sp&" should be: "& LCase(samacct)
' This means that the Alias is still set to the old account information.
If Not testrun Then
objUser.mailnickname = samacct
objUser.SetInfo
Else
ef.WriteLine "testrun - values not changed."
End If
Else
' don't bother updating as it is already correct.
ef.WriteLine "Alias correct: " & mbxAlias &sp&" is "& LCase(samacct)
End if
Set objuser=Nothing
End If
Wend

tf.Close
ef.Close

Powershell Quick report - all smtp email addresses

Attending a class this week for Exchange 2007. The question came up to export all SMTP email addresses for all mailboxes. In the lab environment, I worked up this script.


$data = get-mailbox | %{ $dname = $_.displayname;$em=$_.emailaddresses -replace("smtp:",",");$dname+$em}

I have not tested this in a production environment, so I am not sure what will display for users with other than SMTP email addresses.

Purging a list of Mailboxes from Exchange 2003

The migration process used to move a number of mailboxes created a couple hundred dead mailboxes objects in our environment. Their mailbox information needed to be cleared before their mailboxes could be migrated to the server for the final time. To fix this, I looked into a script. First I found the script at TelnetPort25, here. This script goes through the entire environment and purges all deleted and non-system mailboxes. This would work great, but querying an environment with literally thousands of mailboxes is a daunting task.

That's when I found this script that would simply dump all the deleted-not-purged mailboxes on a specific server. Running this script generates a CSV file with their displayname, and some additional information. I copied the displayname field out of Glen's script and pasted to a text file to use and re-wrote Telnet's to fit my purpose.

Note: uncomment the line ' objExchange_Mailbox.Purge when you feel the script is working correctly.


'===================================================='
' VBScript Source File -- Created with SAPIEN Technologies PrimalScript 3.1
'
' NAME: PurgeSelectedMailboxes.VBS
'
' AUTHOR: Eric Woodford - EricWoodford@gmail.com
' DATE : 3/10/2008
'
' COMMENT: Purge listed mailboxes on this Exchange server.
' Must be ran from the server.
'
'====================================================

Const cWMINameSpace = "root/MicrosoftExchangeV2"
Const cWMIInstance = "Exchange_Mailbox"

strComputerName = "."
strPathToCSV = "c:\"
strListofNames = "names.txt"

Set objfso = CreateObject("scripting.filesystemobject")
Set infile = objfso.opentextfile(strPathtoCSV&"\"&strListofNames,1)

Set wmiConn = GetObject("WinMgmts:{impersonationLevel=impersonate}!\\" & strComputerName & "\root\microsoftexchangev2")

If Err.Number <> 0 Then
WScript.Echo "Cannot connect to the Exchange WMI Namespace"
Wscript.Quit
End If

haltloop = false
DO While (not inFile.atendofstream)
strUserDisplayName = inFile.readline
WScript.Echo strUserDisplayName
strWQL = "SELECT * FROM Exchange_Mailbox WHERE MailboxDisplayName = '" & strUserDisplayName & "'"
' filter collection to only selected names
WScript.Echo " Searching... Please wait"
Set wmiColl = wmiConn.ExecQuery(strWQL)
WScript.Echo "Found "&wmiColl.count & " matching mailboxes"

For Each objExchange_Mailbox In wmiColl
WScript.Echo "Processing: " & strUserDisplayName
' objExchange_Mailbox.Purge
next
Loop
infile.close
Set wmiColl = Nothing
Set objWMIExchange = Nothing
set objfso = Nothing

Wscript.Echo "Script Completed"

Tool to Export DL Membership

Here is a small HTA applet that I have built to export the membership of various groups in Windows Active Directory. It provides some basic filtering (owner, specific OU, name contains), plus 3 modes of export, XLS, CSV, or HTML to screen. Beware, if you have office 2007, XLS is XLSX format.

To run this in your environment, it will need to be modified:
line 27 & 28 strHTML=strhtml&"All OUs"
strHTML=strhtml&"This SUB OU Only"

line 55 & 56strHTML=strhtml&"All OUs"
strHTML=strhtml&"This SUB OU Only"

CSV mode gives the best detail as XLS has a 255 character limit on single fields.

Just fixed issue where it required you to pick a subOU in order to continue.

Find ALL DLs that a specific person manages

Friend is working on the dirty deed of removing a series of accounts from AD. As part of that clean-up he is to find all the distribution lists that each account is assigned manager on. Using the Quest ActiveRoles Powershell tools, you can quickly find these unfortunate people using:


$selectUser = Get-QADUser "John Doe"
Get-QADGroup -SizeLimit 0 -ldapFilter '(&(mail=*)(managedby=*))' | where {$_.managedby -eq $selectUser} | select displayname, managedby

Using a Batch to Exmerge a Recovery Storage Group - Mailbox from Exchange 2003

I am working on a project to recover 30 individual days of email from a clients mailbox. This requires me to restore 30 days worth of backups to the Recovery Storage Group(RSG) on a specific server. I have quickly grown tired of selecting the name from the giant Exmerge list and worked out how to use a batch file to run Exmerge against the RSG.

Part 1: Exmerge.ini

[Exmerge]
MergeAction=0
RestoreDB=1
FileContainingListOfDatabases=database.txt
SourceServerName=
FileContainingListOfMailboxes=Mailboxes.txt
CopyDeletedItemsFromDumpster=1
DataDirectoryName=D:\EXMERGEDATA
LogFileName=ExMerge.log

Part 2: database.txt

CN=SG1-PRIV1,CN=RECOVERY STORAGE GROUP

Part 3: mailboxes.txt

/o=/ou=First Administrative Group/cn=Recipients/cn=
/o=/ou=First Administrative Group/cn=Recipients/cn=

Part 4: the Batch file (exportMbx.bat)

exmerge.exe -b -f exmerge.ini

I've now put a shortcut to the bat file on my desktop and double-click it after the restore completes. If I could only find a script that would then dismount the database, mark it for overwrite and start a new restore from the server, I'd be done! Unfortunately, I've been told by Microsoft you can't script against the RSG, so still got a bunch of clicks for each day.

Back to work...

VBScript- Create Distribution Groups (DL) from CSV

I needed a quick script to create a series of distribution lists on an Exchange environment. I thought, "Cool, a chance to flex my PowerShell muscles!" From that I found new-qadgroup from the quest tools set and new-distributiongroup from the Exchange tools. After quite a bit of muddling around, I was never able to recreate my script.

Requirements I was trying to meet:

  1. Read all data from a CSV file (easy import-csv)
  2. Create a new DL from each line in the CSV (| %{new-distributiongroup -name $_.name}
  3. Populate each list with memers found in the members column (this was semi-colon seperated).
  4. Populate ExtensionAttribute3 with a common value 'FINANCE DLs'. This is used by our Dynamic Address Book views.
  5. Set limit of max message size sent to DL
  6. Set who was able to send to this DL (groups and people)
  7. Set SMTP and Proxy SMTP email addresses.

Unfortunately, after working on the first 3-4 items, I was stumped. VBScript failed me on a number of these, just in the fact that it's not widely published and a few answers varied. So, here is what I developed. The script fails when adding more than 1 additional proxy address, otherwise it worked for all my other tests.

(Sorry for the wrappage..)


'=====================================================
'
' VBScript Source File -- Created with SAPIEN Technologies PrimalScript 3.1
'
' NAME: CreateDL.VBS
'
' AUTHOR: Eric Woodford
' DATE : 7/25/2008
'
' COMMENT: Script is designed to create fully populated distribution lists.
' The data is pulled from a CSV file.
' Assigned fields are:
' Display Name,
' Alias,
' SMTP Email address,
' Proxy SMTP addresses,
' Accept Messages from,
' Maximum Accepted message size (KB - integer format please)
' Members of the group
'
' Known issue: Accounts with more than 1 proxy address mail fail to load correctly. Still working out the details.
' ProxyAddresses - http://www.eggheadcafe.com/forumarchives/scriptingVisualBasicscript/Aug2...
'=====================================================

Const ForReading = 1, ForWriting = 2, ForAppending = 8
Const DLGroupEA3 = "Finance DLs"
Const GroupOU = "OU=Finance Users,"
Const CSVFilePath = "C:\Finance_DistributionLists.csv"
Const LogFIlePath = "C:\CreateDL.LOG"

Set fso = CreateObject("scripting.filesystemobject")
Set myCSV = fso.opentextfile(CSVFilePath, ForReading)
Set MyLogFile = fso.opentextfile(LogFIlePath, ForWriting, True)

Do While Not MyCSV.AtEndOfStream
strCSVLine = myCSV.Readline
If InStr(strCSVLine,"@")>0 Then ' if it contains a SMTPEmail Address, it must be valid (not a header).
arrStrUser = Split(strCSVLine,",")
strAlias = arrStrUser(1)
strDisplayName = arrStrUser(0)
strSMTP = arrStrUser(2)
strMembers = arrStrUser(3)
strAcceptFrom = arrStrUser(4)
IntMaxMsgSize = cint(arrStrUser(6))
CreateDistGroup strAlias,strDisplayName, strSMTP ,strAcceptFrom, IntMaxMsgSize
End If
Loop
MyLogFile.Writeline "Adding Users"

Set myCSV = fso.opentextfile(CSVFilePath, ForReading)
Do While Not MyCSV.AtEndOfStream
strCSVLine = myCSV.Readline
If InStr(strCSVLine,"@")>0 Then ' if it contains a SMTPEmail Address, it must be valid (not a header).
arrStrUser = Split(strCSVLine,",")
strAlias = arrStrUser(1)
strMembers = arrStrUser(3)
AddGroupMembers strAlias, strMembers
End If
loop

MyLogFile.close
MyCSV.Close

Sub CreateDistGroup(strAlias, strDisplayname, strSMTP, strAcceptMsgsFrom, intMaxSizeKB )
Dim strGroup, strDNSDomain
Dim objOU, objGroup, objUser

Const ADS_GROUP_TYPE_UNIVERSAL_GROUP = &h8
Const ADS_GROUP_TYPE_SECURITY_ENABLED = &h80000000
Const ADS_GROUP_TYPE_GLOBAL_GROUP = &h2

Const ADS_PROPERTY_CLEAR = 1
Const ADS_PROPERTY_UPDATE = 2
Const ADS_PROPERTY_APPEND = 3
Const ADS_PROPERTY_DELETE = 4

Set objRootDSE = GetObject("LDAP://RootDSE")
strDNSDomain = objRootDSE.Get("DefaultNamingContext")
Set objOU = GetObject("LDAP://" & GroupOU & strDNSDomain )
strNewGpLong = "CN=" & strAlias
Err.Clear
On Error Resume Next
Set testGroup = GetObject ("LDAP://"&strNewGpLong&","& GroupOU & strDNSDomain)
If Err <> 0 Then
MyLogFile.Writeline "creating: " & strDisplayname &"("&strAlias&")"
Set objGroup = objOU.Create("Group",strNewGpLong)
objGroup.Put "sAMAccountName", strAlias
Else
MyLogFile.Writeline strDisplayname & " already exists"
End If
objGroup.put "Name", Replace(StrDisplayName," ","")
objGroup.Put "displayname", strDisplayname
objGroup.Put "groupType", ADS_GROUP_TYPE_GLOBAL_GROUP
objGroup.put "extensionAttribute3", DLGroupEA3
if intMaxSizeKB > 0 then objGroup.put "delivcontlength", intMaxSizeKB
objGroup.mailenable
objGroup.setInfo
objGroup.put "dLMemSubmitPerms", "cn="&strAcceptMsgsFrom&","&GroupOU & strDNSDomain
if instr(strSMTP,";")=0 Then
strNewSMTP = mid(strSMTP,6)
'strNewSMTP = strSMTP
MyLogFile.Writeline "Adding : " & strNewSMTP
objGroup.Put "mail", strNewSMTP
objGroup.put "targetAddress", strNewSMTP
objGroup.PutEx ADS_PROPERTY_UPDATE, "ProxyAddresses", array(strSMTP)
objGroup.SetInfo
Else
arrSMTP= Split(StrSMTP,";")
strNewSMTP = mid(arrSMTP(0),6)
MyLogFile.Writeline "Adding : " & strNewSMTP
objGroup.Put "mail", strNewSMTP

'http://support.microsoft.com/kb/q260251/
x = 0
For Each sAddress In arrSMTP
MyLogFile.Writeline "+" & sAddress
If x = 0 Then
objGroup.PutEx ADS_PROPERTY_UPDATE, "ProxyAddresses", array(sAddress)
objGroup.put "targetAddress", sAddress
x = x + 1
Else
objGroup.PutEx ADS_PROPERTY_APPEND, "ProxyAddresses", array(sAddress)
End if
objGroup.SetInfo
Next
End If
objGroup.setInfo
Set objGroup = Nothing

End Sub

Sub AddGroupMembers (strAlias, strMembers)
On Error Resume next
Set objRootDSE = GetObject("LDAP://RootDSE")
strDNSDomain = objRootDSE.Get("DefaultNamingContext")
Set objGroup = GetObject ("LDAP://cn="&strAlias&","& GroupOU & strDNSDomain)
If strMembers <> "" Then
arrMembers = Split(strMembers,";")
For Each struser In arrMembers
Err.Clear
objGroup.Add("LDAP://cn="&strUser&","&GroupOU & strDNSDomain)

If Err = 0 Then
MyLogFile.Writeline "Successfully added: " & strUser & " -> " & strAlias
Else
MyLogFile.Writeline "Failed : " & strUser & " -> " & strAlias
End If
next
End if
End Sub

Find Those Printers

When decommisioning an older server in the environment, it is essential that all the former services are moved off it. This includes the existing applications (including databases), file shares and printer shares. Unfortunately there is no easy way to capture who is mapped to each of the pritner shares. Outside simply monitoring printer traffic, or visiting each desktop, Windows (AFIK) has no method of tracking this.

Hence another script. This script queries the active computer for all printers, then exports the 'resource name' (port) and share name to a CSV in a specific location.


' Source: http://www.devguru.com/Technologies/wsh/quickref/wshnetwork_EnumPrinterC...
' Compiled by Eric Woodford

'change this path to match your current environment.
strLogPath = "\\servername\admin\printermappings"

Const forReading = 1
Const ForWriting = 2
Const ForAppending = 8

'get environment variable username
set shell = WScript.CreateObject( "WScript.Shell" )
username = shell.ExpandEnvironmentStrings("%USERNAME%")
Computername = shell.ExpandEnvironmentStrings("%COMPUTERNAME%")
set shell = nothing

auditfile = strLogPath &"\"&username&"_"&computername&".csv"

set fs = CreateObject("Scripting.FilesystemObject")
Fs.CreateTextFile(AuditFile)
set f = fs.OpenTextFile (AuditFile, ForWriting, True)

strText = "Resource name,Printer name" & vbcrlf
Set WshNetwork = WScript.CreateObject("WScript.Network")
Set clPrinters = WshNetwork.EnumPrinterConnections
For i = 0 to clPrinters.Count -1
if i mod 2 <> 0 then
strtext = strtext & clPrinters.Item(i) & vbcrlf
else
strtext = strtext & clPrinters.Item(i) & ","
end if
Next

f.writeline strText
f.close

set fs=nothing

Query AD for Printer info

Expanding on my code to query single printer information, I found I could pull all networked printers from Active Directory(AD). This will help in cleaning up the naming standard for all our printers. Hopefully making DNS cleaner by using a single standard for host(A) records. Final file is written to c:\printer-info.csv. The attached script will return the following:

  • Printer name
  • Server name - where hosted
  • Port - name of printer port (local or network)
  • Share name
  • Host address (what the port points to) - requires Windows 2003 or XP print server
  • Protocol (RAW or LPT) - requires Windows 2003 or XP print server
  • RAW Port (if using RAW) - requires Windows 2003 or XP print server
  • SNMP Community (if enabled) - requires Windows 2003 or XP print server
  • Host Name - as pulled from DNS - requires IP Address, captured in Host Address field Windows 2003 or XP print server

To get the script to run, you will need to modify this line (#19) to match your current environment.

Const MyDomain = "DC=my_domain_goes_here,DC=com"

Sources: Microsoft, Microsoft, and Geek Speak

Google. The Plagiarism Detector Extrodinaire!

My wife is a seventh grade science teacher. One of the many difficulties she has to deal with, especially on large written projects is student plagiarism. The Internet is just too tempting for some students to avoid. Here they have searched and found the perfect article that answers all the questions that the project asks of them. Why not just print it out and turn it in??

Fortunately for seventh graders, school does not expell kids for plagiarism, they simply have to do the project over again or take a zero on the assignment. Unfortunately for the students, their plagiarism is typically very easy to spot. Not many 12 year olds can typically write at the same level of a college graduate (who would typically be publishing papers on scientists, or science topics). This makes locating sources of plagiarism very easy to locate.

How?
Take the typical class project, a report on a famous scientist and pull segments of phrases that the student used.
Take for example this segment:

Benjamin Franklin
Franklin was a prodigious inventor. Among his many creations were the lightning rod, the harmonica, the Franklin stove, bifocal glasses, and the flexible urinary catheter. Although Franklin never patented any of his own inventions, he was a supporter of the rights of inventors and authors and was responsible for inserting into the United States Constitution the provision for limited-term patents and copyrights. My source..

When reading a paragraph like this on a student's report, a red flag often goes up when they use phrases like Franklin was a prodigious inventor or uses words like "among" or "although". (If not plagiarisd, it is likely Mom or Dad wrote it.)

Using Google
In the search field of Google, I do an explicit search for the scientist's name.

"Benjamin Franklin"

13,400,000 hits Here

Now, I try by adding a phrase from their assignment.

"Benjamin Franklin" "Franklin was a prodigious inventor."

15 hits - 5 shown Here

From here, 5 sources are not that difficult to follow-up on. Typically, you can find the student's source within a few hits. Print out their source, staple it to the assignment, then mark it with a 0! OOOO What fun!!

Hate Commercials? Get a MP3 Player!

4 years ago I invested in my first MP3 player. I purchased online a 1GB micro-drive player from RIO. This device was perfect for what I needed. I could load up 100+ songs on the device and listen to commercial-free music where ever I went.

Early last year, that microdrive quit working. I guess dropping it on the ground a few times was a really bad idea. This was unfortunate, because I found several authors had started podcasting their books for free. Now I had a better understanding of what I wanted in a player.

For a replacement, I had these requirements:

  1. Bookmark functionality - Listening to a 1 hour audio book, you don't want to have to fast-forward through 45 minutes to get to were you left off.
  2. Flash memory - preferrably with a SD memory chip option. My workstation at home, and my corporate laptop both have a SD slot. Using SD, allows more flexibility and infinite expandability.
  3. Inexpensive - I wanted to keep this under $100. Lose it? Buy a new one.
  4. Flexibility - I had heard that the iPOD required you to download from iTunes (only). I wanted a device that would support downloading from any source.

Based on this criteria, I started shopping. I checked Target, Wal-Mart and Best Buy to see what was available. I found that seldom do MP3 manufacturers advertise 'bookmark' functionality on their devices (like the RCA Lyra). As this was key to my search, I went to searching on the web for brands sold locally.

Personally, I like using PriceGrabber.com for their search, and review options. I can filter down all my options in one search field and get the best reviewed products. Plus they often have links to sites with free shipping!! (Almost as good as buying it locally!).

Decision Time

I ended up purchasing another RIO device. I knew the RIO devices had the bookmark functionality, plus the Forge 256mb Sport, has a SD memory slot. I also loved the idea that it used standard AAA batteries, so I wouldn't need to purchase a separate charger for my car. I did purchase a bunch of rechargable AAA batteries and so far, the device has performed wonderfully. I download files to my laptop, use the internal SD slot to put them on the SD chip, then listen!

iPodder/Juice Script Cleanup ID3 tags

As you may already know, I use my MP3 player simply to play audio-books (aka podcasts) while commuting to work.

I use Juice (fka iPodder) to capture these files. It does an excellent job of downloading content and placing it on my machine. Once downloaded, I drag and drop the files to my MP3 player.

Unfortunately, my cheap ($5) MP3 player (Sansa 100?) sorts files using an odd technique. It appears to sort them by track number, then title. So if I have 2 active stories on my player, it will play track 1 - story 1, then track 1 - story 2, track 2 - story 1, etc. I think this is because sometimes the track number is a 'string' value?? To fix this, I cleanup the ID3 tags values.

This script utilizes the CDDBControl (from Roxio) to get access to the ID3 tags. If you have a Roxio product installed, you may already have this file. Otherwise, I've downloaded it from here. Register it like you would any new DLL on your computer.


Dim id3: Set id3 = CreateObject("CDDBControlRoxio.CddbID3Tag")
Dim FS: Set FS = CreateObject("Scripting.FileSystemObject")

argCount = WScript.Arguments.Count
If argCount = 2 Then
FileName = WScript.Arguments.Item(0)
strTitle = WScript.Arguments.Item(1)
set File = fs.getfile(FileName)
id3.LoadFromFile File.Path, False
track = id3.TrackPosition

If (track <> "") And Not(isnumeric(mid(id3.Title,3,2))) Then
If not IsNumeric(track) and len(track)>0 Then
do while Not(IsNumeric(track)) And Len(track)>0
track = left(track,Len(track)-1)
Loop
End if
If CInt(track)<10 Then track = "0"&track
newtitle = ucase(Left(strTitle,2)) & Track & "-" & strTitle
id3.Title = newTitle
id3.SaveToFile File.Path
End If
End If

Inside Juice, I setup an advanced option to call this script when a download finished. Cool? Go into Juice, select File, then Preferences, and click on the Advanced tab. Click the checkbox for Run this command after each download, then put in a fully qualified path to the script. Mine is:

c:\bin\scripts\JuiceUpdate.vbs "%f" "%n"

You see a short popup each time a file finished downloading and the script runs. The Title name changes to the first two characters of the name(track number) - Full name of podcast.

For example (for the Max Quick Part 2):
Title before = PB-Max Quick 2: Two Travelers - Episode 1
Title after = MA01-Max Quick 2: Two Travelers

Hey Neighbor, Thanks for the Free Internet!

In recent months, I have noticed quite a few new local wireless hot-spots appeared in my neighborhood. These hots-spots are my neighbors (within 1000 feet of my house), who are using the out-of-the-box configuration and no security measures. This means anyone driving by my house can access the Internet while sitting in their car (war driving).

Using a free tool (Netstumbler), I can gather information about all my neighbor's wireless routers, including name and encryption settings.

Now that I know their router settings, I can become a member of their network. In addition to using their connection to the Internet for free, I could connect to shared network resources, like printers, and shared drives. Additionally, since they haven't enabled encryption on their router, they probably haven't changed the router's admin account password.

With little effort, I logon to their router, and check the DHCP table for a list of currently active devices, including computer names and IP addresses. I can try PINGing the IP address and see if it responds. If it responds, I can try mapping a drive to the default admin shares like C$ or browse the computer by simply typing in the IP address in the Windows START - RUN line, "\\192.168.1.100", and see what comes up. By default, Windows will have a "Shared Doc" folder available, but sometimes you will also see a printer listed here.

What can you do about it?

Like security for your automobile, doing the basics of locking the doors and closing the windows is enough to deter most (wireless) thieves. They can always simply travel next door and find a much easier target. With the security now configured on your wireless router, you should be able to easily move your wireless devices between home, work and the coffee shop wireless networks with very little difficulties.

Logparser Script: Executive Mailbox Report Access Report

I was recently tasked with the process of setting up a fairly automated report to display each time one of our Executive Management Team's mailboxes had been accessed by internal users.

We had already turned on the log generation on the server according to the Microsoft tech article 867640. This generates a 1016 event in the server's application log each time a mailbox is accessed.

According to MS this includes:

In addition to malicious intent, each time someone books a meeting with another person, a backup is ran that uses a MAPI connection, or services like Blackberry Enterprise Server, accesses a mailbox they will also be annotated in the app logs. Plus, this report is destined for Sr. Management, so sending a dump of the Application logs was out of the question.

LogParser should already be your best friend. You can use this versatile tool to query any ASCII log file or server Event logs to pull out information.

Setup
The attached batch file, runs a logparser query against a mailbox server and generates a SUMMARY.CSV file.

  1. Decompress ZIP file into a folder C:\SUMMARY
  2. Modify SQL file to reflect your environment.
    • Change SMTP Addresses and Display to match those you wish to query
    • Change FROM Server to match the mailbox server containing these users
  3. Run the BATCH file.

Details
We run this batch 6 times a day (using a Windows Scheduled task), creating a 1kb file for the 20 something Executives we monitor. After a month, I have almost 1mb of log files. The script is designed to pull information only from the last time it ran, so no overlap. This batch creates a new file with updates since the last run, then rebuilds the SUMMARY.CSV file.

The summary contains the display name of the executive, date of mailbox access, the domain account that accessed the mailbox, and how many times on that day.

I've expanded this by exporting all active mailboxes in the domain (see HTA coming soon) and import that into an Access database. I then created a 'linked table' connection to the CSV. Using a simple query to correlate the domain account, to a display name from Active Directory. (I only need to update the AD Export, when a account does not resolve correctly in the query.) Then I use Crystal Reports, pulling from the Access Query, to filter the information, generate summaries, etc.

Future
To expand upon this, we've considered porting the data collected to a SQL server (which Logparser handles nicely). Until then I have a simple query I can run anytime, and get relatively up-to-date access reports for these users.

Using LogParser to query Exchange 5.5 SMTP traffic

We are working to decommission our existing Exchange 5.5 environment and looking to migrate all services to Exchange 2003. As part of the decom, we need to re-direct the Internal SMTP traffic off our Exchange 5.5 bridgeheads. This meant, determining which servers were connecting to the server. First we changed all MX records in our DNS to point to the new server. Then we watched the Application logs on the server for MSExchangeIMC - ID: 2000 events.
This provided some 2,000 hits on our server. Ouch! Using LogParser from Microsoft, I was able to generate a quick query to pull the information.

Server Hits
citrixserver.example.local 373
othersmtpserver.example.local 214
appserver.example.local.local 150
192.168.0.28 25
webserver.example.local 1

With this data, I was able to contact the server owners and have them change their relay information.

logparser "select trim(substr(strings,0,index_of(strings,'|'))) as Server, count(*) as hits into LogFile.csv from \\ExchangeServer\application where Sourcename like 'MSExchangeIMC' and TimeGenerated >= SUB(TO_LOCALTIME(SYSTEM_TIMESTAMP()), TIMESTAMP('0000-01-01 00:00:00', 'yyyy-MM-dd hh:mm:ss')) and eventid=2000 group by strings order by hits DESC"

Let me break this down a bit.

  1. What fields I want
    This section determines which fields are pulled out. Strings is a Application Log field. The query pulls out the server name from this field, then counts each entry in the value HITS.

    trim(substr(strings,0,index_of(strings,'|'))) as Server, count(*) as hits

  2. Where to/from
    Put the results in the specified log file when pulling from this server's app log. Putting commas between the different servers, you can list several there. (\\server1\application, \\server2\application).

    into LogFile.csv from \\ExchangeServer\application

  3. Give me only what's relevant:
    Give me all MSExchangeIMC events where the ID = 2000 and in the last 2 days. The long line for 'timegenerated' is the long date difference field.

    Sourcename like 'MSExchangeIMC' and
    eventid=2000 and TimeGenerated >= SUB(TO_LOCALTIME(SYSTEM_TIMESTAMP()), TIMESTAMP('0000-01-02 00:00:00', 'yyyy-MM-dd hh:mm:ss'))

  4. Make it pretty
    This summarizes the data and combines on the SERVER field. Then it sorts the group by HITS in descending order.

    group by server order by hits DESC

This query runs in about 20 seconds on 2 servers with 3 days of data. Putting this into a BATCH file, I was able to make the script rather user friendly.

My HTML Calendar Generating Script for Exchange 2003

In my office, we have over 60 different resources that can be booked online. These resources (including conference rooms, laptops and LCD projectors) can all be scheduled via Outlook and Microsoft Exchange, using Microsoft's latest Auto-Accept Agent. The main draw back is that you can't really tell who has what room for how long. With the previous version of Exchange (5.5) there where issues with users having permissions to the calendars. Outlook would corrupt the conference room calendar and cause the free/busy information to become corrupted.
I developed this script to query the resource calendars and not conflict with the free/busy info. This latest version uses WebDAV to query the calendar (similar to if the user is accessing via OWA!). I have scheduled the script to run every 15 minutes using a Windows Scheduled Task. To export a calendar with 270 items take around 5 seconds.
To configure the script:

  1. Create your conference room mailboxes and configure for the AAA to run
  2. Create a service account in your environment. It will need to have an Exchange Mailbox associated with it.
  3. Via Outlook, grant the service account "REVIEWER" permissions to the conference room calendar. (Right-click | Properties | Permissions | Add...)
  4. On your IIS server, create a new shared folder, and grant the Service Account - Full Control permissions to this folder.
  5. Download and extract the ZIP file below to a server in your environment.
    • (11/21/2005) I've just uploaded an update to the script. This version includes a settings file, which eliminates the code modifcation listed below. Extract this file to the same folder, and modify the corresponding SETTINGS.TXT file.

    Note: Try to avoid putting system variables (? * and \) in the Display Name field.

  6. Double-click on the VBS file and see if it successfully generates the HTML file in the UNC path. If not, verify permissions to the folder. Keep troubleshooting until the HTML file is successful generated.
  7. Now that you have an HTML file, modify the INDEX.HTML file to point to this new resource.
    We've broken our rooms into physical areas.
    Copy the code below for each area, then modify the
    a href=
    tags to point to each resource.


    Building A


    The JavaScript code will make it into a collapsible tree only showing resources they are looking for.

  8. On the server where you placed the VBS file, create a scheduled task to run the VBScript. This scheduled task should run at regular intervals. I've scheduled them to run, Daily starting at 6AM, repeating every 15 minutes, for 15 hours.
  9. Create a virtual web site, referring to the Index.HTML file, and advertise it to your employees.
  10. You can hide the service account mailbox until you need to add a new conference room to the web page.

That's it! Fairly easy to create and maintain. In a future release I plan to have the script update the local Application Event Logs. This would allow products like NetIQ to monitor the script. Currently a log file is written locally.
Let me know what you think. I won't provide any support for your environment, but can help troubleshoot via e-mail.
6-20-2006: Finally uploaded revision including modification for day-light savings. This version reads the Day light settings from the local server, calculates the difference from DLS and modifies the time accordingly. This has been tested on Exchange 2003 successfully in Pacfic Time Zone.

Revision 3.3: If you downloaded the script, check out my article on the changes to Daylight Savings calculation here in the US. It may cause problems with entries booked on your calendar.



Someone recently reported to me that they could not access the ZIP files. Here is a dump of the "WebCalendarHTML.ZIP" file. Copy and paste this to a .VBS file on the server you wish to run the script from.

'==========================================================================
' NAME: WebCalendarHTML.VBS
' AUTHOR: Eric Woodford - mailto:Eric@EricWoodford.com
' DATE: 11/11/2005
'
' COMMENT: Generates a HTML file containing calendar entries for the next 7 months (no FREE type meetings).
' : Uses WebDAV to connect to mailbox and pull data.
' SOURCE: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/wss/wss...
' http://msdn2.microsoft.com/en-us/library/ms879286.aspx
' SOURCE: http://www.alistapart.com/articles/zebratables/
'
' -------------------------------------------------------------------------
' Revisions to 3.2:
' Found issue with meetings occuring after Daylight Savings.
' Added 4 new functions to process DST information
' Modifying script to use WMI to pull DST information
' is server currently participating in DST (DSTEnabled)
' is server currently in DST mode (isDST)
' DST Offset in minutes (offsetmin)
' Adding code to calculate time based on difference to DST Date.
' Calculates DSTStartDate and DSTEndDate based on First Sunday / Last Sunday rule.
' -------------------------------------------------------------------------
' Revisions to 3.1:
' Modified script to read from a SETTINGS.TXT file
' File can contain blank lines & comments
' SETTINGS.TXT should contain:
' FSUSERNAME=User name to map a drive to web server
' FSPASSWORD=Password to map network drive
' MAPDRIVEPATH=Open drive letter to use for script.
' WEBUNCEPATH=UNC Path to network share (like \\server\sharename)
' MAILBOX=servername(tab)account(tab)password(tab)mailbox alias(tab)Mailbox Display name
' MAILBOX=servername(tab)account(tab)password(tab)mailbox alias 2(tab)Mailbox Display name 2
' -------------------------------------------------------------------------
' Revisions to 3.0:
' Converted from Exchange 5.5 Event Script to use CalDAV
' Modified script to utilize CSS for table back ground colors vs BGCOLOR code, encoded in script
' Modified script to utilize mouseover and css to highlight active row, and make it clickable. Clicking turns composes new message.
' -------------------------------------------------------------------------

Option Explicit

Main

Sub Main()
' This sub-routine processes the Settings.txt file and generates a calendar for each entry.
' It will fail out if it reaches a mailbox entry, before all the drive mapping entries are not defined.
Dim strUser, strDevice, strUncPath, strPassword
Dim objNetwork, objFile
Dim fso
Dim strReadLine
Dim arrLine,arrMailbox
Dim intMBXCount
Dim blnMapDrive, blnGotMailbox
Dim intLLevel

intMBXCount=0
blnMapDrive = False
Set fso = CreateObject("Scripting.FileSystemObject")
Set objNetwork = CreateObject("WScript.Network")
Set objFile = fso.OpenTextFile("settings.txt",1) ' for reading

'generate log file & place in current directory.
GenLogFile CStr(Now()) & " Starting Processing"

Do Until objFile.AtEndOfStream
if blnGotMailbox Then
genlogfile CStr(Now()) & "Error: Not enough parameters completed before first MAILBOX parameter." & vbcrlf & "Need to map Drive first."
genlogfile "SETTINGS.TXT should contain:"
genlogfile vbtab & "FSUSERNAME=User name to map a drive to web server"
genlogfile vbtab & "FSPASSWORD=Password to map network drive"
genlogfile vbtab & "MAPDRIVEPATH=Open drive letter to use for script."
genlogfile vbtab & "WEBUNCEPATH=UNC Path to network share (like \\server\sharename)"
genlogfile vbtab & "MAILBOX=servername(tab)account(tab)password(tab)mailbox alias(tab)Mailbox Display name"
Exit Do
End if

strReadLine = trim(objFile.ReadLine)
If (Left(strReadLine,1)<> "'") and (len(strReadLine)>0) then
arrLine = Split(strReadLine,"=")
Select Case ucase(arrLine(0))
Case "FSUSERNAME"
strUser = trim(arrLine(1))
Case "FSPASSWORD"
strPassword = trim(arrLine(1))
Case "MAPDRIVEPATH"
strDevice = trim(arrLine(1))
Case "WEBUNCPATH"
strUncPath=trim(arrLine(1))
Case "LOGGINGLEVEL"
intLLevel=cint(trim(arrLine(1))) '0 = none, 1=basic, 2=verbose
Case "MAILBOX"
arrMailbox=Split(arrLine(1),vbtab)
blnGotMailbox = true
End Select
if Not(blnMapDrive) then blnMapDrive = mapDrive(fso, objNetwork, strUser, strPassword, strDevice, strUncPath)
If blnMapDrive And blnGotMailbox Then
GenerateHTML arrMailbox(0), arrMailbox(1), arrMailbox(2), arrMailbox(3), arrMailbox(4), strDevice, intLLevel
blnGotMailbox = false
End if
End If
Loop
GenLogFile CStr(Now()) & " Completed Processing"
objNetwork.removenetworkdrive strDevice

'cleanup log files
Dim strFolder
Dim objFolder
Dim colfiles
Dim ext
'Dim objFile

strFolder = ".\"
Set objFolder = FSO.GetFolder(strFolder)
Set colFiles = objFolder.Files
For Each objFile In colFiles
ext = right(objfile.path,3)
If (DateDiff("d",objFile.DateLastModified,Now()) > 30) And (ext="log") Then
GenLogFile "Cleanup: Deleted "&objFile.path
fso.deletefile(objfile.path)
End If
Next

Set objfile = nothing
Set objNetwork = nothing
Set fso=Nothing
End Sub

Sub GetTimeOffSet(TimeZone, isDST, DSTEnabled, llevel)
' NOTES: This routine returns the time-zone off-set (in minutes) for the computer running the script.
' The information is pulled from the system using WMI instead of a registry pull.
'
' SOURCE: http://www.windowsitpro.com/Articles/Index.cfm?ArticleID=26030&DisplayTa...
Dim objOS
Dim curTZ
Dim CurDST
for each objOs in getobject("winmgmts:").InstancesOf("Win32_ComputerSystem")
curTZ = objOs.CurrentTimeZone
If llevel = 2 Then GenLogFile "Current Time Zone:" & curTZ
curDST = objOs.DayLightinEffect
If llevel = 2 Then GenLogFile "Currently DST:" & curDST
DSTEnabled = objOS.EnableDaylightSavingsTime
If llevel = 2 Then GenLogFile "Daylight Savings Enabled:" & DSTEnabled
If IsNull(DSTEnabled) Then DSTEnabled = False
isDST = CurDST
Next
'if curDST then
' curTZ = curTZ - 60
'end if
Set objOS = Nothing
TimeZone = CurTZ
End Sub

Function GetDSTEndDate(llevel)
' In the US, Daily Savings Ends at 2AM on the First Sunday in November.
' This function is to determine the next date.
Dim DSTYear
Dim DSTDate
DSTYear = CStr(Year(Now))
DSTDate = dateadd("d",(8-weekday(cdate("11/1/"&DSTYear))),cdate("11/1/"&DSTYear & " 02:00:00"))
If now() > DSTDate
DSTYear = CStr(Year(DateAdd("yyyy",1,Now)))
DSTDate = dateadd("d",(8-weekday(cdate("11/1/"&DSTYear))),cdate("11/1/"&DSTYear & " 02:00:00"))
End If
If llevel >0 Then GenLogFile "Daylight savings ends:" &DSTDate
GetDSTEndDate = DSTDate
End Function

Function GetDSTStartDate(llevel)
' In the US, Daily Savings starts at 2AM on the Second Sunday of March.
' This function is to determine the next date.
Dim DSTYear
Dim DSTDate
DSTYear = CStr(Year(Now))
DSTDate = dateAdd("d",7,dateadd("d",(8-weekday(cdate("3/1/"&DSTYear))),cdate("3/1/"&DSTYear & " 02:00:00")))
If now() > DSTDate
DSTYear = CStr(Year(DateAdd("yyyy",1,Now)))
DSTDate = dateAdd("d",7,dateadd("d",(8-weekday(cdate("3/1/"&DSTYear))),cdate("3/1/"&DSTYear & " 02:00:00")))
End if
If llevel >0 Then GenLogFile "Next Daylight savings is:" &DSTDate
GetDSTStartDate = DSTDate
End Function

Function AdjustForDST (isDST, DSTStart, DSTEnd, meetingTime, llevel )
' Adjusts for meetings that happen on other side of DST boundary than current time.
If Not(IsDST) And (meetingTime >= DSTStart) And (meetingTime < DSTEnd) Then
' Now() is before April, but dtmStart is before Last of October
If llevel >0 Then GenLogFile "Adjusting meeting forward 60 minutes"
meetingTime = DateAdd("n",60,meetingTime)
ElseIf IsDST And (meetingTime < DSTStart) and (meetingTime >= DSTEnd) Then
' Now is after April, but dtmStart is after End of October
If llevel >0 Then GenLogFile "Adjusting meeting backward 60 minutes"
meetingTime = DateAdd("n",-60,meetingTime)
Else
If llevel >0 Then GenLogFile "No adjustment made to meeting"
End If
AdjustForDST = meetingTime
End Function

Function mapDrive(fso, objNetwork, strUser,strPassword,strDevice,strUncPath)
' Maps network drive if has all four parameters - read from settings file.
mapDrive = false
If strUser = " Then Exit Function
If strpassword = " Then Exit Function
If strDevice = " Then Exit Function
If strUNCPath = " Then Exit Function

'remove mapping for drives that already exist.
If fso.DriveExists(strDevice) Then objNetwork.removenetworkdrive strDevice
Set objNetwork = CreateObject("WScript.Network")
objNetwork.MapNetworkDrive strDevice, strUncPath, false, strUser, strPassword
strPassword = vbEmpty
strUncPath = strDevice
If fso.DriveExists(strDevice) Then mapDrive=True
End Function

Sub GenerateHTML(mailboxserver, userAcct, userpswd, mailboxalias, MailboxDisplayname, strDevice, LLevel)
' Reads mailbox information and outputs to HTML webpage as defined by DisplayName and UNC path.
' Mailbox Server: NetBIOS name of the server
' UserAcct: AD account, with (at least) REVIEWER level permissions to the mailbox. "domain\account"
' UserPswd: Password for UserAcct
' MailboxAlias: Either the account name or the SMTP address for the resource calendar to be read.
' MailboxDisplayName: Display Name to put at top of generated Webpage.
' strDevice: Name of drive letter mapped for the web content

' Variables.
Dim strCalendarURI ' As String
Dim reqDoc ' As Msxml2.DOMDocument
Dim resDoc ' As Msxml2.DOMDocument
Dim pi ' As IXMLDOMProcessingInstruction
Dim strPassword ' As String
Dim strUserName ' As String
Dim searchrequestNode ' As IXMLDOMNode
Dim sqlNode ' As IXMLDOMNode
Dim strQuery ' As String
Dim queryNode ' As IXMLDOMText
Dim req ' As MSXML2.XMLHTTP
Dim objSubjectNodeList ' As IXMLDOMNodeList
Dim objStartTimeNodeList ' As IXMLDOMNodeList
Dim objEndTimeNodeList ' As IXMLDOMNodeList
Dim objBusyStatusNodeList ' As IXMLDOMNodeList
Dim objInstanceTypeNodeList ' As IXMLDOMNodeList
Dim objOrganizerNodeList ' As IXMLDOMNodeList
Dim objNode ' As IXMLDOMNodt
Dim i ' As Integer
Dim strInstanceType ' As String
Dim strDateNow ' As string
Dim strDateEnd
Dim StrDate ' used as temporary variable. Reads from NODE and processes into
dim myCalendar ' array for building side calendar
dim counter ' # of meetings found in calendar
dim WebHeader ' Contains Header to put in Item Count at end of building form
dim WebContent ' string container for web page content
dim bgcolor ' place holder for bgcolor tag in code
dim strOrganizer ' organizer for meeting string holder
dim pdm,pdd ' Values for previous month, day - transitition on colors
dim cdm, cdd, cdy ' current working day, month values
Dim edd, edm ' end date day value
Dim objFileSystem ' connect to fso to create file
dim objFile ' current file connection
dim colorstep ' container for current color value on table
dim dtmStart ' DTM container for Start Date
dim dtmEnd ' DTM container for End Date
dim strSubject ' Subject of current calender item
dim strStartTime, strEndTime ' formatted time string value for HTML output.
dim x ' variable for place holders..
dim dow ' day of week for when to start new week
dim workingdate ' start of week date for buidling small selection calendar, = Saturday of current week
dim monthstr ' 3 letter value for current month JAN, FEB, MAR, etc. small calendar
dim ConferenceRoomName ' conference room name for file output.
dim strDayOfWeek ' String Value for Day of Week.
Dim strDayOfWeekEnd ' String Value for Day of Week for End Date
Dim blnAllDayEvent ' int value for determining where in multiday event 0= not, 1=start, 2=middle/end
Dim strCurrentUser ' resource mailbox name.
Dim oshell ' connect to system for time-offset for daylight savings
Dim atb ' registry key containing value
Dim offsetmin ' time off-set in minutes from daylight savings.
Dim IsDST, DSTEnabled
Dim DSTStartDate
Dim DSTEndDate

'MsgBox TimeZoneOffSet & isdst

'const strUNCPath="\\sacwebsrv001\ConferenceRoomReservations\" ' Path to folder containing web pages. INDEX should point to reside here
Const strUncPath="z:\"

Dim ScriptVersion ' string variable to put value into HTML
ScriptVersion = "3.2 WebDAV"
'genlogfile "Starting "& Mailboxdisplayname&" : "& Now()
' Initialize variables.
strUserName = useracct
strPassword = userpswd
strCalendarURI = "http://"&mailboxserver&"/exchange/"&mailboxalias&"/calendar/"
strCurrentUser = mailboxdisplayname
ConferenceRoomName = RemoveSpaces(strCurrentUser, LLevel) ' remove wild-cards and spaces for filenames

GetTimeOffSet offsetMin, IsDST, DSTEnabled, LLevel
DSTStartDate = GetDSTStartDate(llevel)
DSTEndDate = GetDSTEndD