A friend of mine called me, and asked me for help… He had had a migration go wrong and had ended up with a bunch of .pst files containing 1000’s of emtpy folders. He had started cleaning up the .pst’s manually, after having spent several hours on one .pst file he decided on another approach, and this is where I came in.

He asked if it was possible to write a script that would delete all the empty folders for him, I said to him that I bet it is possible, just not sure if I can do it, I have never really “scripted” Outlook before..

So I started looking for ways to manipulate data in Outlook and found out that the Outlook.Application COM object allowed me to manipulate folders and messages within Outlook.. Did some quick testing, told my friend that it should be possible, and that I would get back to him…

After some mucking around here is what I ended up with.
[ps]
$olApp = new-object -com Outlook.Application
$namespace = $olApp.GetNamespace("MAPI")
#Root folder to match, look in your outlook and look at what folders you have.
#RooFolders could be Mailbox – <username>, Public folders, or other mapped in PST’s or mailboxes.
$RootfolderToMatch = "Inbox"

$Global:StopValue = 1

Function ListEmpty {
Param ($infolder)
Foreach ($fldr in $infolder.Folders) {

If ($fldr.Items.Count -eq 0 -and $fldr.Folders.Count -eq 0) {

# I have had to check for system folders, these cannot be deleted via the script, so to prevent the script for erroring out, I check for it.
if ($fldr.name -match "Slettet" -or $fldr.name -match "Deleted" -or $fldr.name -match "journal" -or $fldr.name -match "rss"
-or $fldr.name -match "note" -or $fldr.name -match "kalender" -or $fldr.name -match "kontakt" -or $fldr.name -match "udbakke"

-or $fldr.name -match "Sync" -or $fldr.name -match "Server" -or $fldr.name -match "contacts" -or $fldr.name -match "Outbox" )
{"Cannot delete systemfolder : """ + $fldr.FolderPath + ""

}

Else {
if ($fldr.Parent.name -notmatch "slettet" -and $fldr.Parent.name -notmatch "Deleted" ) {
#Remove # from the below line, to actually delete Outlook folders.
#$fldr.delete()
$fldr.FolderPath + " has been deleted!!"

$Global:StopValue = 0
}
}
}

Else {
if ($fldr.name.length -gt 0) {

ListEmpty($fldr)}
}

}

}

$RootFolders = $namespace.Folders | ?{$_.name -match $RootfolderToMatch}

Listempty($RootFolders)

While ($Global:StopValue -eq 0) {
$Global:StopValue = 1
Listempty($RootFolders)
}

[/ps]

I start by initializing the Outlook COM object, and declaring some variables
[ps]
$olApp = new-object -com Outlook.Application
$namespace = $olApp.GetNamespace("MAPI")
#Root folder to match, look in your outlook and look at what folders you have.
#RooFolders could be Mailbox – </username><username>, Public folders, or other mapped in PST’s or mailboxes.
$RootfolderToMatch = "Inbox"

$Global:StopValue = 1
[/ps]

I then create a function that takes an Outlook folder as a parameter.
I then loop through each subfolder, and checks to see if each folder contains any files or other folders, if it finds a folder that does not contain any files/folders it then checks to see if the folder name is something like “outbox, sent items, Journal etc etc.” The reason I do this is because these are system folders, and cannot be deleted, so if the script tried to delete one of them, it would come back with an error.

So if the folder that is currently being checked is empty and is not a system folder it will be deleted.(Actually the line where the folder is deleted is commented out, just to make sure there is not accidental deletions) The script then recurses through all subfolders to check for empty folders.

[ps]

Function ListEmpty {
Param ($infolder)
Foreach ($fldr in $infolder.Folders) {

If ($fldr.Items.Count -eq 0 -and $fldr.Folders.Count -eq 0) {

# I have had to check for system folders, these cannot be deleted via the script, so to prevent the script for erroring out, I check for it.
if ($fldr.name -match "Slettet" -or $fldr.name -match "Deleted" -or $fldr.name -match "journal" -or $fldr.name -match "rss"
-or $fldr.name -match "note" -or $fldr.name -match "kalender" -or $fldr.name -match "kontakt" -or $fldr.name -match "udbakke"

-or $fldr.name -match "Sync" -or $fldr.name -match "Server" -or $fldr.name -match "contacts" -or $fldr.name -match "Outbox" )
{"Cannot delete systemfolder : """ + $fldr.FolderPath + ""

}

Else {
if ($fldr.Parent.name -notmatch "slettet" -and $fldr.Parent.name -notmatch "Deleted" ) {
#Remove # from the below line, to actually delete Outlook folders.
#$fldr.delete()
$fldr.FolderPath + " has been deleted!!"

$Global:StopValue = 0
}
}
}

Else {
if ($fldr.name.length -gt 0) {

ListEmpty($fldr)}
}

}

}

