Friday, September 20, 2013

Removing Outlook Email Attachments and Replace Them with a Links to a Saved Location

I have come across the need to strip out email attachments from Outlook a number of times in the past but never wanted to pay for a solution.  If you are in the same boat here is a method to do this for free.  There are a number of variations on this VBA script out there, just google the first line of the VBA code to find them.

My biggest issue with this method is that I never figured out a way to deploy it to all of my users at once but I still found this very useful for the big offenders and for keeping my own email box size under control.

Without further ado I present:

A VBA Macro for:

1. Removing attachments of selected messages (one or many) and saving them to a Local/Mapped drive location

2. Adding a link to the bottom of each email to the saved attachment files.

How to use it:

1. Add Developer Tools to the Outlook Ribbon:

 

 



2. Set Macro Security to “Prompt for All”




3. Create a New Macro. Title it “SaveAttachmentsSelected” (You open the window the type the name and click “Create”)





4. Replace everything in the window that appears with the contents of the code below.



5. Click save and close the VBA editor.

6. Add a “Quick Access Toolbar” link to the Macro









7. Find an email to use as a test (make sure it’s something you won’t miss in case there is a problem.)

8. Select the email, then click the Macro icon in the Toolbar



9. Check out the body of the email….it should have links to the attachment(s) at the very bottom of the email message. Make sure they work.

10. To do multiple emails, just select multiple items.

Be patient with this method. When you select a large number of emails just sit back and give Outlook time to crank through everything. Start with small batches so you can get a feel for how it works and the time it takes to save the files.
Sub SaveAttachmentsSelected()
Dim filesys, newfolder
Dim objOL As Outlook.Application
Dim pobjMsg As Outlook.MailItem 'Object
Dim objSelection As Outlook.Selection
Dim strFolderpath As String

'SETTINGS HERE AND THERE IS ANOTHER SET FURTHER DOWN
'I had issues using the HOMEDRIVE environmental variable so I hard coded this to the AD user drive.
'Feel free to try the other options as they might work for you
strFolderpath = "U:"

' Get the path to your Home Direcorty folder
'strFolderpath = Environ("HOMEDRIVE")

' The following line sets the base location as My Documents
'strFolderpath = CreateObject("WScript.Shell").SpecialFolders(16)
On Error Resume Next

strFolderpath = strFolderpath & "\Outlook_Attachments\"

'Create Base Directory
Set filesys = CreateObject("Scripting.FileSystemObject")
If Not filesys.FolderExists(strFolderpath) Then
   newfolder = filesys.CreateFolder(strFolderpath)
End If

' Instantiate an Outlook Application object.
Set objOL = CreateObject("Outlook.Application")

' Get the collection of selected objects.
Set objSelection = objOL.ActiveExplorer.Selection

For Each pobjMsg In objSelection
SaveAttachments_Parameter pobjMsg
Next

ExitSub:

Set pobjMsg = Nothing
Set objSelection = Nothing
Set objOL = Nothing
End Sub

Public Sub SaveAttachments_Parameter(objMsg As MailItem)
Dim filesys, newfolder, colProcessEnvVars
Dim objAttachments As Outlook.Attachments
Dim i As Long
Dim lngCount As Long
Dim HomeDir As String
Dim strFile As String
Dim strFolderpath As String
Dim strDeletedFiles As String

'SETTINGS HERE AND THERE IS ANOTHER SET ABOVE
'I had issues using the HOMEDRIVE environmental variable so I hard coded this to the AD user drive.
'Feel free to try the other options as they might work for you
strFolderpath = "U:"

' Get the path to your Home Direcorty folder
'strFolderpath = Environ("HOMEDRIVE")

' The following line sets the base location as My Documents
'strFolderpath = CreateObject("WScript.Shell").SpecialFolders(16)

On Error Resume Next

' Set/Create the Attachment folder. 
' This Adds a folder structure which places attachements in folders based on Year and Month
strFolderpath = strFolderpath & "\Outlook_Attachments\" & Format(objMsg.ReceivedTime, "yyyy-MM") & "\"

Set filesys = CreateObject("Scripting.FileSystemObject")
If Not filesys.FolderExists(strFolderpath) Then
   newfolder = filesys.CreateFolder(strFolderpath)
End If

' Get the Attachments collection of the item.
Set objAttachments = objMsg.Attachments
lngCount = objAttachments.Count

If lngCount > 0 Then

