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!
AttachmentSize
GoogleSearch.gadget4.29 KB

Better Warcraft Gear Script Version 2

Of course, you make one thing, then people come up with something better. That same guildie who is working on my robe, came back and said, "Why don't you just read the ingredients directly from Wowhead?".

OK, easy enough. Read from Wowhead, get reagents, recurse via loop. Two sources came in extremely handy for this. First WWoIT provided several excellent examples for reading from a web page, then Dan Sullivan provides this excellent XML in Powershell examples.

The final result is a bit more versatile than the last script.

Please Enter the WowHead ItemID (found in the URL)?: 42101
Ebonweave Robe x 1
  Ebonweave x 8
  Bolt of Imbued Frostweave x 8
   Bolt of Frostweave x 16
    Frostweave Cloth x 80
  Infinite Dust x 16
  Eternal Shadow x 16
 Bolt of Imbued Frostweave x 6
  Bolt of Frostweave x 12
   Frostweave Cloth x 60
  Infinite Dust x 12
 Eternium Thread x 1
 Frozen Orb x 1

So, here's the new and improved script:

function Read-URL ($url) {
#source: <a href="http://waynes-world-it.blogspot.com/2008/05/reading-web-content-with-powershell.html
" title="http://waynes-world-it.blogspot.com/2008/05/reading-web-content-with-powershell.html
">http://waynes-world-it.blogspot.com/2008/05/reading-web-content-with-pow...</a>       $col = new-object System.Collections.Specialized.NameValueCollection
        $col.Add("
a","stats")
        $col.Add("
s","s451qaz2WSX")
        $wc = new-object system.net.WebClient
        $wc.proxy = $proxy
        $wc.QueryString = $col
        $webpage = $wc.DownloadData($url)
        $string = [System.Text.Encoding]::ASCII.GetString($webpage)
        return $String
}

function get-ingredientfor([string]$item, [int]$multi) {
        $x +=4
        #$Needed = $csv | ?{$_.item -eq $item} 
        [xml] $needed = Read-URL($url+ $item + $strXML)
        Write-Host ("
" * $x) $needed.wowhead.item.name.psbase.innertext "x" $multi
        $reagents = $needed.wowhead.item.createdBy.spell.reagent
        if ($reagents -ne $null) {             
                $reagents | % {                        
                        if ($_.id -ne "
") {
                                get-ingredientfor $_.id ($multi * [int]$_.count)
                        } else {                                                               
                                #Write-Host ("
" * $x) "-" $needed.wowhead.item.class.psbase.innertext
                        }
                }
        } else {       
                #Write-Host ("
" * $x) "-" $needed.wowhead.item.class.psbase.innertext
        }
}

$x = -4
$url = "
http://www1.wowhead.com/?item="
#$itemCode = "
42101"
$strXML = "
&xml"
cls
Write-Host "
Beta - PowerShell World of Warcraft Ingredients List"
Write-Host "
This code will read the WowHead website and process all the ingredients necessary to make something."
Write-Host "
----------------------------------------------------------"
$ItemCode = Read-Host "
Please Enter the WowHead ItemID (found in the URL)?"

if ($itemcode -eq $null) {Throw "
Please enter an itemID to continue"}
$test = Read-URL($url+ $itemCode + $strXML)
if ($test -match "
not found") {Throw "Item not found in WowHead"}

get-ingredientfor $itemCode 1

Better Warcraft Gear through Powershell Recurrsion

My warlock has reached 80. He has just about all the regular quest rewards that one can get through soloing the area. Now comes the long haul of playing the more advanced group areas of the game, except even my best quested gear wasn't cutting it for some of the lowest heroic dungeons.

So, using a combination of MaxDPS and the Gear Wishlist, I determined that the best tailored outfit would be the Ebonweave Robe. This tailored purple item would be a great step up in the current item I have equipped.

Unfortunately for one of my guilde tailors to make this for me is going to require some serious materials "mats". Just exactly what it would require, I wasn't so sure. So, knowing this is possible via a script, I developed the following. It reads a simple list of ingredients from a CSV (attached below). It then takes the ingredients of those ingredients (Bolt of Imbued Frostweave = requires infinite dust and a bolt of frostweave, etc.). It then creates a list of the raw ingredients to make this robe.

In case your are curious, they are:
Raw materials for Ebonweave Robe

  • Frostweave x 60
  • Infinite Dust x 12
  • Frostweave x 80
  • Infinite Dust x 16
  • Eternal Shadow x 16
  • Eternium Thread x 1
  • Frozen Orb x 1

function get-ingredientfor([string]$item, [int]$multi) {       
        $Needed = $csv | ?{$_.item -eq $item}  
        if ($needed -ne $null) {
                $needed | % {                  
                        if ($_.ingredient -ne "") {get-ingredientfor $_.ingredient ($multi * $_.count)}
                        else {                         
                                Write-Host $item "x" $multi
                        }
                }
        } else {
                #return $item
                Write-Host $item "x" $multi
        }
}


$csv = Import-Csv -path c:\ebonweaverobe.csv
$Wish = "Ebonweave Robe"
Write-Host "Raw materials for" $wish
get-ingredientfor $Wish 1

AttachmentSize
ebonweaverobe.csv435 bytes

WoW Wallpaper Grabber Version 2

This updated version of the code will pull down the latest image from WorldofWarcraft.com from either their Blizzard images or the new Fan Art section randomly.

Recent Updates to code:

  • July 9, 2010 - Added code to randomly pull from any image on page.
  • July 22, 2010 - Added code to check what images were available last time it checked the Blizzard images, then download the newest if one is available. No changes - randomly between all Blizz images.Removing code and placing into ZIP attachment. Code appears to be causing formatting issues on later pages.
AttachmentSize
getWoWBkgd.zip1.59 KB

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..

AttachmentSize
signature.zip6.76 KB

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!

AttachmentSize
My_Background.zip3.96 KB

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.

AttachmentSize
House Payment Estimator.zip4.59 KB

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.

AttachmentSize
Configuring Outlook AutoArchiving Options.pdf121.74 KB

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: <a href="http://forums.vandyke.com/showthread.php?t=597
'
" title="http://forums.vandyke.com/showthread.php?t=597
'">http://forums.vandyke.com/showthread.php?t=597
'
</a>  mailto: <a href="http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=2149800&SiteID=1
'==========================================================================

Set"
title="http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=2149800&SiteID=1
'==========================================================================

Set"
>http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=2149800&SiteID=1
'...</a> 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

Set Default Outlook Address Book via Script

The default Outlook Address Book is saved in the Windows registry. I've created this script to be used to push down a new Address Book view at logon for a large group of users. The script will read the current default Outlook profile for the currently logged on user, then push down the address book view you specify.

To configure the script:

  1. Set your Outlook Address Book to the view you want to set on the remote workstation. (Tools - Address Book - Tools - Options - Set show this address list first).
  2. Export the registry key for this Outlook profile.
     
    [HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\Windows Messaging Subsystem\Profiles\(your default Outlook profile)\9207f3e0a3b11019908b08002b2a56c2]
  3. Open the .REG file and copy the HEX code for "01023d06". For example:
     00,00,00,00,dc,a7,40,c8,c0,42,10,1a,b4,b9,08,00,2b,2f,e1,82,01
  4. Edit the Set-DefaultABView.VBS file (see attached) line to use your hex code. Make sure to leave in the double-quotes.
     Const DestABUsers =  "00,00,00,00,dc,a7,40,c8,c0,42,10,1a,b4,b9,08,00,2b,2f,e1,82,01"
  5. Save, and test drive.

Now, you can push it out as part of your logon scripts?? or add it to the RunOnce settings on your Windows image.

:)

Have Fun!

AttachmentSize
set-DefaultABView.zip2.42 KB

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.

AttachmentSize
Removable Media Synchronizer2.zip5.82 KB

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 <number>;
        Mon, 29 Jan 2007 13:55:16 -0800 (PST)
3: Return-Path: <alice@a.org>
4: Received: from a.org (a.org [123.45.67.89])
        by b.org with ESMTP id  <number number number>;
        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 <a@a.org>
9: To: Bob <bob@b.org>

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.

      <select onChange="" name=ClassesPulldown>
        <option value="'LDAP://dc=corp,dc=ent'" >All OUs
        <option value="'LDAP://ou=users,dc=corp,dc=ent'" >Users OU Only  
      </select>

    • 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.

AttachmentSize
User Export Report.zip3.72 KB
User Export Report 2.zip4.7 KB

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..

AttachmentSize
SetNewDLManager.hta18.81 KB

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.

AttachmentSize
SetDelegatePerms.hta12.8 KB

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

Find an email address in your environment

In my environment, I am always looking up a user's email address, or trying to find out what Exchange server their mailbox is on. That's why I developed the attached script.

It will do a wild-card search against your currently logged on domain to find any account with those values. It searches the Displayname, proxyaddresses and mail fields.

AttachmentSize
FindEmail.zip7.03 KB

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.

AttachmentSize
Find_Duplicates_in_File_v2.zip2.64 KB

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 - <a href="mailto:EricWoodford@gmail.com">EricWoodford@gmail.com</a>
'
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&"<option value=""LDAP://dc=local,dc=corp"" >All OUs"
strHTML=strhtml&"<option value=""LDAP://ou=sub,dc=local,dc=corp"" >This SUB OU Only"

line 55 & 56

strHTML=strhtml&"<option value=""LDAP://dc=local,dc=corp"" >All OUs"
strHTML=strhtml&"<option value=""LDAP://ou=sub,dc=local,dc=corp"" >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.

AttachmentSize
DL Export Report.zip6.3 KB

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=<Exchange Server Name>
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=<Organization Name>/ou=First Administrative Group/cn=Recipients/cn=<MailboxAlias>
/o=<Organization Name>/ou=First Administrative Group/cn=Recipients/cn=<MailboxAlias2>

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 - <a href="http://www.eggheadcafe.com/forumarchives/scriptingVisualBasicscript/Aug2005/post23622005.asp
'=====================================================

Const"
title="http://www.eggheadcafe.com/forumarchives/scriptingVisualBasicscript/Aug2005/post23622005.asp
'=====================================================

Const"
>http://www.eggheadcafe.com/forumarchives/scriptingVisualBasicscript/Aug2...</a> 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: <a href="http://www.devguru.com/Technologies/wsh/quickref/wshnetwork_EnumPrinterConnections.html
'
" title="http://www.devguru.com/Technologies/wsh/quickref/wshnetwork_EnumPrinterConnections.html
'">http://www.devguru.com/Technologies/wsh/quickref/wshnetwork_EnumPrinterC...</a> 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

AttachmentSize
enum printers.zip699 bytes

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

AttachmentSize
EnumPrinters.vbs_.txt6.87 KB

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.

AttachmentSize
Executive_MBX_Access.zip1.69 KB

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 &gt;= 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 &gt;= 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

AttachmentSize
WebCalendarHTML.zip10.38 KB
ConferenceRoomWebsite.zip7.93 KB
settings.txt831 bytes

Microsoft Auto-Accept Agent Accepts Anything!

Microsoft has an Auto-Accept Agent (or AAA) that can be used on Exchange 2003 servers to process incoming meeting requests to resource mailboxes. Simply send the resource an invitation and within 10-30 seconds, the room will respond back with an acceptance or denial of your request.

For the last 2 months we've been using this script to process the 61 various resources in the environment (conference rooms, laptops and LCD projectors). Combine that with my own home-brewed calendar export script, the company has a complete resource tool for virtually all items in the environment. Unfortunately over the last few weeks, we've had new issues.

Corrupted CDO Free/Busy Cache: First we had users reporting getting declined responses when the calendar shown free, and canceled meetings were still showing as busy. Why? My best results were pulled from a technical article (kb886760). Now, if you read this article VERY closely, pay attention to:

Hotfix installation information
After users apply this hotfix, they may have to wait until the end of the month before they stop experiencing the symptoms that are described in the ""Symptoms"" section. This is because the EXCDO component keeps free/busy information cached until the last day of the month. The free/busy cache is rebuilt on the first day of the month.

GREAT!! So end-users will need to wait until the end of the month to view the correct free/busy information on our resource calendars as the CDO free/busy cache is only completely rebuilt once a month. There has to be a better way...

After talking with Microsoft PS Support, I got this answer..

  1. Set the CalendarRecovery registry value to 1 on the Exchange 2003 computer. To do this, follow these steps:
    • Click Start, click Run, type regedit in the Open box, and then click OK.
    • Locate and then click the following registry subkey:
        HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\EXCDO\Parameters<br />
    • On the Edit menu, click Add Key, type EXCDO, and then press ENTER.
    • On the Edit menu, click Add Key, type Parameters, and then press ENTER.
    • On the Edit menu, point to New, click DWORD Value, type CalendarRecovery, and then press ENTER.
    • Double-click CalendarRecovery, type 1 in the Value data box, click Decimal, and then click OK.
    • Quit Registry Editor.
  2. Start Outlook on a client computer and log on by using the resource mailbox profile.
  3. Right-click the Calendar folder, and then click Properties.
  4. Click the General tab, type CleanupExpansionCachesInTheCalendarFolder in the Description box, and then click OK.

Notes

  • If the Description box already includes CleanupExpansionCachesInTheCalendarFolder, clear the Description box, click Apply, type CleanupExpansionCachesInTheCalendarFolder in the Description box, and then click OK.
  • After you complete steps 1 through 4, you may have to run Outlook with the /CleanFreeBusy command-line switch. This will update the free/busy information. To do this, click Start, click Run, type Outlook.exe /cleanfreebusy , and then click OK.

I needed to touch every mailbox, so it was (going to be) quite a tedious task of creating a profile and opening each mailbox. Luckily, I do have a script also for that.

Alright, that problem solved. On to the next one..

Faulty Replication: At the same time the free/busy information was not replicating, users were reporting other issues with the calendars. This fix was applied 891509.

Unplanned for behavior: OK, patched and meetings are processing. Now meetings are booking, and the calendars look really good. What do we find out? That the, wait, really, you won't believe it. Meeting requests booked on the calendar as FREE, are actually booked on the calendar as FREE. Hu? What? Users can successfully book the conference room, get an acceptance back from the room and still be booked over by other requesters who book the room as "tentative", "BUSY" or "Out of Office".

According to our Microsoft TAM, this functionality is by design.

A customer requested functionality to send a 'free' request to resources to get a accepted/denied response back, then the end-user would use other means to officially book the room.

So, now we are in the uncomfortable place of telling our users that the conference rooms will accept your meeting request marked "FREE" and very likely you will get booked over. (Don't change it to FREE later on either, it has the same effect.) I still wish we went with SWINC's product Exchange Resource Manager or ERM as it was fully configurable. it would have accepted those "FREE" requests and converted them to "BUSY" with a simple click of a check box.

So much for saving $$!

Saving More Daylight!!

Thanks to a recent change, Americans will soon be getting more daylight. Yes, we will start saving daylight approximately 3 weeks earlier this year. Previously, Daylight Savings started the first Sunday in April, now it will be the second Sunday in March!

What does that mean for IT? Nothing, and a bunch. First Outlook bases DST calculations on the local operating systems settings. So, if you book a meeting at 10AM PST after March 11th, 2007, it will actually be an hour off. Why? The calendar stores it stores it in Greenwhich Mean Time (GMT). (10AM + 8hrs for GMT = 6PM). When in actuallity (because it's after DST starts), that meeting should be on the calendar for 5PM GMT. In addition, all applications that use straight CDO will have issues (like my script and Blackberry servers).

Nicely, Microsoft has provideed a web page specifically addressing these issues. (see here)

To fix my HTML Generating Script, search for these two functions Function GetDSTStartDate(llevel) and Function GetDSTEndDate(llevel) and replace it with this code:

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 then
        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 then
        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

Hopefully, we all get to enjoy a bit more of this saved up daylight by playing outside. I declare from this date forward that 3PM is beginning of recess. Now just need to decide what time zone that's in....

PowerShell - Enumerate SENDAS Permissions on all mailboxes

We are working on a security review of our Exchange environment. Part of that is to review who has permissions to access what mailboxes.

That's why I put together this little script. Using the Exchange 2007 Management shell, it reads our Exchange 2003 and 2007 environments, processing all the mailboxes. It outputs a single CSV file containing each instance where someone else (not SELF and not a SID) has permissions to a mailbox.

The heart of the script is this line of code. It is the one-liner that reads the mailbox information and filters on the rights I wanted. Without the -and and -notlike operators, I would need to filter through all of the various account permissions.

get-mailbox $thisuserDN | get-adpermission | select user, extendedrights | where {($_.extendedrights -like 'send-as') -and ($_.user -notlike '*SELF*') -and ($_.user -notlike '*S-1-5*')}

Note: It will prompt for the OU of the folder you want. If you don't specify anything and simply hit ENTER, it will use the root of your currently logged on domain.

AttachmentSize
EnumSendAsRights.ps12.17 KB

Create mailboxes from a CSV file

Takes a CSV file, reads headers and creates mailbox based on values.
Requires two columns in CSV, UserPrincipalname and Name. The UPN can be the SAMAccount value renamed. It will append the variable $UPNDomain to end.

Also requires both Quest ActiveRoles PowerShell tools (to create AD Account) and Exchange 2007 Management tools to set mailbox quota limits.

#CreateMailboxFromCSV.PS1
$file = "c:\List_Of-mailboxes.csv"
$Container = "ou=EricTest,ou=Mailbox Customers,dc=corp,dc=ent"
$UPNDomain = "@example.com"
$strMailServerName = '/o=CA/ou=First Administrative Group/cn=Configuration/cn=Servers/cn=EXCHSrvr1'
$strDatabaseName = 'CN=SG1-PRIV1,CN=SG1,CN=InformationStore,CN=EXCHSrvr1,CN=Servers,CN=First Administrative Group,CN=Administrative Groups,CN=CA,CN=Microsoft Exchange,CN=Services,CN=Configuration,dc=corp,dc=ent'

[array]$csv = import-csv $file

# This script implies that columns "UserPrincipalName", "name" exist in the csv file

if (($csv[0].UserPrincipalName -eq $null) -or ($csv[0].Name -eq $null) -or ($csv[0].Database -eq $null) ) {Throw “Parameter missing… Make sure the CSV file has the following columns: UserPrincipalName, Name, Database, OrganizationalUnit.”}

# Create collection of the commands that we will invoke in the end
[collections.arraylist]$Commands = new-object system.collections.arraylist

for($i = 0; $i -lt $csv.Count; $i ++ ) {
        if ($csv[$i].UserPrincipalName.contains("@")) {
                [void]$Commands.Add($objNewUser =' + ” new-qaduser -UserPrincipalName `”$($csv[$i].UserPrincipalName)`” -Name `”$($csv[$i].Name)`” -ParentContainer `”$($Container)`”" + ‘ -Password $pwd ‘)
        } else {
                [void]$Commands.Add(‘$objNewUser ='
+ ” new-qaduser -UserPrincipalName `”$($csv[$i].UserPrincipalName+$UPNDomain)`” -Name `”$($csv[$i].Name)`” -ParentContainer `”$($Container)`”" + ‘ -Password $pwd ‘)
        }
}

# Add other parameters if present in the CSV

if ($csv[0].Alias -ne $null) {
        for($i = 0; $i -lt $csv.Count; $i ++ ) {
                $Commands[$i] = $Commands[$i] + ” -Alias `”$($csv[$i].Alias)`”"

        }
}

if ($csv[0].DisplayName -ne $null) {
        for($i = 0; $i -lt $csv.Count; $i ++ ) {
                #clean up display name value of extra characters
                $DNameStr = $csv[$i].DisplayName.Replace('`','')
                #write-host $dnamestr
                $Commands[$i] = $Commands[$i] + ” -displayname `”$($DNameStr)`”"
        }
}


if (($csv[0].FirstName -ne $null) -or ($csv[0].GivenName -ne $null)) {
        for($i = 0; $i -lt $csv.Count; $i ++ ) {
                if ($csv[0].FirstName -ne $null) {
                        $Commands[$i] = $Commands[$i] + ” -FirstName `”$($csv[$i].FirstName)`”"
                } else {
                        $Commands[$i] = $Commands[$i] + ” -FirstName `”$($csv[$i].GivenName)`”"
                }
        }
}


if (($csv[0].LastName -ne $null) -or ($csv[0].sn -ne $null)) {
        for($i = 0; $i -lt $csv.Count; $i ++ ) {
                if ($csv[0].LastName -ne $null) {
                        $Commands[$i] = $Commands[$i] + ” -LastName `”$($csv[$i].LastName)`”"
                } else {
                        $Commands[$i] = $Commands[$i] + ” -LastName `”$($csv[$i].sn)`”"
                }
        }
}


if ($csv[0].Initials -ne $null) {
        for($i = 0; $i -lt $csv.Count; $i ++ ) {
                $Commands[$i] = $Commands[$i] + ” -Initials `”$($csv[$i].Initials)`”"
        }
}

if ($csv[0].telephonenumber -ne $null) {
        for($i = 0; $i -lt $csv.Count; $i ++ ) {
                $Commands[$i] = $Commands[$i] + ” -PhoneNumber `”$($csv[$i].telephonenumber)`”"
        }
}

if ($csv[0].faxnumber -ne $null) {
        for($i = 0; $i -lt $csv.Count; $i ++ ) {
                $Commands[$i] = $Commands[$i] + ” -fax `”$($csv[$i].faxnumber)`”"
        }
}

if ($csv[0].department -ne $null) {
        for($i = 0; $i -lt $csv.Count; $i ++ ) {
                $Commands[$i] = $Commands[$i] + ” -department `”$($csv[$i].department)`”"
        }
}

if (($csv[0].SamAccountName -ne $null) -or ($csv[0].UserPrincipalName -ne $null)) {
        for($i = 0; $i -lt $csv.Count; $i ++ ) {
                if ($csv[0].SamAccountName -ne $null) {
                        $strSam = $csv[$i].SamAccountName
                } else {
                        $strSam = $csv[$i].UserPrincipalName
                        $intAt = $strSam.indexof("@")
                        if ($intAT -ne -1) {
                                $strSam = $strSam.substring(0,$intAt)
                        }
                }
                $Commands[$i] = $Commands[$i] + ” -SamAccountName `”$($strSam)`”"
        }
}


for($i = 0; $i -lt $csv.Count; $i ++ ) {
        $pwd = new-object Security.SecureString

        if ($csv[0].Password -ne $null) {
                $csv[$i].Password.ToCharArray() | foreach { $pwd.AppendChar($_) }
        } else {
                #$csv[$i].UserPrincipalName.ToCharArray() | foreach { $pwd.AppendChar($_)}
                $a = new-object random
                $b = [string]$a.next()
                $B.toCharArray() |      foreach { $pwd.AppendChar($_)}
        }
        write-host $Commands[$i]           #varibable is assigned when ran.
        Invoke-Expression $Commands[$i]
               
        ##http://www.powergui.org/thread.jspa?messageID=11753&#11753
        $LastMbx = $objNewuser.DN
        $ldapquery = "LDAP://" + $objNewuser.DN
        $newmbx = [ADSI]$ldapquery
        $newmbx.mailNickname = $csv[$i].mailnickname
        $newmbx.msExchHomeServerName = $strMailServerName
        $newmbx.homeMDB = $strDatabaseName
       
        if ($csv[0].mail -ne $null) {
                #$strEmailAddress =
        $newmbx.Mail = $csv[$i].mail
        }
       
        if ($csv[0].proxyaddresses -ne $null) {
                $strProxy = $csv[$i].proxyaddresses
                $strProxy = $strProxy.Replace(";;",";")
                $arrProxy = $strProxy.split(";")
        $newmbx.proxyaddresses = $arrProxy
        }
                       
        $newmbx.setinfo()              
}

#### If you don'
t have the Exchange 2007 management tools, Delete the rest of the script..

"Waiting for Mailbox GUID to populate"

do {
        $a = Get-QADUser $lastmbx -IncludedProperties 'msexchmailboxguid'  | select msexchmailboxguid  
        $b = [string]$a.msexchmailboxguid
} while ($b.Length -eq 0)

"I just found:" + $b

if ($csv[0].mdbQuota -ne $null) {
        for($i = 0; $i -lt $csv.Count; $i ++ ) {
                $csv[$i].UserPrincipalName
                get-mailbox $csv[$i].UserPrincipalName | Set-mailbox  –prohibitsendquota $csv[$i].mdbQuota -IssueWarningQuota $csv[$i].mbdLimit -UseDatabaseQuotaDefaults $FALSE

        }
}

Create DLs from CSV

At my current employer, we are constantly bringing on new customers. As I detailed in my VBScript version, it's a fairly complex environment.

The attached Powershell script does the following:

  1. Read CSV file of distribution list properties. (Alias, displayname, email address, etc.)
  2. Check then create each new DL
  3. Populate secondary properties on each DL like, proxy addresses, Allowed Senders, Size limits, and members.
  4. Define owner, and grant modify permissions to Associated External Account

Requirements:

  • Exchange 2007 Powershell cmdlets
  • Quest Active Roles Powershell cmdlets
  • CSV File with the following fieldsnames:
    • Alias
    • Displayname
    • PrimarySMTPAddress (can be all smtp addresses seperated by ;
    • mdbquota (in MB) (mailbox send/receive attachment limit)
    • AcceptMessagesFrom - List of names or DLs, ; seperated that can send to this DL.
    • Members - names of all recipients
AttachmentSize
Create-DLsFromCSV.zip2.1 KB

Find All Mailboxes with Mixed up Quota settings

Invariably, when you let your local admins create mailboxes using a web interface, you will get some odd settings.

One of things that I've noticed, when running PowerShelll queries, is all the mailboxes with incorrect quota settings. Users with Send Receive limits lower than the Notification settings. Tired of these alerts, I created the following to dump those mailboxes to a CSV.

Get-Mailbox -Filter "UseDatabaseQuotaDefaults -eq `$false" -ResultSize unlimited | where {($_.prohibitsendquota -lt $_.issuewarningquota) -or ($_.prohibitsendquota -gt $_.prohibitsendreceivequota)} | select servername,Displayname,name, issuewarningquota, prohibitsendquota, prohibitsendreceivequota | Export-Csv -Path "C:\mbxs_with_bad_quotas.csv"

I am hoping that someone might be able to develop a better filter statement. When I tried to use:

-filter {prohibitsendquota -lt issuewarningquota}

it would always error out.

Get-Mailbox : Cannot bind parameter 'Filter' to the target. Exception setting "Filter": "Invalid filter syntax. For a description of the filter parameter syntax see the command help.
"
prohibitsendquota -lt issuewarningquota" at position 23."
At line:1 char:20
+ Get-Mailbox -Filter  <<<< "prohibitsendquota -lt issuewarningquota"

Now.. Send the CSV to my remote admins, and ask them to help out their users.

:)

Import Remote IP Range into Exchange 2007 EDGE servers

We maintain an Exchange EDGE server on our perimeter as SMTP relay. Application servers can use this server to relay email out (to internal recipients or the Internet). To keep this server from being an open relay, we maintain a list of valid IP addresses that can relay through this server. To update the the relay, we run my script below to import the IPs. On our first attempt, we found that MS Edge 2007 - Receive Connectors have a limit on allowed RemoteIPRanges values. After approximately 1100 individual IPs, it would quit accepting any additional IP addresses. Unfortunately, we have over 1300 current application servers running currently.

Dealing with Receive Connector limits. This field accepts 3 different formats: straight IP, IP Range (0.0.0.0 - 0.0.0.1) and CIDR format (0.0.0.0/24). Since I am working from a list of allowed IP addresses, the first option is easy. CIDR format requires a bit of fiddling, as you want valid ranges and using defining a valid subnet mask is complicated. So, why not use ranges.

What my script does:

  1. Reads a text file of IP addresses and CIDR IPs.
  2. Breaks apart octets and sorts IP addresses (numerically not alphabetically)
  3. Parses IP addresses and consolidate's IP ranges with no skips
    • 10.0.0.1, 10.0.0.2, 10.0.0.3, 10.0.0.4 -> 10.0.0.1 - 10.0.0.4
  4. Updates local Receive Connector - RemoteIPRAnges value on local host.

Drawback is that if someone manually adds an IP directly to the server, it will be lost. Currently the script deletes any values already there.

Get-PSSnapin -registered | Add-PSSnapin -passThru
$ips = Get-Content c:\ip-addresses.txt

$broken = @()
$connIPRanges = @()

Function Add-OneIP($IPAddress) {
        $octets = $IPAddress.split(".")
        $Entry = new-Object -typename System.Object
        $Entry | add-Member -memberType noteProperty -name A -Value ([int]$octets[0])
        $Entry | add-Member -memberType noteProperty -name B -Value ([int]$octets[1])
        $Entry | add-Member -memberType noteProperty -name C -Value ([int]$octets[2])
        $Entry | add-Member -memberType noteProperty -name D -Value ([int]$octets[3])
        $Entry | add-Member -memberType noteProperty -name Subnet -Value $subnet
        $outstr = $octets[0] +"," +$octets[1] +"," +$octets[2] +"," +$octets[3]+ ","+$subnet | Out-File c:\ip.csv -Append
        return $entry
}


foreach ($ip in $ips) {
        if ($ip -match "/") {
                $Parts = $ip.Split("/")
                $ip = $parts[0]
                $subnet = $parts[1]
                $ipobj = Add-OneIP $IP
                $broken += $ipobj
        } elseif ($ip -match "-") {

                $Range = $ip.split("-")        
                $subnet = ""
                $StartIP = $range[0].trim().split(".")
                $EndIP = $range[1].trim().split(".")
                for ($a = [int]$StartIP[3]; $a -le $EndIP[3]; $a++) {                  
                        $IPString = [string] $StartIP[0] +"." + $StartIP[1] +"." + $StartIP[2] +"." + [string]$a                       
                        $ipobj = Add-OneIP $IPstring
                        $broken += $ipobj
                }
        } else {
                $subnet = ""
                $ipobj = Add-OneIP $IP
                $broken += $ipobj

        }      
}

$broken.count
$ipranges = $broken | Sort A, b, c, d
$last = $ipranges[0]
$null | out-file c:\ip-addresses2.txt

foreach ($ipr in $ipRanges) {
        if ($IpR.Subnet -ne "") {
                $ipstr = [string]$ipr.a+"."+[string]$ipr.b+"."+[string]$ipr.c+"."+[string]$ipr.d+"/"+$ipr.Subnet
                $ipstr | out-file c:\ip-addresses2.txt -append
                $ConnIPRanges += $ipstr
        } else {
                $value = ($ipr.a - $last.a)*1000 + ($ipr.b - $last.b)*100 + ($ipr.c - $last.c)*10 + ($ipr.d - $last.d)
               
                if ($value -eq 1) {
                        if ($Start -eq $null) {$start = $last}
                        $counter += 1
                } else {               
                        if (($start -ne $last) -and ($start -ne $null)) {
                                Write-Host $start.a $start.b $start.c $start.d - $last.a $last.b $last.c $last.d, "("$counter")"
                                $ipstr = [string]([string]$start.a+"."+[string]$start.b+"."+[string]$start.c+"."+[string]$start.d+"-"+[string]$last.a+"."+[string]$last.b+"."+[string]$last.c+"."+[string]$last.d)
                                $ipstr | out-file c:\ip-addresses2.txt -append
                                $ConnIPRanges += $ipstr
                        } else {
                                $ipstr = ([string]$last.a+"."+[string]$last.b+"."+[string]$last.c+"."+[string]$last.d)
                                $ipstr | out-file c:\ip-addresses2.txt -append
                                $ConnIPRanges += $ipstr
                                write-host $last.a $last.b $last.c $last.d
                        }
                        $counter = 0
                        $start = $null
                }
                $last = $ipr
        }
}

$conn = get-receiveconnector
$conn.remoteipranges.clear()
ForEach ($RIP in $connIPRanges) {
        $conn.remoteIPRanges.add($RIP) 
}      
$conn | Set-receiveconnector

PowerShell Power Story

I hadn't fully realized the power and simplicty of PowerShell until recently. My manager had asked if a script could be written that would take a CSV file of mailbox names, and hide them or unhide them. My co-worker, well-versed in VBScript, generated a 220 line solution that did just that. When I got into the office, I took that as an invitation to flex the PowerShell

Here is my code:

Import-csv Big-List-of-users.csv | %{get-mailbox $_.emailAddress | set-mailbox -HiddenFromAddressListsEnabled $true/$false}

Some nifty points:

  1. The value $_.emailAddress refers the the column in the CSV file of the same name.
  2. It's really easy to hide versus unhide a mailbox; simply change the $true to $false.

Note: This script utilizes the Exchange 2007 Management Shell, so you'll need to run this from a box with Exchange 2007 System Management tools on it. It does work with Exch2003 mailboxes.

Why such a difference? PowerShell has built-in functions that complete many of the basic tasks that you commonly need to do. For example the code:

import-csv (filename)

does quite a bit of work in only one bit. This reads the file, assigns variables based off the headers, and populates each record with the remainder of the file. Very nice function when you are looking to do a repetitive operation on a series of objects (like modify accounts, or query servers). In VBScript, the code would be something like:
 
set fso = createobject("scripting.filesystemobject")  ' create pointer to file system
set objFile = fso.opentextfile("filename.csv",1) '
create pointer to file

Set MyRSet = CreateObject("ADODB.Recordset") ' initialize a 2d array
If not objfile.atendofstream then  
     strFirstLine = objfile.readline  '
read first line to get headers
     arrFirstLine = split(strFirstLine,",") ' create array of header values
     For each strHeader in arrFirstLine  '
loop through array
          MyRSet.Fields.Append strHeader, adVarChar, 255 ' build records
     Next
     MyRSet.Open  '
open connection to data array created
     do while not objfile.atendofstream ' read file to populate array
          strCSVLine = objFile.readline
          arrCSVLine = split(strCSVLine,",")
          MyRSet.addnew Array(strFirstLine), arrCSVLine
     Loop
End if
set objFile = nothing

I know, the code is much more complicated than necessary for my initial project. It just goes to show how much more efficient PowerShell is for scripting. In VBScript, I would have likely done all my processing inside the file reading loop and never populated the array. My script would have been more tightly focused to that single purpose.

As I learn more about PowerShell, I become more, and more, fond of it. The versatility and simplicity are quickly winning me over and I find myself writing more PS scripts.

Query Size and Free Space of Windows Volume Mount Points

I have been tasked with writting a script to analyze our servers on a daily basis. I found this most excellent server inventory script that pulls virtually every conceivable value from a server.

Now, on our Exchange cluster, we've used Volume Mount Points for the resources. This means that a 9gb drive with eight 330gb folders (or mount points) to SAN storage. If I simply pull the drive freespace, I get ~6gb (local hard drive), even though the mount points each show over 100gb free.

So.. this script. It queries the server ($ServerName) and looks up all the mount points, then queries the volumes and returns the current size, free space, and % free for each labeled mount point.

Note (Nov 10, 2008): I cleaned up a warning in the script that popped up a false error when the folder was empty. In addition, I realized the data is more useful to me in a record format, so I have the data returned in an array instead of simple string (CSV) format. Powershell displays the data for one server like this:

Name FileSize FreeSpace PercFree
I:\SG1\ 229641617408 128873562112 36 %
I:\Utility\ 0 347809751040 100 %
I:\SMTPMTA\ 0 53606744064 100 %

 
#Get-MountPointInfo.PS1 Script
#Eric Woodford
#Scripts@ericwoodford.com
#Nov 11, 2008
#Discover and detail volume mount points on a specified Windows server.
#

function Get-MountPointInfo($ServerName) {
        $Summary = @()
       
        $objFSO = New-Object -com Scripting.FileSystemObject
        $MountPoints = gwmi -class "win32_mountpoint" -namespace "root\cimv2" -computername $ServerName
        $Volumes = gwmi -class "win32_volume" -namespace "root/cimv2" -ComputerName $ServerName| select name, freespace


        foreach ($MP in $Mountpoints) {
                $MP.directory = $MP.directory.replace("\\","\")        
                foreach ($v in $Volumes) {
                        $vshort = $v.name.Substring(0,$v.name.length-1 )
                        $vshort = """$vshort""" #Make it look like format in $MP (line 11).
                        if ($mp.directory.contains($vshort)) { #only queries mountpoints that exist as drive volumes no system
                                $Record = new-Object -typename System.Object
                                $DestFolder = "\\"+$ServerName + "\"+ $v.name.Substring(0,$v.name.length-1 ).Replace(":","$")
                                #$destFolder #troubleshooting string to verify building dest folder correctly.
                                $colItems = (Get-ChildItem $destfolder |  where{$_.length -ne $null} |Measure-Object -property length -sum)
                                #to clean up errors when folder contains no files.
                                #does not take into account subfolders.
                               
                                if($colItems.sum -eq $null) {
                                        $fsize = 0
                                } else {
                                        $fsize = $colItems.sum
                                }
                               
                                $TotFolderSize = $fsize + $v.freespace
                                $percFree = "{0:P0}" -f ( $v.freespace/$TotFolderSize)
                                $Record | add-Member -memberType noteProperty -name Name -Value $V.name
                                $Record | add-Member -memberType noteProperty -name FileSize -Value $fsize
                                $Record | add-Member -memberType noteProperty -name FreeSpace -Value $v.freespace
                                $Record | add-Member -memberType noteProperty -name PercFree -Value $percFree
                                $Summary += $Record
                        }
                }
        }
        return $Summary
}



$ServerName = "YourServerNameHere"
Get-MountPointInfo($ServerName) | convertto-html -title $ServerName > c:\Report-DriveSpace_for_$ServerName.html

Quick DL Search - All DLs that a specific person can SEND To (AcceptMessagesOnlyFrom)

This uses the Powershell filter to limit the list of ALL DLs to only those locked down to individuals. It doesn't filter on lists that the client is a member of having permissions. I specifically was looking for mail enabled contacts having permissions on my DLs.

$AllDLs = Get-DistributionGroup -ResultSize unlimited -Filter {AcceptMessagesonlyFrom -like "*"}
$alldls | ?{$_.AcceptMessagesOnlyFrom -match "Woodford, Eric"} | select displayname

Returns the names of all DLs.

Recurse DL Parents

I am working on a project to add members to a series of distribution lists. These lists are nested into various parent lists, sometimes 2-3 layers deep. It's never been updated, but almost all these distribution groups are Mail-enabled Global Security groups. Powershell really, really wants to work with Universal groups. You can't just convert the current DL to Universal, it's parent needs to be Universal also.

The following function recurses a DL's parents and converts them all to Universal DLs using Quest's Set-QADGroup.

function Find-ParentDL ($dl) {
        $MyDL = Get-QADGroup $dl
               
        if ($myDL.memberof -ne $null -and $myDL.groupscope -eq "Global") {
                foreach ($m in $myDL.Memberof) {
                        find-parentDL $m
                }
        }
        if ($dl -ne "" -and $myDL.groupscope -eq "Global") {Set-qadgroup -GroupScope Universal -Identity $dl}

        #       If you want to return the DL structure.
        #       $arr = New-Object system.Object
        #       $arr | Add-Member -memberType NoteProperty -name Members -value $dl
        #       return $arr
}

#Call by running:
Find-Parent "Example All Users List"

Woo hoo, my first PowerShell script!

This PowerShell script replicates the basic functionality of my Exchange Mailbox export HTA script. Rather quickly, it exports the following values (for all accounts with an email address) :

  • First name (givenname)
  • Last name (sn)
  • Distinguished name
  • Primary SMTP Address (mail)
  • User Logon name (userprincipalname)
  • User Logon name "Pre-Windows 2000" (sAMAccountName)
  • All secondary email addresses (proxyaddresses)

get-qaduser -dontusedefaultincludedproperties -ObjectAttributes @{mail='*'} -includedproperties sAMAccountName,employeeid,distinguishedname,mail,userprincipalname,givenname,sn,proxyaddresses -searchroot 'corp.ent/SBSUsers/_A - G' -serializevalues -sizelimit 0 | export-csv 'c:\Accounts_mail_A-G.csv'

To run this script, you will need to download and install the latest Quest tools. If you would like to include or change the fields (GivenName, employeeID, etc.) it pulls, don't expect to pull all the user properties as defined by:

Get-QADUser -IncludeAllProperties -ReturnPropertyNamesOnly

I kept trying, but getting only a few fields. Looked like I would get up to the first field that it could not query, then would drop the rest in the request. It appears to want the LDAP equivalents. Maybe I was just phrasing my query wrong, but I still can't pull the 'initials' value for anyone (and I know it is populated.)

I have recently revised this script that exports a few more fields from specific OUs.

PowerShell - Find all enabled AD users

A friend is working on a script to pull active LCS accounts from his AD. One last bit of information that he that was troubling him was enabled/disabled AD accounts.

Reading the Scripting Guys article, I found a switch that will tell all disabled AD accounts. Perfect, but just the opposite of what he wanted. Reading deeper, they state that when the bit is set to 2, it shows disabled accounts, so I implied that when it's set to (something else??) 0 (or 1) it must be enabled. Tested 1, nope. Then I found this article that shown that using a NOT statement will return what I was looking for.

This tiny script will query your current AD environment and return all ENABLED accounts in the environment.

get-qaduser -includeallproperties -ldapfilter "(!(userAccountControl:1.2.840.113556.1.4.803:=2))"

The "-includeallproperties" switch is required, otherwise you will get all accounts, and not those that apply to the LDAP filter.

Note, if you haven't already done so, you'll need to download and configure the Quest ActiveRoles Management Shell to run this query. This article helped me setup my environment and includes links to the various tools I use.

Powershell - Enumerate Delegate Rights for a mailbox

Troubleshooting Outlook delegate permissions is a pain. I found the easiest way to get a user's delegates is to create a profile, open their mailbox and check each person.

That's why I created this script. Using the Quest Powershell addons for AD, it reads the delegate permissions for a specified mailbox, then looks up the display name for each delegate or mailbox they are a delegate for.

I'd like to clean up the results a little more, but for now this works nicely.

$entry = Read-Host "Display name of mailbox"
if ($entry -ne $null) {
        $a= Get-QADUser $entry -ldapfilter '(mail=*)' -IncludedProperties displayname, publicdelegates, publicdelegatesbl
        foreach ($user in $a) {
                $user.displayname
                "================================="
                if ($user.publicdelegates -eq $null) {
                        Write-host "Has no delegates"
                } else
                {      
                        Write-host "Delegates:"
                    $b = $user.publicdelegates;
                        foreach ($del in $b) {Get-QADUser $del | select-object displayname| sort-object displayname};
                        "    "
                }
               
                if ($user.publicdelegatesbl -eq $null) {
                        Write-host "Is not a delegate"
                } else
                {
                                Write-Host 'Is a delegate for:'
                            $b = $user.publicdelegatesbl;
                                foreach ($del in $b) {Get-QADUser $del |select-object displayname| sort-object displayname};
                                "    "
                        }
                "    "
        }
}

AttachmentSize
EnumerateDelegates.ps1848 bytes

Powershell Script #2 - Getting last logon date for Exchange mailboxes

I have been attempting to do some cleanup of the Active Directory environment. My latest endeavor is to capture the last logon time from AD and correlate it to active accounts. If they're not logging on, maybe they don't know how, or don't need a mailbox??

My script here, will query all the Exchange servers for user display name and last logon time (LastLogonTime). Since the last logon is a 64-bit integer, you need to do some special handling of it. I found a number of tricks that used bit-level handling to convert the value to a date-time format, but after some pointing and nudging, I realized the simplicity of simply pulling it apart as a string value.

Last bit, I wanted a single CSV file for all my Exchange servers. There is no way I wanted to have 8 CSV files to sort through for all the mailbox information. Even more so, I didn't want to have to merge them all back together, this is a computer I am spending my life in front of.

Finally, I have to thank Microsoft for a great resource for converting my VBScript experience into Powershell. This site is an easy to understand reference for experienced VB scriptter who want to quickly learn PS.

Update (May 2008): In the BETA version of the Quest Active Roles Management shell includes a lastlogon field. This means you can quickly export the data without special formatting (like below).

Get-QADUser -SearchRoot 'your.domain.local/' -IncludeAllProperties | Format-List name, lastlogon*, accountis*

For each account on your domain, this script returns.
Name: guest
LastLogonTimeStamp: May 1, 2008
LastLogon: May 7, 2008
AccountIsDisabled: False
AccountIsLocked: False
...

Re-route this to a CSV file, and you have something to share with the whole family!

Thanks Quest, Nice addition to this tool!

[end update]

#        Name:  get-lastlogon.ps1
#      Author:  Eric Woodford - <a href="http://www.ericwoodford.com
#" title="www.ericwoodford.com
#">www.ericwoodford.com
#</a>        Date:  09/26/2007
#        Description:  Mailbox last logon information to single CSV file.
#              Display Name, Last Logon Time
#

#create header for CSV file.
$CSVFilePath = 'c:\mail_LastLogon.csv'
$strDate = get-date -uformat "%Y/%m/%d"
$strDate | out-file -filepath $CSVFilePath -encoding ascii
$strOutputString = "Display Name,LastLogonTime"
$strOutputString | out-file -filepath $CSVFilePath -encoding ascii -append

#set this to match your environment.
$Computers = get-qadComputer -searchRoot 'corp.ent/Member Servers/Exchange Servers'
foreach ($computer in $computers) {
        $users = Get-Wmiobject -namespace root\MicrosoftExchangeV2 -class Exchange_Mailbox -computer $computer.name | Select-Object MailBoxDisplayName, LastLogonTime
        # Get-Wmiobject -namespace root\MicrosoftExchangeV2 -class Exchange_Mailbox -computer $computer.name | Select-Object MailBoxDisplayName, LastLogonTime
        foreach ($user in $users) {
                   $date= [string] $user.LastLogonTime
                   if ($date.length -eq 0) {
                        $strOutputString = """"+ $user.MailBoxDisplayName + """, N/A"                          
                }
                else {
                        $strOutputString = """"+ $user.MailBoxDisplayName + """," + $date.substring(4,2)+"/"+$date.substring(6,2) +"/"+ $date.substring(0,4)
                }
           $strOutputString | out-file -filepath $CSVFilePath -encoding ascii -append
        }
}

Query or change DHCP settings of all XP workstations in AD

Due to a DHCP failure, a client had to go through and set all their workstations to static IP addresses. This is approximately 75 workstations throughout their environment. This week, I am tasked with fixing their DHCP issues, and resetting all the workstations back to using DHCP.

This broad-stroke VBScript searches for all workstations in an environment, and queries their DHCP settings for all of them. By removing a single comment, the same script can be used to set all the same workstations to use DHCP (which can be dangerous).

It needs a little work troubleshooting. It gets settings for about 90% of the computers on the local domain, but some return nothing. It's pinging, so it must be on the network. Possibly the PC is using a non-standard NIC, that doesn't support WMI? Maybe a firewall is blocking the query? Maybe it has simply been too long and the computer has lost it's DHCP lease, and not gotten a new one (waiting for someone to use it)??

AttachmentSize
DHCPUpdate.zip2.28 KB

Series of DNS Cleanup Scripts

One of my current projects is helping with the removal of a legacy DNS server. I am reviewing Wireshark logs to determine which devices are statically set to utilize the old DNS servers. These devices range from servers and workstations to printers, digital senders and ILO servers. Each device should be logged into, and updated with the new DNS server IP address.

Here is my process:

  1. Consolidate the Wireshark report. Wireshark generates a detailed report that contains the source and destination for each DNS transaction. Take this report then print it to a TXT file and save it to my local PC. I have written a script to pull out only the relevant information. It then attempts to do a WMI PING. If it get's a positive response, it then queries the NBT name, and system OS. The list is compiled into a CSV file and written to the C drive. (A 1mb file takes about 4 minutes to process).
  2. Clean-up the servers. My next step was to take all the Windows servers and run a cleanup process. Filtering on the OS, I copied all the
  3. Windows 2003 and XP IP addresses and created a new text file. Then I ran PSEXEC with my update script to touch each machine.

    PSEXEC @Serverlist.txt "cscript.exe" "\\mypc\script$\dnsupdate.vbs"

    The script updates the DNS server settings on each active NIC for the computer.
  4. Repeat... After a few repetitions of exporting the wireshark logs, I found that several of the servers didn't forget. They maintain the old DNS server in their DNS cache as a potential DNS server. That is why I created the last 3 scripts (zipped into DNS_Cleanup.zip). dd.bat while query and display the DNS cache. fd.bat runs a /flushdns command on the server/PC. UD.bat runs the DNSUpdate.vbs command on the server. So if after running dd.bat, I see it is using the old DNS, I hit F3, then HOME and change the DD to UD. If it is using the new DNS, I repeat it with FD just because.

Hopefully these help you in your clean-up also!

AttachmentSize
Scan Sniffer File.vbs.txt4.65 KB
dnsUpdate.vbs.txt4.52 KB
DNS_Cleanup.zip790 bytes

Server Inventory Script

I have been working on a number of projects lately where I need to touch most of the servers in our environment; be it changing DNS servers, or querying event logs, I needed a way to get every live server out there.

The attached script will query AD for all servers, then if pingable, it will attempt to gather more info via WMI. The final result is output to a CSV file in the C:\ root of the local computer.

On servers not running WMI, it will attempt to do a DOS Ping and then run NBTStat to gather the IP and MAC address.

AttachmentSize
GetServerInfo_dyna.vbs_.txt7.38 KB

Quckie - Get Windows Service Pack Level

We had a cluster issue with one of our Exchange 2007 CCR clusters. It didn't successfully failover when the virtual server quit responding. On this, we noticed that this server was still running Windows 2008 SP1.

This quick script exports all the windows versions and service packs for all current Exchange servers in your environment.

$servers = Get-ExchangeServer

$report = @()
foreach ($S in $servers) {
        $colItems = get-wmiobject -class "Win32_OperatingSystem" -namespace "root\CIMV2" -computername $s.Name
        $report += $colItems | select Caption, CSName,CSDVersion
}

$report | Export-Csv -Path ".\serverreport.csv"

Site Specification Project

This project is to define the steps from inital site request description to final specification document for building a site.

This is inspired by the IBM Developer works series. While this will not be from an IA professional, this project will use some of the techniques to define a site design before it is built.

Background: My wife is a part time school teacher for a local middle school. She has been wanting to start a website for a number of years to improve the communications with her students and parents. Previous failed starts required a high level of sophistication and technical expertise, including experience coding HTML, or using FrontPage and FTP clients. By taking advantage of the Drupal engine for content management, my wife simply needs to enter her information and attach any handouts. I still get involved in some of the more complicated programming, but no longer need to create and edit content for her.

Also make sure to check out my completed document. I used the design of the website for a final paper in a class. The document is available here.

WoodfordScience.com.

Initial Design

The Initial Description of the project was for an online website for a Middle School teacher. She intended the site to be used to provide a communications medium to parents, a "Homework Hotline". By providing parents with regular updates, the educator hopes that it will decrease the number of regular phone calls asking for homework updates. In addition, the site would provide sick and/or vacationing students a regular site to check for assignments.

The site must:

  • Be easy to create new content. - As an educator, she has little time to learn a new language, working with HTML would be out of the question. The site needs to be easy to update, easy to add new agenda items, and virtually self maintaining.
  • Be easy to attach pictures and handouts. Possibly photo galleries showing students performing labs. Also would help eliminate requests from students who lose key handouts. They could simply print them out at home, the library or parents work.
  • Be easy to expand. A fellow teacher may want to utilize the site to add her content to the site.

In addition, the site could:

  • Provide sample tests for students
  • Provide extra credit activities, via links, or polls on the site
  • Provide forms for downloads. (student contract, classroom policies, etc.)
  • Provide resource materials for large class projects.
  • Provide forum for student discussion.

Analysis
From the intial descriptions this seemed a posible job for page or book module and Event module. Upload, image and image_gallery modules as well.

Elements of Design

When initial design of the site, I created a base site that I thought my client would like. Links to information, I thought she would use, created sample posts to populate the site and demonstrated the site to her. She hated it. Colors all wrong, layout didn't make sense and the website simply wouldn't work.

I started over with a different approach. I asked her to search for websites that contained content similar to what she wanted to post. Based on her results, we took each site she liked, and picked apart the different elements that she liked and what she did not like.

  1. Bright colors, simple layout.
  2. Animated graphics, portraying subject matter related items (beating heart, jumping frog, etc.)
  3. Graphical buttons
  4. Menu items along side (as compared to across top, or in grid along middle of site.)

Based on her client base (parents and students in low-middle income households), I made a few suggestions.

  • We chose a basic 2-column layout. Looking at various Drupal Themes, I could quickly apply a theme to the basic site, which helped find a number of pre-done themes she liked. Nifty Corners screenshot had many of the elements she was looking for. Simple design, bright colors and even a large header area.
  • No large or animated graphics. Considering most parents are likely still on dial-up Internet access, we don't want to add any extra bandwidth. We compromised with a larger graphic header (based on the Nifty Corners theme). We pulled colors from the graphic, to use for the various elements on the site (headers, links, mission statement, etc.).
  • Graphical buttons, translated to links without the link-line. We considered removing the line, but wanted the interface to be intuitive, and left the line in place.

Once we had the look and feel of the site closer to what the client expected, we could work on how she would use the site.

Complete Write-up

As part of my Master's program, I completed a detailed analysis of the site. This document includes design considerations for the site, customizations done to the site, and even basic build instructions. This should be helpful for an IT individual with experience installing Windows to build a similar site.

I am happy to say that I received an 'A' in the class and on this assignment!

AttachmentSize
Eric Woodford TS5004 Final Paper.pdf461.92 KB

Sudoku Solver!

While watching a rerun of NUMB3RS on TV, the mathmatician, Charlie, suggested to his dad that he should just right an algorithm to solve the Sudoku puzzles. Hmm, I could do that!

I started out writing a short vbscript that would attempt to solve the puzzle.

  1. Find the possible values for each square and populate all the squares with only 1 possible value.
  2. Find the squares, where only 1 square in the 3x3 grid can have a specific value, and populate those squares.
  3. Find the squares where the rows and columns do not have conflicting possible values to one in this square.

This could solve virtually all Easy and Moderate rated puzzles. When using attempting a Hard puzzle, it would fail when only squares with 2+ possible values remained.

Reading in a Sudoku puzzle book, they suggested a few additional methods and I added logic for these scenarios.

  1. When you have 2 squares in a row/column/3x3 grid containing the same 2 possible values, these are the ONLY squares that can contain these values. Eliminate them from all other's in the same row/column/3x3 grid.
  2. Make a guess (covered next).

This allowed the script to solve several HARD puzzles that the previous script crashed on. Unfortunately, not all mulitple possible value scenarios have multiples.

Alright, now time to guess. To guess, the script does a few things.

  1. Finds all squares with 2 possible values.
  2. Eliminates all squares with similar possible values.
  3. Eliminates all squares on same row/column as other squares.
  4. Once a guess is made, it marks it location and guess value.
  5. Each subsequent square is marked as Post-Guess.

If the first guess is wrong, it uses the post-guess values to revert the changes made. The script then attempts the second possible value in the guess. If this also fails, it reverts again, and finds another square.

During my practice runs, the script has completed 'Diabolical' level puzzles in a matter of seconds!! Solved scripts are saved to the root of C:\ in a CSV format.

AttachmentSize
sudoku_8-4.zip6.83 KB

The Lost Manual: Biological Autonomous Bipedal Energy Engine (BABEE)

Prologue

As the father of both a 4 year old and 7 month old, I have some recent knowledge about care for a newborn baby. At my work, my two co-workers are both expecting new girls first quarter 2007.

Why not create a technical manual that these guys can relate to?


Introduction
You are the proud owner of OBGYN's latest model BABEE. This has been custom built using the latest genetic technology to compliment your very lifestyle. Please take a few moments and read through the entire manual before operating your BABEE. These delicate devices are extrememly sensitive to the environment and if not properly handled can cause you an enormous nightmare.

System Requirements
Out of the box, your BABEE unit has very basic requirements. Unfortunately they will likely not be tuned to your schedule for several weeks and we at OBGYN suggest that you take a few weeks off from your work schedule to accomodate your BABEE unit.

  1. Power consumption: At first, the BABEE Storage Array comes with very limited capacity. You should expect to be recharging the SA every couple hours (see section on alerts). Customers have seen the SA automatically increase after a few weeks, some have reported their BABEE taking almost 3 months before the storage array will sustain power for 5 hours (aka "slept all night"). When your BABEE unit has slept all night, don't celebrate too quickly, as your unit will soon start growing!!
  2. Disposal and cleaning: Your BABEE unit has an automatic waste disposal (see Outputs below) unit built into it. This unit purges waste whenever space is required. Like the SA, the AWD unit will increase in storage capacity over the next few months. Don't fret, you shouldn't be cleaning your BABEE unit for more than 3 years!

I/O
Inputs: Your new BABEE has sensitive audio/visual input sensors. Using NAI (non-artificial intelligence), it will soon learn to recognize you by sight and sound. This process typically takes 1-3 months. Until then, anyone who approaches your BABEE unit will be greeted similarly.

As mentioned above, you should quickly locate the power recharging input. The easiest method of recharging your unit is direct application of BABEE Low Gas Formula Fuel (BLGFF) to the input near the visual inputs. Upon delivery of the unit, until it can self-sustain, you or another engineer will need to recharge your BABEE unit. BLGFF can be located in several local retailers.

Outputs: Your BABEE unit has 2 basic outputs. Both of these outputs should be located immediately.

  1. The AWD is located near the base of the unit. This will need to be cleaned each time liquid or solid waste is disposed by the unit.
  2. The second output is located inside the recharging port. This output can produce complicated auditory alerts (discussed in the Alerts section of this manual) and works as a secondary AWD. After recharging your BABEE, please take a few moments to prime the line and remove any air bubbles. Patting the unit softly on the back has been proven effective with removing the air.

Alerts
Screaming babyWatch out! Your BABEE has very sophisticated alert system. It will notify you of every possible type problem. Unfortunately the alerting early off is rather confusing, sorry. (Click here to purchase a supplementary manual.) This was to make your unit versatile for international sales. Don't worry, as the unit localizes, it will soon learn to mimic your vocalizations and respond. Customers have reported positive results around 1 year of training. Use of manual communication has shown positive results also.

Beyond the audible alerts, also watch for leaking by the visual inputs, swinging of the appendages, and irritable spouses. These alternate alerts can often provide additional insight.

Training
To increase the functionality of your BABEE unit, it is never too late to start a rudimentary training regiment. After a few months, you should find that repetitive exposure to various inputs will help set in a routine. By maintaining a regular schedule of events, your young BABEE unit will start to respond accordingly. At first your focus should be to keep a regular schedule for feedings, morning and night time routines, and extra-curricular activities (like gym, walks, etc.).
For example:

  1. up at 7
  2. daytime activity:
  3. lunch at 11:30am
  4. dinner by 5pm
  5. bedtime activity: story time, language lessons, music (avoid consuming fluids)
  6. bed by 8pm

After successful training, you will quickly find that your unit will start alerting you when a scheduled event is about to happen.

NOTE: Disregard all (except from a trained support professional) suggestions that your BABEE unit should be woken on regular intervals. This only trains your BABEE to wake on regular intervals, even after you have quit.

The Lost Manual: BABEE - Chapter 2

Congratulations, you have now completed one full year with your BABEE unit. During the last few months, you should have noticed some drastic changes. This chapter is to cover some of the additional challenges that come with your growing BABEE.

Mobility Starting at about 9 months, your BABEE unit probably started quad appendage mobility. As your BABEE strengthens, this will convert to two legged, then full speed running. Traction control is recommended to keep up with your BABEE unit, especially in crowded areas.

Safety Along with the increased mobility, comes the increased need for unit safety. Your BABEE unit has been programmed with innate curiosity. This is to help the unit learn faster, unfortunately, it also has no limits. You may find that your BABEE is attempting to climb up on your furniture, check the contents of the silverware drawer, and taste the cleaning fluid under your sinks. It is in your best interest to protect your unit. Here are a few useful tricks:

  1. Relocate toxic chemicals to a white room safety cabinet.
  2. Implement biometric scanners on chemical cabinets and cutting utensils.
  3. Lock down delicate computer equipment.
  4. Attach earthquake bands to your large-screen television and other electronic equipment.

Interaction
With increased mobility, and if you have been working on the manual interaction method, your patience should be paying off. Somewhere around the 1 year mark, your unit should start showing signs of mimicking your language and vocal tonality.

This is a work in progress (I have a different story running through my head and can't seem to focus on this one.) ... check back for the entire document. Make sure to check out Chapter 1 of this book.

Use PING to notify when server reboots.

Working on servers, you inveriably need to reboot them. One of the primary tools I use to determine if a server has shut down is to PING the server.

PING servername -t

This will PING the server repeatedly until I quit it. When the server goes down, it will return "Request Timed Out" instead of a valid PING response. Here is what it would look like when the server shuts down, then comes back up again.

Pinging Servername [192.168.1.1] with 32 bytes of data:
Reply from 192.168.1.1: bytes=32
Reply from 192.168.1.1: bytes=32
Reply from 192.168.1.1: bytes=32
Reply from 192.168.1.1: bytes=32
Reply from 192.168.1.1: bytes=32
Request timed out
Request timed out
Request timed out
Request timed out
Request timed out
Reply from 192.168.1.1: bytes=32
Reply from 192.168.1.1: bytes=32
Reply from 192.168.1.1: bytes=32

Unfortunately, if you happen to miss the downtime (change screens, leave your desk, etc.) you'll never know what happened. Did the server reboot or has Windows crashed while trying to go down???

That's why I developed this little batch script. It utilizes the DOS PING command to monitor the status change. When the server successfully shuts down, it changes the display and waits for the server to come back up.

Checking Server [servername] for response
Check # 0 - Server Down: 16:32:27.89
Waiting For Server to come up: 4
Server Pinging Again Passes: 5

Highlight and copy the following code to your buffer (CTRL+C). Open notepad, and paste it into a new text file. Save that text file as PingAlive.bat. Copy this file into your system path (like C:\Windows\System32).



echo off
CLS
Set A=0
Set ServerName=%1

if not '%1'=='' goto :Pass1
Set /p ServerName=[What is the server name?]
if not '%ServerName%'=='' goto :Pass1
echo Failed to get Server name..
Goto :End

:Pass1
Echo Checking Server %ServerName% for response
echo Check # _
:Pass1a
ping %ServerName% > results.txt
find /C "Request timed out" results.txt > fresults.txt

for /F "skip=1 tokens=1,2 delims=:" %%a in (fresults.txt) DO (
        if NOT "%%b"==" 0" goto :TimedOut
        cls
        Echo Checking Server %ServerName% for response
        echo Check # %A%
        Set /A A+=1
        Goto :Pass1a
)
:TimedOut
Set DownTime=%TIME%
Echo Server Down %DownTIME% Passes: %A%
Set B=0
:Pass2
ping %ServerName% > results.txt
find /C "Request timed out" results.txt > fresults.txt
for /F "skip=1 tokens=1,2 delims=:" %%a in (fresults.txt) DO (
        if "%%b"==" 0" goto :BackUp
        CLS
        Echo Checking Server %ServerName% for response
        echo Check # %A% - Server Down: %DownTime%
        Echo Waiting For Server to come up: %B%
        Set /A B+=1
        Goto :Pass2
)

:BackUp
echo Server Pinging Again - Passes: %B% %TIME%
:End



Now when you reboot a server, simply open a new command window and type:

PINGALIVE SERVERNAME

Now I can use my server reboots as coffee breaks and not worry about missing anything!

AttachmentSize
PingAlive.zip564 bytes

Another great use for PSExec

Tired of searching through racks and racks of servers to not find what your looking for?

Tired of having to run back to your desk to remote into that server to insert a CD?

Utilizing the power of PSExec, you can eject the CD drive remotely without having to leave your comfy desk.

@echo off
if "%1"=="" goto end
echo Starting process to eject the CD on %1
psexec \\%1 -c "\\yourworkstationname\script$\ejectcd.exe"
:end

AttachmentSize
Eject Remote CD.zip21.21 KB

Hello? Is anyone out there? If so, ping me..

When you are looking for something, like a piece of hardware, and you don't know where to start looking, it is always useful to have a good set of tools.

For example, I wanted to find what IP address that a new UPS device was using. I could not locate it in the DHCP tables and figured it must have a static IP address. The administration tools could not access it via the USB cable, so I needed to brute force search for the IP address. What I would do for an IP scanner right then? Code one!

The script below is a command-line script that uses WMI to check the pingstatus of the system at each IP address. I grabbed parts from Windows IT Pro. So far, it will only increment the last octet of the IP address. Maybe if the need arises, I will modify the code to take a start and end range.

Update:
After reviewing Scott's enormous list of his favorite tools, I found this one which appears to have a great GUI.

' Ping Range Script (PingIP.vbs)
'
By Eric Woodford (<a href="http://www.ericwoodford.com" title="http://www.ericwoodford.com">http://www.ericwoodford.com</a>)
' Original Source from <a href="http://www.windowsitpro.com/Article/ArticleID/48449/48449.html

Option" title="http://www.windowsitpro.com/Article/ArticleID/48449/48449.html

Option">http://www.windowsitpro.com/Article/ArticleID/48449/48449.html

Option</a> Explicit

Dim strHost, strEnd
Dim Start_IP, x, strIP

'
Check that all arguments required have been passed.
If Wscript.Arguments.Count < 1 Then
    Wscript.Echo "Arguments <Host> required. For example:"
    Wscript.Echo "Ping a single IP address or hostname:"
    Wscript.Echo vbtab& "cscript PingIP.vbs (hostname)"
    Wscript.Echo vbtab& "cscript PingIP.vbs ipaddress" & vbcrlf
    Wscript.Echo "Ping a range of IP addresses from ipaddress to ipaddress+count"
    Wscript.Echo vbtab& "cscript vbping.vbs ipaddress count"
    Wscript.Quit(0)
End If

strHost = Wscript.Arguments(0)
if wscript.arguments.count = 2 then
        strEnd = wscript.arguments(1)
        if not(isnumeric(strend)) then  
                wscript.echo "Count argument needs to be numeric (1-254)."
                wscript.quit
        end if
        if cint(strend)>=254 or cint(strend)<=0 then
                wscript.echo "Count argument needs to be numeric (1-254)."
                wscript.quit
        end if
        strEnd=cint(strEnd)
end if
if strEnd <> "" then
        'get last subnet of start
        strIP = left(strHost,instrrev(strHost,"."))    
        Start_IP = right(strhost,len(strHost)- instrrev(strHost,"."))
        if isnumeric(start_IP) then
                start_ip=cint(start_ip)
        else
                wscript.echo "ERROR: To ping a range, you need to put in the starting IP address, not a hostname"
                wscript.quit(0)
        end if
        if start_ip = 0 then start_ip = 1
        wscript.echo start_ip + strEnd
        if (Start_IP + strEnd) > 255 then
                wscript.echo "range exceeds maximum for octet."
                strEnd= strend - ((start_ip+strend)-255)
                wscript.echo "converting max of range to :" & strEnd
        end if
        for x = start_IP to (start_ip + strEnd)        
                if Ping(strIP&x) = True then
                        Wscript.Echo "Host " & strIP&x & " contacted"
                Else
                        Wscript.Echo "Host " & strIP&x & " could not be contacted"
                end if
        next   
Else
        if Ping(strHost) = True then
           Wscript.Echo "Host " & strHost & " contacted"
        Else
                Wscript.Echo "Host " & strHost & " could not be contacted"
        end if
End If

Function Ping(strHost)

    dim objPing, objRetStatus

    set objPing = GetObject("winmgmts:{impersonationLevel=impersonate}").ExecQuery _
      ("select * from Win32_PingStatus where address = '
" & strHost & "'")

    for each objRetStatus in objPing
        if IsNull(objRetStatus.StatusCode) or objRetStatus.StatusCode<>0 then
    Ping = False
            '
WScript.Echo "Status code is " & objRetStatus.StatusCode
        else
            Ping = True
            'Wscript.Echo "Bytes = " & vbTab & objRetStatus.BufferSize
            '
Wscript.Echo "Time (ms) = " & vbTab & objRetStatus.ResponseTime
            'Wscript.Echo "TTL (s) = " & vbTab & objRetStatus.ResponseTimeToLive
        end if
    next
End Function

Function Resolve(strHost)

    dim objPing, objRetStatus

    set objPing = GetObject("winmgmts:{impersonationLevel=impersonate}").ExecQuery _
      ("select * from Win32_ComputerSystem where address = '
" & strHost & "'")

    for each objRetStatus in objPing
        if IsNull(objRetStatus.StatusCode) or objRetStatus.StatusCode<>0 then
                Resolve = ""
            '
WScript.Echo "Status code is " & objRetStatus.StatusCode
        else
            Resolve = True
            'Wscript.Echo "Bytes = " & vbTab & objRetStatus.BufferSize
            '
Wscript.Echo "Time (ms) = " & vbTab & objRetStatus.ResponseTime
            'Wscript.Echo "TTL (s) = " & vbTab & objRetStatus.ResponseTimeToLive
        end if
    next
End Function

AttachmentSize
PingIP.zip1.21 KB

Pipes, not just for plumbers.

For quite some time, I have wanted to develop a batch script that I could use to automate the process of backing up my documents then email them to myself. Using my GMail account's enormous capacity, I can keep several copies of my documents accessible online without having to worry about drive space.

My requirements:

  1. Compression needs to be a standard format. (ZIP, TAR, etc.) I want the option to access these files using any computer.
  2. Need to be able to filter out files or include only specific files (not just the entire folder). I have a few large databases, that I wouldn't want to be sent. No matter what, I don't want this 2gb Access database sent to me.
  3. Automated. I want this to happen on a regular basis, without me having to initiate it. Backups are useless if you can't rely on them.
  4. Sent to me via email. As I stated, I want this to be emailed to me, instead of sitting on the computer I ran it from.

Here is the code that I came up with. I make use of DOS pipes in order to get my data.

dir /b /s /AA "%userprofile%\my documents\" | find "My Shapes" /v |  find "My Pictures" /v |  find "My Videos" /v | c:\bin\zip232xn\zip c:\My_Stuff.zip -@ >> c:\my.log

The DIR command displays the directory in BARE format (path and filename only), only show files with the Archive bit turned on, and search sub-folders. The FIND statement, searches the filenames given it, and excludes entries that contain the search string (like "My Shapes"). Finally Info-Zip takes the filename and compresses it into a single file, C:\My_Stuff.zip.

When that is complete, I run the DOS Attrib command to turn off the attribute bit on all the files I compressed. Then I email myself the file using BLAT. Note, that I have set it to span the email into multiple 10mb attachments. You'll need to adjust this to match your outgoing SMTP server, and use an email program that can handle multipart UUEncoded attachments.

You'll need:

The full script is below.

Echo off
echo %DATE% %TIME% > c:\my.log

dir /b /s /AA "%userprofile%\my documents\" | find "My Shapes" /v |  find "My Pictures" /v |  find "My Videos" /v | c:\bin\zip232xn\zip c:\My_Stuff.zip -@ >> c:\my.log
if "%errorlevel%"=="0" attrib -A /s /D "%userprofile%\my documents\*.*" >> c:\my.log

REM Repeat code above for additional folders. For example a server side share.

:send
C:\BIN\blat262\full\blat.exe c:\my.log -to <a href="mailto:example@gmail.com">example@gmail.com</a> -server mail -Subject "Stuff from %date% %time%" -f <a href="mailto:MyFromAddress@example.com">MyFromAddress@example.com</a> -attach c:\my_stuff.zip -multipart 10240

if "%errorlevel%"=="0" del c:\my_stuff.zip
del c:\my.log

Playing with PSInfo

Now that Daylight Saving Time has begun, I am constantly getting requests for help on why the calendar info is wrong. I suggest that maybe they need to install the Microsoft patch (931836). Unfortunately it is not easy to scan a computer to find a single patch. Since Service Pack 2, there has been at least 150 patches for various issues, updates, and such.

To make things easier, I found that the PSInfo will return all the patches for a specific computer. This will return all the patches on the local computer. Unfortunately, it does not appear that you can filter on a specific patch using the embedded 'filter' command of PSInfo (like you can for other components that it will return).

PSInfo -h
----------------------------------
Installed     HotFix
8/28/2006     Microsoft .NET Framework 1.1 Hotfix (KB886903)
8/27/2006     Microsoft .NET Framework 1.1 Service Pack 1 (KB867460)
10/14/2006    This Security Update is for Microsoft .NET Framework 2.0. \n
(etc.)

Great! Now I can have my Dad wade through 3 pages of hotfixes, looking for a specific number in the list. Of course, I can have him pipe it to a text file, then simply search the file in Notepad or such. Why? DOS has a great command called "FIND". This command will search an input source and return matching contents. So I have them type in:

PSInfo -h | find "931836"

This returns one line when it finds the patch:

PsInfo v1.74 - Local and remote system information viewer
Copyright (C) 2001-2005 Mark Russinovich
Sysinternals - <a href="http://www.sysinternals.com

2/17/2007"
title="www.sysinternals.com

2/17/2007"
>www.sysinternals.com

2/17/2007</a>     Update for Windows XP (KB931836)

and no patch info when it is not installed.

To extend this, I have considered taking advantage of the wildcard functionality in PSInfo. You could type "\\*" after the PSINFO statement and it would search all computers on the local network! Unfortunately, I've been told that this returns the number of computers with a give patch, not which ones. I'd suggest a BATCH FOR ... loop..

Thanks Mark, I love PSTools!

Using Ping to Notify when a Server Reboots v2.

Working on network workstations, I found I needed a method to tell when they came online. I wanted a script to email me so I could receive it on my PDA when the connection worked. This way I could be working in the wiring cabinet and hear it successfully connect.

The following script can easily be modified to notify when a server is up or down. I've also added an option to loop the script continuously until specific triggers are met. These triggers can be set at the command line, or hard set into the code for quick deployment. You'll just need to put your email address into the script before you run it.

Requirements: Latest version of BLAT (note the path in line 75).

@echo off
REM ======================================================================
REM
REM Batch File -- Created with SAPIEN Technologies PrimalScript 3.1
REM
REM NAME: NOTIFY.BAT
REM
REM AUTHOR: Eric Woodford
REM DATE  : 2/13/2008
REM
REM COMMENT: This script can be configured to notify when a specific server is up or down
REM          requires BLAT to send SMTP email messages.
REM ======================================================================
cls

set dest=%1
if '%dest%'=='' goto :error

REM address to notify when triggered.
Set emailaddr=eric@example.com
set mailsrvr=mail.example.com

if not '%2'=='' goto :params
rem set this to 1 to trigger when true
set exitwhenup=0
set notifyup=0
set exitwhendown=1
set notifydown=1
goto :repeat

:params
set exitwhenup=%2
set notifyup=%3
set exitwhendown=%4
set notifydown=%5


:repeat

if exist c:\notifydown.txt del c:\notifydown.txt
ping %dest% -n 1 | find  /c "Reply from " > c:\NotifyDown.txt

for /F "tokens=1" %%a in (c:\notifydown.txt) DO (
set /A serverstatus=%%a
if '%serverstatus%'=='1' (
        set msgsubject=Server Up - %time%
        cls && echo %msgsubject%
        if '%notifyup%'=='1' call :sendmsg
        if '%exitwhenup%'=='1' goto :exit
) ELSE  (
        set msgsubject=Server Down - %time%
        cls && echo %msgsubject%
        if '%notifydown%'=='1' call :sendmsg
        if '%exitwhendown%'=='1' goto :exit
)
)

goto :repeat

:error
echo %0 - Notify when device is down
echo.
echo parameters:
echo 1. IP address or host name
echo.
echo [Optional]
echo    2. Exit when up (0=no, 1=yes)
echo    3. Notify when up (0=no, 1=yes)
echo    4. Exit when down (0=no, 1=yes)
echo    5. Notify when down (0=no, 1=yes)

goto :exit

:sendmsg
REM This is all one line, upto the GOTO statement.
call c:\bin\blat262\full\blat.exe c:\notifydown.txt -t %emailaddr% -server %mailsrvr% -f %emailaddr% -subject "%dest% %msgsubject%"
goto :EOF


:exit
echo exiting %time%    

AttachmentSize
Notify.zip906 bytes

Using the ADUC Saved Query functionality - MemberOf

A new HR system is going to generate e-mails to every employee in the company. To accomplish this, about 200 people needed to have accounts created for them. To make administration easier, all 200 new accounts were added to a single AD Group, "Limited Use Employees".

Using Active Directory for Users and Computers (ADUC), you can query for individual names, groups, and even resources on the network, like computers, printers, etc.. If you have 200 people you are attempting modify, you can modify each one individually (ouch!!), you can modify the container they reside on (not bad, especially if you use a GPO), or you can a mass modify solution, like scripting or LDIFDE. LDIFDE work best if you have some field that ties them all together (same department, same company field, etc), but not so well if you don't. Scripting works great, but does require a bit of coding experience. Read file, find account, modify value, save user, repeat.

We wanted something simpler. ADUC has a built-in query function to allow you to build 'Saved Queries'. This option has a number of basic wizards that allow you to build various queries to search your AD environment. For example, finding all mail-enabled groups that don't have managers.

  1. NEW -> Query
  2. Give it a detailed name
  3. Click Define Query
  4. In the FIND dropdown, select Exchange Recipients
  5. Deselect all but Mail-enabled groups
  6. Select Advanced tab
    1. Field - Group - Managed By and the Condition = Not Present, Click Add

  • Field - Group - Members and the Condition = Present, Click Add
  • Click OK twice to save
  • Click Refresh (F5) to get the latest information.

    As I stated, we added all 200 employees to the same distribution list. This means if we could create a query to return all the members of a specific distribution group. I found only one way to get this query to work. You need to create the query so it points directly to the distinguishedname(DN) of your group. The DN can be found by searching ADSI Edit or you can build it by hand.

    To build the DN by hand you need the value that's in the Canonical Name of Object field. This can be found on the Object tab in ADUC. It should look something like:

    corp.ent/Groups/Limited Use
    (Domain)/OU1/../OUn/Group Name

    (where ... equals any number of sub-OU containers.)

    The DistinguishedName simply reverses the information and describes each portion. So for my example, the distinguished name would be (NOTE: The '\' before the group name is required):

    cn=\Limited Use,ou=Groups,dc=corp,dc=ent

    So, for the Advanced Filter, I put:

    Member Of is (exactly) cn=\Limited Use,ou=Groups,dc=corp,dc=ent

    Unfortunately after all this, I found out that you cannot modify Exchange Mailbox properties, like Delivery Restrictions, using this method. So I guess, we are back to the other options, individually (probably safer) or using a script.