[/PS]

Rootfolders is the initial "path"/folder that the scripts starts to look for empty folders from.
$namespace.folders is the highest folder level in Outlook, it contains the user mailbox, public folders and any mapped in .pst files, so what I do here is match a single top level folder.

Here is an example, lets say I have opened a .pst file called BusinessCases09, I would set $RootfolderToMatch equal to <em>BusinessCases09</em> (This is set at the beginning of the script), that means when the ListEmpty function is called for the first time, it will scan through all subfolders of the .pst file.
[ps]

$RootFolders = $namespace.Folders | ?{$_.name -match $RootfolderToMatch}

Listempty($RootFolders)

[/ps]

In order to make sure the script runs to all empty folders is deleted, I have set up a while loop to call function until no more empty folders are found.
[ps]
While ($Global:StopValue -eq 0) {
$Global:StopValue = 1
Listempty($RootFolders)
[/ps]

Our friend Darren Mar-Elia over at SDM software has just released some nice PowerShell cmdlets, to  view Tombstoned objects in AD… You can even revive(or reanimate them as I think the correct AD term is) them as well.. The only requirements are PowerShell (duuh), .Net 2.0 and W2k3 running AD.

I installed it without any problems on a x86 system, but had a few minor problems running it on my 64bit Vista rig, here is what I did to get it working.

Creating an Alias for the .Net installutil, notice the Framework64 here.
[sourcecode lang=”ps”]
set-alias installutil $env:windir\Microsoft.NET\Framework64\v2.0.50727\installutil
[/sourcecode]

I then installed the .dll file manually using the alias I just created above.
[sourcecode lang=”ps”]
installutil -i “C:\Program Files (x86)\SDM Software\SDM Software AD Tombstone Cmdlets\ADTombstones.dll”
[/sourcecode]

Mind you if you are running Vista, the regular user will not have access to “program Files” folder, and you will not be able to register the .dll without running as admin.

I then checked to see if the .dll actually had been loaded

[sourcecode lang=”ps”]
get-pssnapin -registered
[/sourcecode]

I saw that it was succesfully added, I was then ready for the final thing, before I could set out on my revival quest.

[sourcecode lang=”ps”]
add-pssnapin SDMSoftware.PowerShell.AD.Tombstones
[/sourcecode]

Take it for a spin, it is a nice little cmdlet.

I had to run a script the other day, that I wrote some time ago to change some files in a folder, the script uses the Shell.Application COM object BrowseForFolder method, to show a GUI that lets you browse through the directory structure and choose a folder. I thought that it might come in handy one day, to have this functionality in PowerShell as well.

First off I remembered seeing someone creating a PowerShell script, that read the input from WSH’s InputBox() by using the COM object MSScriptControl.ScriptControl.

[sourcecode language=’CSS’]
$a = new-object -comobject MSScriptControl.ScriptControl
$a.language = “vbscript”
$a.addcode(“function getInput() getInput = inputbox("Message box prompt“,"Message Box Title“) end function” )
$b = $a.eval(“getInput”)

[/sourcecode]
So I thought that if you can do the simple script above, you can do something a little more advanced.

I started out by looking at the example I had allready created, just copied that to the $a.addcode line of code, that didn’t work of course. After playing with it for a while, I figured out that it was problems with my ‘ ” ”s, they had to be escaped in order for the code to be evaluated correctly, so I ended up with something like this.

[sourcecode language=’CSS’]
Function Get-Folder {
$a = New-Object -com MSScriptControl.ScriptControl
$a.language = “vbscript”
$a.addcode(“Function ShowBrowse() r Set objShell = CreateObject(“Shell.Application") r Set objFolder = objShell.BrowseForFolder (0, "Choose a Folder:“, 0, "“) r If objFolder Is Nothing Then r WScript.Quit r End If r Set objFolderItem = objFolder.Self r ShowBrowse = objFolderItem.Path r End Function” )
$b = $a.eval(“ShowBrowse”)
Return $b
}
[/sourcecode]
The above has been incapsulated into a function for ease of use.

I posted the above script on the Minasi forum, and only minutes went by, before I had the first reply.. It was from no other than Don Jones himself, asking if I knew I could use COM objects directly in PowerShell, I thought to my self, Yes, I have just called one in the script above.

Then it dawned on me, I used PowerShells functionality to create a “MSScriptControl.ScriptControl” object, that then creates a “Shell.Application” object in WSH “MSScriptControl.ScriptControl” runspace. Why not create the “Shell.Application” object directly.
That gave me a lot simpler code, and a chance to slap myself for not thinking of that earlier, here is what it looks like

[sourcecode language=’CSS’]
Function Get-folder {
$a = New-Object -ComObject “Shell.application”
$b = $a.BrowseForFolder(0,”Choose Folder”,0,””)
$c = $b.Self
$c.path
}

[/sourcecode]

Don Jones’ keynote basically said, if you do not learn PowerShell, you will not be able to function as an Windows IT administrator in 5 years time or so… After what was told on the MVP summit, about Microsofts commitment to PowerShell, and how all new products will be based on it, he foresees are very dim future for admins who do not know it.

One of his suggestions was to read a PowerShell help file every day… Just go through the help files one by one, you will most  likely not remember all of it, but if you read enough, some point might stick 😉

So on the way home I decided to try to write a little PowerShell script that would display a help file, and automatically store the the number of the helpfile that I have reached. (So I do not have to keep track of it myself)

This is the most basic version, I came up with it on the plane home from the Forum Meet.

[int]$HelpNr = Get-Content -Path c:\scripts\test.ini
$vhelp = Get-Help * | Get-Help
$i=0
foreach ($vvhelp in $vhelp){
$i++
if ($i -eq $HelpNr) {$vvhelp}
}
$HelpNr++
Set-Content -Path c:\scripts\test.ini -Value $HelpNr


It has to be said though, that this is hardcoded to look for a file “c:\scripts\test.ini ” which is where it keeps track of the number of help files you have gone through.
If you are going to test the program, in this version you have to create the .ini file, and just put a 1 in the first line of it.

What are you doing between April 20 and April 23rd ??? You don’t know ??
Well if you are the least bit interested in computers, there is only one place to be… The Minasi Forum meet 2008
It is your chance to hear about new technology, directly from the leading people of the industry, it is hosted by none other than Mark Minasi himself (If you have ever read a good computer book, you would have guessed that allready)

This year some of the keynote speakers are Mark Minasi, Jeremy Moskowitz, Don Jones, Rhonda Layfield, Eric B. Rux, Greg Shields and Nathan Winters, that means you will be able to learn everything from Windows 2008 to Exchange to PowerShell…

I bet right now you are thinking, this must be very expensive…….. WRONG

It only costs 450$ and with the current exchange rate, that is only 2277Kr… The event takes place in Virgina Beach, VA USA, the only other costs are hotel, getting your self there, beer money and a little bit extra for food.

Here is the current schedule (which is subject to change)

  • Sunday, April 20, 2008

    1:00 pm: Mark Minasi Keynote
    2:15 pm: Break
    2:30 pm: TBA Industry Keynote
    3:45 pm: Break
    4:00 pm: Eric B. Rux Fun with Windows Home Server; trust me, you’ll want one…don’t let the word “Home” fool you
    5:00 pm: ‘No Host’ dinner at local restaurant (share your war stories and learn from the experts!)

  • Monday, April 21, 2008

    9:00 am: James Summerlin Microsoft SQL Server 2005 Integration Services
    10:15 am: Break
    10:30 am: Greg Shields Documentation & Change Control: Hating it to Loving it in 75 minutes
    11:45 am: Lunch
    1:00 pm: Mark Minasi Windows Server 2008
    2:15 pm: Break
    2:30 pm: Jeremy Moskowitz Group Policy 2.0: It’s freekin’ sweet! (with Vista & Windows Server 2008)
    3:45 pm: Break
    4:00 pm: Nathan Winters Exchange 2007 from the command line – what makes a good Exchange 2007 deployment
    5:00 pm: Hosted Dinner at Pungo Grill. Mmmmmm! (we ate there last year, it was very good)

  • Tuesday, April 22, 2008
  • 9:00 am: Greg Shields Tips & Tricks for Preventing an Active Directory Failure
    10:15 am: Break
    10:30 am: Don Jones Dr. PowerShell: How I Learned to Stop Clicking and Love the Command-Line
    11:45 am: Lunch
    1:00 pm: Rhonda Layfield TBA
    2:15 pm: Break
    2:30 pm: Curt Spanburgh Journey to Planet Sharepoint
    3:45 pm: Break
    4:00 pm: Nathan Winters OCS 2007 – An Introduction to the new VOIP world of Office Communication Server 2007
    5:00 pm: ‘No Host’ dinner at local restaurant (one last chance to rub elbows with people in the know)

  • Wednesday, April 23, 2008
  • 9:00 am: Don Jones Windows PowerShell: The Future of Server Administration
    10:15 am: Break
    10:30 am: Mark Minasi Windows Vista
    11:45 am: Lunch
    1:00 pm: All Attendees Roundtable discussion for as long as you want!
    End of Forum Meeting. See you again next year!

    More info is available at www.minasi.com/forummeet2008

    Just been playing with PowerTabs for PowerShell, it is kinda like M$ Intellisence code completion, but for your PowerShell command prompt. Let me tell you, it rocks… Just type get- < T A B> and you get all your cmdlets that starts with get-

    This is really really nice if you use your powershell command prompt a lot (Which I try to do :))

    Here is the Link

    In PowerShell you have the option create custom profiles that you can use, in the profiles you can create different aliases, change the prompt and a lot of other things, today we will look at adding some info to prompt. First we need to open profiles.ps1 , this file can be put in the WindowsPowershell folder under System32, or in the users \My Documents\WindowsPowerShell. If you are running vista, and want to play around with your profiles.ps1 file, I would put it in the \My Documents\WindowsPowerShell folder, since you have permission to write to files in that dir. (You can read more about profiles here
    Well ok… on with the coding.. Lets say you want to add some basic info to your prompt.
    When you start up powershell for the first time, your prompt will be defined as

     “PS “ + $(get-location) + ">”

    When you open your profile.ps1 file (There is a template in C:\Windows\System32\WindowsPowerShell\v1.0\Examples\profile.ps1) you will find a function called
    “ prompt”, this is where you will make the following changes.
    The simplest alteration you can make is changing PS to your name

    “Xenophane “ + $(get-location) + ">”
    Will give a prompt like this “Xenophane C:\Users\xen\_”

    What if you want to know something about the system at the prompt, is that possible…. Of course it is…
    Let say you want to know how many processes you are running


    function prompt
    {
    "Processes " + (Get-Process).Count+ " " + $(get-location) + "> "
    }

    Will give you a prompt like this: “ Processes 71 C:\Users\ctn\_”

    But it is very annoying to see that info on the same line as your cursor, so lets add the info on the line over the cursor, and add some more information.


    function prompt
    {
    [wmi]$Global:Cdrive=Get-WmiObject -query "Select * from win32_logicaldisk where deviceid='c:'"
    #Define Cdrive as global variable, which is type cast as a wmi
    $processcount=(Get-Process).Count
    #Define variable processcount as number of processes running
    $cpuload=(Get-WmiObject win32_processor).loadpercentage
    #cpuload is defined as a variable
    $diskinfoC=" Free C: "+"{0:N2}" -f (($global:Cdrive.freespace/1gb)/($global:cdrive.size/1gb)*100)+"%"
    #diskinfoC is defined, {0:N2} is .NET for 2 decimal places, -f is a the "format" property of the string object.
    # Then the amount of free space on drive C is divided by the size of drive C multiplies by 100 to get
    #the percentage of free space.
    $time=Get-WmiObject -class Win32_OperatingSystem
    #Define $time is object Win32_OperatingSystem
    $t=$time.ConvertToDateTime($time.Lastbootuptime)
    #Define #t as the result of the convertion of LastBootuptime into a readable dateTime format.
    [TimeSpan]$uptime=New-TimeSpan $t $(get-date)
    #Typecast the variable $uptime as a [timespan], the timespan cmdlet in powershell is a way to do date arithmetic in ps
    #This way we can compare our variable $t (Which was Lastbootuptime) and the current time Get-date cmdlet
    $up="$($uptime.days)d $($uptime.hours)h $($uptime.minutes)m $($uptime.seconds)s"
    #define variable $up to display the uptime in days/hours/minuted/seconds
    $TextToDisplay="CPU:"+$cpuload+"% Processes:" + $processcount + $diskinfoC+ " " + $diskinfoG+ " "+ ([char]0x25b2)+$up +" "+(Get-Date -format g)
    #define one variable that contains all the data we collected so far.
    Write-Host -fore red $TextToDisplay
    #Write the information to the screen in red
    Write-Host "My PoSh:" $(get-location) -nonewline
    #Display the regular prompt.
    }

    One other thing to be aware of is that PowerShell by default will not let you run scripts, so in order to run scripts like profile.ps1 you need to loosen up the security, to get help you can type the command “get-help about_signing” in a powershell command prompt. I have chosen to use RemotedSigned which will let me run scripts locally on the machine. But you can read about the different security levels in the help file.

    Profile txt file

    Imagine that you want to list, the last 15 events in all the eventlogs on the system, do you have to write several lines of codes to do that, like this below??

    get-eventlog -logname Application -newest 15
    get-eventlog -logname System -newest 15
    get-eventlog -logname Application -newest 15

    Of course not, PowerShell is much smarter than that…
    You can use the ‘foreach’ command.


    foreach ($eventlog in (get-eventlog -list -asString)) {write-host $eventlog; get-eventlog $eventlog -newest 15}

    This will give you the newest 15 log entries in each of the eventlogs on the system, let me try to break it down.
     One thing that is important to know about PowerShell is how it handles () and {}
    As in math the () parenthesis’ are evaluated first, the {} curly brackets are used for compulsory arguments.

    The foreach makes the script run through all eventlogs on the system, and add them to the variable $eventlog, then PowerShell evaluates the {} arguments for each.

    {write-host $eventlog; get-eventlog $eventlog -newest 15}

    simply writes the variable $eventlog (this contains the name of a eventlog on the system) to the screen, then it goes on to invoke the get-eventlog again, and tells that to return the newest 15 entries, and write them to the screen.  

    Kiddo… This isnt very readable I would like to have a better indication of when a new eventlog listing is started.

    I say we can take a look at the properties of write-host (You can type help write-host -full or man write-host -full)
    and low and behold it has a feature called background color and foregroundcolor, let try them out.

    foreach ($eventlog in (get-eventlog -list -asString)) {write-host -foregroundcolor blue -backgroundcolor green $eventlog; get-eventlog $eventlog -newest 15}

    But kiddo I want some specific properties from the eventlogs, how do I do that….

    In order to figure out what properties an obeject has, you can use the get-member command as described in another post.
    Get-eventlog -Logname system | get-member (The -logname paramenter is required)
    Now you have a list of properties on the eventlog object, say you would like (Index,EventID,TimeGenerated and Message)

    foreach ($eventlog in (get-eventlog -list -asString)) {write-host -foregroundcolor blue -backgroundcolor green $eventlog; get-eventlog $eventlog -newest 15| select Index,EventID,TimeGenerated,Message}

    Started playing around with PowerShell, so I thought I would post a few simple things on the blog, maybe you won’t find it interesting, but it is a good way for me to remember, so BLAH!.

    First in order to get information from the eventlog you call the cmdlet called:
    get-eventlog

    Then you need to tell it what you want to get.
    get-eventlog -logname Application

    Which will get you the Application log on the local machine.

    That will list the entire Application eventlog to the screen, which is usually a lot of data. So we want to be able to pick specific data out of the eventlog, and here is a way to do it.

    get-eventlog -logname Application -newest 50 | where {$_.EventID -eq "1"}

    What this does is, it gets the Application eventlog, and gives me an object with the newest 50 entries, i pipe this object into another function called “where”,  I tell “where” to show me all objects where EventID is equal to 1. I this case you see I use $_.EventID, the $_ in powershell represents the object the as being passed into the new function, so in this case $_ represents the newest 50 values in the Application Eventlog.