' We need to use a count down loop for removing items
' from a collection. Otherwise, the loop counter gets
' confused and only every other item is removed.
For i = lngCount To 1 Step -1

' Save attachment before deleting from item.
' Get the file name.
strFile = objAttachments.Item(i).FileName

' Combine with the path to the Temp folder.
' This Creates a filename which includes the senders name, the date the email was sent, and the filename.
strFile = strFolderpath & objMsg.SenderName & "_" & Format(objMsg.ReceivedTime, "yyyy-MM-dd-h-mm-ss") & "_" & i & "_" & strFile

' Save the attachment as a file.
objAttachments.Item(i).SaveAsFile strFile

' Delete the attachment.
objAttachments.Item(i).Delete

'write the save as path to a string to add to the message
'check for html and use html tags in link
If objMsg.BodyFormat <> olFormatHTML Then
strDeletedFiles = strDeletedFiles & vbCrLf & "<file://" & strFile & ">"
Else
strDeletedFiles = strDeletedFiles & "<br>" & "<a href='file://" & strFile & "'>" & strFile & "</a>"
End If
Next i
End If

' Adds the filename string to the message body and save it
' Check for HTML body
If objMsg.BodyFormat <> olFormatHTML Then
objMsg.Body = objMsg.Body & vbCrLf & vbCrLf & "The attachment(s) were saved to " & strDeletedFiles
Else
objMsg.HTMLBody = objMsg.HTMLBody & "<br>" & "<br>" & "The attachment(s) were saved to " & strDeletedFiles & ""
End If
objMsg.Save
ExitSub:

Set objAttachments = Nothing
Set objMsg = Nothing
Set objOL = Nothing
End Sub

Monday, April 1, 2013

Powershell - RunAsAdminAlways Function

Here is a function I use in many of my powershell scripts to check if they have been launched with elevated privileges. If it has then it just changes the window color and title. If it has not it is relaunched with those privileges.

If you create a PS1 file with all the code below and then replace everything below the "Stuff to be run in an elevated prompt goes below here." comment it will ensure that code is executed in an elevated command prompt.

Of course you could also always grab just the function and call it from one of your own existing script(s).

Function RunAsAdminAlways ($command) {
#*=============================================
#* Function: RunAsAdminAlways
#* Created: [02/15/2011]
#* Author: Charles Ulrich
#* Arguments: Should always be called with 
#* $myInvocation.MyCommand.Definition as the only argument
#* Ex: $y = RunAsAdminAlways ($myInvocation.MyCommand.Definition)
#*=============================================
#* Purpose: Checks if script is running as admin
#* and relaunches if not.
#*
#*=============================================

# Get the ID and security principal of the current user account
$myWindowsID=[System.Security.Principal.WindowsIdentity]::GetCurrent()
$myWindowsPrincipal=new-object System.Security.Principal.WindowsPrincipal($myWindowsID)
  
# Get the security principal for the Administrator role
$adminRole=[System.Security.Principal.WindowsBuiltInRole]::Administrator
  
# Check to see if we are currently running "as Administrator"
if ($myWindowsPrincipal.IsInRole($adminRole))
    {
    # We are running "as Administrator" - so change the title and background color to indicate this
    $Host.UI.RawUI.WindowTitle = $command + "(Elevated)"
    $Host.UI.RawUI.BackgroundColor = "DarkBlue"
    clear-host
    }
else
    {
    # We are not running "as Administrator" - so relaunch as administrator
    Start-Process powershell -ArgumentList $command -verb "runas"
    
    # Exit from the current, unelevated, process
    exit
    }
}

$y = RunAsAdminAlways ($myInvocation.MyCommand.Definition)

# Stuff to be run in an elevated prompt goes below here.

Write-Host "Looks Good!"
Write-Host "Press any key to continue."

$z = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")

Monday, March 11, 2013

Lync Server 2013 - Bulk Updating Contact Groups

I have been working on a rollout of Lync 2013 at my company over the last couple weeks. We are moving from an OpenFire/Spark setup to Lync mostly because we wanted to give our users the ability to do video chats and screen share's easily and Spark did not really provide this functionality for us.

The biggest thing we found lacking in Lync 2013 is no central management of Contact Groups. With Spark we setup the groups and they are loaded whenever a user signs on.



Our users really like having this list and we did not want them to have to manually configure all of the groups. I was not able to find any specifics of exactly how to do this on the web so I decided to roll up my sleeves and do it myself.  There are a few more things I would like to add to it and clean up but I wanted to get it out in the wild for other people to use.

Currently this script will only update the Contact Groups for a user who has logged into the server at least once. Additionally if the user has already added some of the groups in the past it will retain the original order of the previously added groups and append the new ones on at the bottom.

You will need 7-Zip installed on your link server to run this script.

Here are the basic steps to use the script:
  1. Create a reference account and add the Contact Groups you want to load for your other users.

  2. Log into your Lync Server and from the Lync Management Shell export the user you updated with the following command.

    Export-CsUserData -PoolFqdn "YOURLINKPOOL" -UserFilter first.last@something.com -FileName "c:\temp\ExportedUserData.zip"
    

  3. Extract the Zip file and open the DocItemSet.xml file.  I recommend using Notepad++ with the XML Tools Plug to format that file into something readable.

    We are interested in the "ContactGroups" XML node.  Each ContactGroup entry in this list corresponds to one of the contact groups you added to the reference account.  Copy and paste this out of the XML file.  It should look something like this:

     
       
       
       
       
       
       
       
       
       
       
       
       
       
       
       
     
    
  4. You're ready to copy the script below. Update variables as you see fit in the the Settings section on top. You will definitely have to update the following variables:

    $lyncpoolfqdn - to your own FQDN

    $replacement - paste your default ContactGroups XML in place of my sanitzed version


##########################################################################################################################
#
#    Name:            Update_Lync_2013_Groups.ps1    
#    Author:         Charles Ulrich
#    Date:            03/08/2013
#    Description:    Script to update the Contact List for 1 or All Lync 2013 users.
#
#    Requirements:    1. MUST BE RUN ON LYNC SERVER - Export-CSUserData does not like Remote Shell or Implicit Remoting
#                    2. 7-Zip must be installed - Used to manage the ZIP files from the export and needed for the import.
#                    3. If not running under Powershell v3.0 see comments in MultipleSelectionBox function.
#
#
##########################################################################################################################

# Settings

#Lync Server
$lyncpoolfqdn = "YOURLINKSERVERPOOL"

#Base File Path
$BaseFilePath = "c:\scripts\lync_user_export"

#7-Zip Paths
$7ZipPath = "c:\Program Files\7-zip\7z.exe"
$7ZipOutputDir = $BaseFilePath + "\working"
$7ZipOutputParam = "-o" + $7ZipOutputDir
$7ZipIncludeFiles = $7ZipOutputDir + "\*.xml"

#Lync XML and ZIP file paths
$LyncXMLFile = $7ZipOutputDir + "\DocItemSet.xml"
$ExportFileNamePath = $BaseFilePath + "\ExportedUserData.zip"
$UpdatedFileNamePath = $BaseFilePath + "\UpdatedUserData.zip"

# replacement node with child nodes
[xml]$replacement = @'
    <ContactGroups>
      <ContactGroup Number="1" DisplayName="fg=="/>
      <ContactGroup Number="2" DisplayName="WJvopsjer3ojfaopj" ExternalUri="Pfaohiweifgouwe90ufjSDAW$jop3958osdkpodasok32480mVkR3JvdXAiPjxlbWFpbC8+PC9n3904kjlkJKSL904="/>
      <ContactGroup Number="3" DisplayName="SFDEW3523j" ExternalUri="Pfaohiweifgouwe90ufjSDAW$jop39357673425hdfbsart3462twqgaedfgajoisdfoipqhuy38tpy32u980hfiaosdhiogpkR3JvdXAiPjxlbWFpbC8+PC9n3904kjlkJKSL904="/>
      <ContactGroup Number="4" DisplayName="35uyh" ExternalUri="Pfaohiweifgouwe90ufjSDAW$jop3958osdkpodasok32480mVkR3JvdXAiPjxlbWFpbC8+PC9n3904kjlkJKSL904="/>
      <ContactGroup Number="5" DisplayName="fbdsfh346" ExternalUri="Pfaohiweifgouwe27y0ahsvokja;sdC9n3904kjlkJKSL904="/>
      <ContactGroup Number="6" DisplayName="sfdhw3466" ExternalUri="Pfaohvbasrtyq4tyasv58osdkpodasok32480mVkR3JvdXAiPjxlbWFpbC8+PC9n3904kjlkJKSL904="/>
      <ContactGroup Number="7" DisplayName="asetg4363y6" ExternalUri="Pfaohiweifgouwe90vsdgwerhjudfbsdfkR3JvdXAiPjxlbWFpbC8+PC9n3904kjlkJKSL904="/>
      <ContactGroup Number="8" DisplayName="634tsagasdg" ExternalUri="Pfaohidsvsdgherhysdkpodasok32480mVkR3JvdXAiPjxlbWFpbC8+PC9n3904kjlkJKSL904="/>
      <ContactGroup Number="9" DisplayName="ngfd56w3" ExternalUri="Pfaohiweifgouwe90ufjdasok32480mVkR3JvdXAiPjxlbWFpbC8+PC9n3904kjlkJKSL904="/>
      <ContactGroup Number="10" DisplayName="asdgsa46" ExternalUri="Pfaohiweifgouwe90ufSDfsdtewtgvjopaiwejr302582385ioskjgl;skdaopsok32480mVkR3JvdXAiPjxlbWFpbC8+PC9n3904kjlkJKSL904="/>
      <ContactGroup Number="11" DisplayName="agasg" ExternalUri="Pfaohiweifgouwe90ufjSDAW$jop3958osdkpodasok32480mVkR3JvdXAiPjxlbWFpbC8+PC9n3904kjlkJKSL904="/>
      <ContactGroup Number="12" DisplayName="235wgsdfa" ExternalUri="Pfaohiweifgouwe90ufjSDAW$jop3958osdkpodasok32480mVkR3JvdXAiPjxlbWFpbC8+PC9n3904kjlkJKSL904="/>
      <ContactGroup Number="13" DisplayName="236gvbasetaw" ExternalUri="Pfaohiweifgouwe90ufjSDAW$jop3958osdkpodasok32480mVkR3JvdXAiPjxlbWFpbC8+PC9n3904kjlkJKSL904="/>
      <ContactGroup Number="14" DisplayName="asdgawqt3aqw" ExternalUri="Pfaohiweifgouwe90ufjSDAW$jop3958osdkpodasok32480mVkR3JvdXAiPjxlbWFpbC8+PC9n3904kjlkJKSL904="/>
      <ContactGroup Number="15" DisplayName="casdrwqt5" ExternalUri="Pfaohiweifgouwe90ufjSDAW$jop3958osdkpodasok32480mVkR3JvdXAiPjxlbWFpbC8+PC9n3904kjlkJKSL904="/>
    </ContactGroups>
'@


Function MultipleSelectionBox ($inputarray,$prompt,$listboxtype) {

# Taken from Technet - http://technet.microsoft.com/en-us/library/ff730950.aspx
# This version has been updated to work with Powershell v3.0.
# Had top replace $x with $Script:x throughout the function to make it work. 
# This specifies the scope of the X variable.  Not sure why this is needed for v3.
# http://social.technet.microsoft.com/Forums/en-SG/winserverpowershell/thread/bc95fb6c-c583-47c3-94c1-f0d3abe1fafc

$Script:x = @()

[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing") 

$objForm = New-Object System.Windows.Forms.Form 
$objForm.Text = $prompt
$objForm.Size = New-Object System.Drawing.Size(300,600) 
$objForm.StartPosition = "CenterScreen"

$objForm.KeyPreview = $True

$objForm.Add_KeyDown({if ($_.KeyCode -eq "Enter") 
    {
        foreach ($objItem in $objListbox.SelectedItems)
            {$Script:x += $objItem}
        $objForm.Close()
    }
    })

$objForm.Add_KeyDown({if ($_.KeyCode -eq "Escape") 
    {$objForm.Close()}})

$OKButton = New-Object System.Windows.Forms.Button
$OKButton.Location = New-Object System.Drawing.Size(75,520)
$OKButton.Size = New-Object System.Drawing.Size(75,23)
$OKButton.Text = "OK"

$OKButton.Add_Click(
   {
        foreach ($objItem in $objListbox.SelectedItems)
            {$Script:x += $objItem}
        $objForm.Close()
   })

$objForm.Controls.Add($OKButton)

$CancelButton = New-Object System.Windows.Forms.Button
$CancelButton.Location = New-Object System.Drawing.Size(150,520)
$CancelButton.Size = New-Object System.Drawing.Size(75,23)
$CancelButton.Text = "Cancel"
$CancelButton.Add_Click({$objForm.Close()})
$objForm.Controls.Add($CancelButton)

$objLabel = New-Object System.Windows.Forms.Label
$objLabel.Location = New-Object System.Drawing.Size(10,20) 
$objLabel.Size = New-Object System.Drawing.Size(280,20) 
$objLabel.Text = "Please make a selection from the list below:"
$objForm.Controls.Add($objLabel) 

$objListbox = New-Object System.Windows.Forms.Listbox 
$objListbox.Location = New-Object System.Drawing.Size(10,40) 
$objListbox.Size = New-Object System.Drawing.Size(260,20) 

$objListbox.SelectionMode = $listboxtype

$inputarray | ForEach-Object {[void] $objListbox.Items.Add($_)}

$objListbox.Height = 470
$objForm.Controls.Add($objListbox) 
$objForm.Topmost = $True

$objForm.Add_Shown({$objForm.Activate()})
[void] $objForm.ShowDialog()

Return $Script:x
}


#Load Lync Powershell Commands
Import-Module Lync

#Who should we update
$sora = Read-Host "Do you want to update (A)ll users or a (S)ingle user? (S or A)"

If ($sora -eq "S")
    {
        # Get user to update 
        $userlist = Get-CSUser
        $user_email=MultipleSelectionBox $userlist.UserPrincipalName "Choose Lync 2013 User" "One"
        
        #Export Single Users Data
        Export-CsUserData -PoolFqdn $lyncpoolfqdn -UserFilter $user_email -FileName $ExportFileNamePath
    }
Else
    {
        #Export All Users Data
        Export-CsUserData -PoolFqdn $lyncpoolfqdn -FileName $ExportFileNamePath
    }

#Extract the Exported Zip file. Requires 7-Zip
Write-Host " "
&$7ZipPath e $ExportFileNamePath $7ZipOutputParam
Write-Host " "
Write-Host " "
$original = [xml] (Get-Content $LyncXMLFile)

#Set our loop counter to 0
$count = $original.DocItemSet.DocItem.Count + 1

Write-Host "########################################################################"

#Loop through all DocItem Elements and replace any with ContactGroups in them
For ($i=0; $i -lt $count; $i++) {
        If (($original.DocItemSet.DocItem[$i].Data.HomedResource.ContactGroups.ContactGroup.Count -gt 0)) 
            {
                # get the target node
                
                Write-Host " "
                Write-Host "Working on XML Node: " $original.DocItemSet.DocItem[$i].Name
                Write-Host " "
                Write-Host "Contact Groups Before: "$original.DocItemSet.DocItem[$i].Data.HomedResource.ContactGroups.ContactGroup.Count
                Write-Host " "
                $inner = $original.DocItemSet.DocItem[$i].Data.HomedResource.ContactGroups
                
                # import the replacement values
                $new = $original.ImportNode($replacement.ContactGroups, $true)
                
                # replace old node with new one (replacement node)
                $dump = $original.DocItemSet.DocItem[$i].Data.HomedResource.ReplaceChild($new, $inner)
                
                Write-Host "Contact Groups After: "$original.DocItemSet.DocItem[$i].Data.HomedResource.ContactGroups.ContactGroup.Count
        }
    }

Write-Host " "
Write-Host "########################################################################"
Write-Host " "

#Remove blank xmlns tags created by importing the node
$original = [xml] $original.OuterXml.Replace(" xmlns=`"`"", "")

# save changes (full path to file)
$original.Save($LyncXMLFile)

# create updated zip file
& $7ZipPath a $UpdatedFileNamePath $7ZipIncludeFiles

Write-Host " "
Write-Host "########################################################################"
Write-Host " "
Write-Host "The XML file has been updated with the default groups."
Write-Host " "
Write-Host "If you want to take a look at the file its path is"
Write-Host " " 
Write-Host $LyncXMLFile
Write-Host " "
Write-Host "This file will be deleted once this script has finished."
Write-Host " "
Write-Host "########################################################################"
Write-Host " "

$sure = Read-Host "About to upload the changes to the server.  Are you sure? (Y or N)"

If ($sure -eq "Y")
    {
        Write-Host " "
        Write-Host "Updating Server with the new Contact Group Settings."
        Write-Host " "
        Write-Host "The user(s) will have no Contact groups until they" 
        Write-Host "log off and back on to Lync."
        
        If ($sora -eq "S")
            {
                # Update the server with the new User Data
                Update-CsUserData -Filename $UpdatedFileNamePath -UserFilter $user_email
            }
        Else
            {
                # Update the server with the new User Data
                Update-CsUserData -Filename $UpdatedFileNamePath
            }
        Write-Host " "
    }
Else
    {
        Write-Host " "
        Write-Host "Update Aborted!!!!!!"
        Write-Host " "
        Write-Host "Please Come Again!  :) " 
        Write-Host " "
    }

Write-Host "I will clean up the files and close the window after you hit Enter."
Write-Host " "

PAUSE

# Clean Up
Remove-Item ($BaseFilePath + "\*") -recurse

Friday, March 8, 2013

Powershell Multi-Select List Box Function

I use a bit of code for creating a multiple selection list box from an old technet article (http://technet.microsoft.com/en-us/library/ff730950.aspx) in a number of my powershell scripts.

Recently I was putting together a script on my new Windows 8 box and found that the function no longer returned what was selected. I ended up finding the solution to my problem here.

The issue seems to be related to the way Powershell 3.0 handles variable scopes. To make it work I had to add the script scope to the variable in the function.

Originally the variable was $x, now its $Script:x.

In case anyone finds themselves in the same boat i did here is an updated version of the function.

Function MultipleSelectionBox ($inputarray,$prompt,$listboxtype) {

# Taken from Technet - http://technet.microsoft.com/en-us/library/ff730950.aspx
# This version has been updated to work with Powershell v3.0.
# Had to replace $x with $Script:x throughout the function to make it work. 
# This specifies the scope of the X variable.  Not sure why this is needed for v3.
# http://social.technet.microsoft.com/Forums/en-SG/winserverpowershell/thread/bc95fb6c-c583-47c3-94c1-f0d3abe1fafc
#
# Function has 3 inputs:
#     $inputarray = Array of values to be shown in the list box.
#     $prompt = The title of the list box
#     $listboxtype = system.windows.forms.selectionmode (None, One, MutiSimple, or MultiExtended)

$Script:x = @()

[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing") 

$objForm = New-Object System.Windows.Forms.Form 
$objForm.Text = $prompt
$objForm.Size = New-Object System.Drawing.Size(300,600) 
$objForm.StartPosition = "CenterScreen"

$objForm.KeyPreview = $True

$objForm.Add_KeyDown({if ($_.KeyCode -eq "Enter") 
    {
        foreach ($objItem in $objListbox.SelectedItems)
            {$Script:x += $objItem}
        $objForm.Close()
    }
    })

$objForm.Add_KeyDown({if ($_.KeyCode -eq "Escape") 
    {$objForm.Close()}})

$OKButton = New-Object System.Windows.Forms.Button
$OKButton.Location = New-Object System.Drawing.Size(75,520)
$OKButton.Size = New-Object System.Drawing.Size(75,23)
$OKButton.Text = "OK"

$OKButton.Add_Click(
   {
        foreach ($objItem in $objListbox.SelectedItems)
            {$Script:x += $objItem}
        $objForm.Close()
   })

$objForm.Controls.Add($OKButton)

$CancelButton = New-Object System.Windows.Forms.Button
$CancelButton.Location = New-Object System.Drawing.Size(150,520)
$CancelButton.Size = New-Object System.Drawing.Size(75,23)
$CancelButton.Text = "Cancel"
$CancelButton.Add_Click({$objForm.Close()})
$objForm.Controls.Add($CancelButton)

$objLabel = New-Object System.Windows.Forms.Label
$objLabel.Location = New-Object System.Drawing.Size(10,20) 
$objLabel.Size = New-Object System.Drawing.Size(280,20) 
$objLabel.Text = "Please make a selection from the list below:"
$objForm.Controls.Add($objLabel) 

$objListbox = New-Object System.Windows.Forms.Listbox 
$objListbox.Location = New-Object System.Drawing.Size(10,40) 
$objListbox.Size = New-Object System.Drawing.Size(260,20) 

$objListbox.SelectionMode = $listboxtype

$inputarray | ForEach-Object {[void] $objListbox.Items.Add($_)}

$objListbox.Height = 470
$objForm.Controls.Add($objListbox) 
$objForm.Topmost = $True

$objForm.Add_Shown({$objForm.Activate()})
[void] $objForm.ShowDialog()

Return $Script:x
}
After an extremely long hiatus........I am going to try this once again.  I have some good stuff coming. The first post I am working on will be posted in the next few days.  It is a powershell script for updating Lync 2013 Contact Groups from the server side.