Steve Goodman's Exchange Blog
5Apr/1114

How to get info about your ActiveSync, EWS and WebDAV clients before migrating to Exchange 2010

imageWhen you're moving from Exchange 2007 to Exchange 2010 there are a few gotchas that it's worth watching out for in the planning stages before you change over Client Access Namespaces or start migrating Mailboxes. If you don't you might find you have broken Exchange 2007 clients, due to the CAS changes, and find some clients can't connect after their mailboxes are migrated.

When it comes to the Windows version of Outlook, you can find out about currently logged-in clients using the Get-LogonStatistics command; however you also need to be aware of other clients using protocols like ActiveSync, Exchange Web Services and of course WebDAV.

ActiveSync clients will mostly be fine with CAS Namespace changes, as depending on version they should be automatically proxied from Exchange 2010 back to Exchange 2007 or should autodiscover; however some clients like the iPhone don't work properly and you need to consider a workaround.

Exchange Web Services clients, for the most part, shouldn't have issues thanks to AutoDiscover, however it's good to understand what clients you have out there already so you can test and plan for any issues. There's a number of iPhone apps that use EWS out there that your users may have bought and I've seen some funny issues myself with the EWS version Mac Mail on Snow Leopard that may require a client visit.

Finally, WebDAV. There's little to explain about WebDAV apart from it's not supported in Exchange 2010! You need to find these clients (think Entourage 2004 and 2008) and upgrade them.

Unfortunately there isn't anything built-in to Exchange 2007 or 2010 to examine this data, but the good news is it should all be available to you via the IIS log files. Whilst logparser is pretty good, personally I wanted the data collected and grouped all in one go ready to use. And that's where this script came from…

What information does the script output provide?

The output from the script is in CSV format, so it's easy to use in Excel for further data processing. The CSV file itself has the following fields:

Username: Logon name of the user
ActiveSyncUser: If the user uses an ActiveSync mobile device
ActiveSyncProxyUser: If the user is currently being proxied through to this client access server
ActiveSyncClients: A semi-colon separated list of the clients in use, eg. iPad;htchero;
ActiveSyncLastAccess: Last date found for ActiveSync use
EWSUser: If the user uses some sort of Exchange Web Services client
EWSPCOutlook: Version information if the user has the Windows version of Outlook 2007 or 2010
EWSMacMail: Version information if user has the EWS version of Mac Mail
EWSMacOutlook: Version information if the user has Mac Outlook 2011
EWSEntourage: Version information if the user has Entourage 2008, Web Services Edition
EWSOther: A semi-colon separated list of any other EWS clients the user has
EWSLastAccess: Last date found for EWS use
WebDavUser: If the user uses some sort of WebDAV Exchange client
WebDavClient: A semi-colon separated list of WebDAV client software and versions in use
WebDavLastAccess: Last date found for WebDAV use

In an actual export, the above looks a little bit like this (apart from the blurry usernames!)

image

How to use the script

Example one - Parses log files from the default log directory "C:\WINDOWS\system32\LogFiles\W3SVC1" to "C:\output.csv"

ExIISLogParser.ps1

Example two - Parses the last 30 days of log files from the current directory  to cas_results.csv in the current directory, and saves the state to state.xml in the current directory (could take a LONG time!)

ExIISLogParser.ps1 -LogFilePath ".\" -Days 30 -OutputCSVFile ".\cas_results.csv" -SaveStateFile ".\state.xml"

Limitations

So far, only tested with Exchange 2007 IIS log files.. Any probs, let me know.

Script Code

<#
    .SYNOPSIS
    Parses IIS Log files for records relating to ActiveSync, Exchange Web Services and WebDAV
   
    Steve Goodman
    .DESCRIPTION
    Looks at logs files and produces a CSV file with summary data listing:
    * Activesync clients, whether it is proxied by another CAS and ActiveSync device type
    * EWS clients with columns for PC Outlook, Mac Outlook and Entourage 2008 EWS Edition and other clients
    * WebDAV clients including client versions.
   
    .PARAMETER LogFilePath
    Path to base directory of IIS Log files, e.g. "C:\WINDOWS\system32\LogFiles\W3SVC1"
   
    .PARAMETER Days
    How many days log files to look back by
   
    .PARAMETER OutputCSVFile
    File to write CSV output to
   
    .PARAMETER SaveStateFile
    File to save or load internal state to. Useful when looking at multiple CAS servers or to update output later on based on only more recent logfiles
   
    .EXAMPLE
    Parses log files from the default log directory "C:\WINDOWS\system32\LogFiles\W3SVC1" to "C:\output.csv"
    .\ExIISLogParser.ps1
   
    .EXAMPLE
    Parses the last 30 days of log files from the current directory  to cas_results.csv in the current directory, and saves the state to state.xml in the current directory
    .\ExIISLogParser.ps1 -LogFilePath ".\" -Days 30 -OutputCSVFile ".\cas_results.csv" -SaveStateFile ".\state.xml"
   
    #>

param(
    [parameter(Position=0,Mandatory=$false,ValueFromPipeline=$false,HelpMessage="Full path to log file directory")][string]$LogFilePath = "C:\WINDOWS\system32\LogFiles\W3SVC1",
    [parameter(Position=1,Mandatory=$false,ValueFromPipeline=$false,HelpMessage="Last write date of logs to search back by")][int]$Days=0,
    [parameter(Position=2,Mandatory=$false,ValueFromPipeline=$false,HelpMessage="CSV file for output")][string]$OutputCSVFile="C:\output.csv",
    [parameter(Position=3,Mandatory=$false,ValueFromPipeline=$false,HelpMessage="Script state XML file")][string]$SaveStateFile
   
    )
if (!(Test-Path $LogFilePath))
{
    throw "LogFilePath does not exist"
}
if ((Test-Path $OutputCSVFile))
{
    throw "OutputCSVFile already exists"
}
[hashtable]$Users = @{}
if ($SaveStateFile)
{
    if ((Test-Path $SaveStateFile))
    {
        $Users = Import-Clixml -Path $SaveStateFile
    }
}
$EarliestLogDate = (Get-Date).Subtract([timespan]"$($Days).00:00").Date
[array]$Files = Get-Item -Path "$($LogFilePath)\*.log" | Where {$_.LastWriteTime -gt $EarliestLogDate}
for ($i = 0; $i -lt $Files.Count; $i++)
{
    Write-Progress -id 1 -activity "Overall Progress" -status "File $($i) of $($Files.Count)" -percentComplete (($i/$Files.Count*100)*0.9);
    Write-Progress -id 2 -activity "Log $($Files[$i].Name)" -status "Loading" -percentComplete 0
    $Log = Get-Content $Files[$i] | Where {$_ -like "*/Microsoft-Server-ActiveSync/*" -or $_ -like "*/EWS/*" -or $_ -like "*/exchange/*"}
    for ($j = 0; $j -lt $Log.Count; $j++)
        {
        Write-Progress -id 2 -activity "Current Log $($Files[$i].Name)" -status "Working" -percentComplete ($j/$Log.Count*100);
        # Clear Username
        $Username=$null
       
        # Split up log line
        $arrLog = $Log[$j].Split(" ");
       
        # Extract username first
        if ($arrLog[5] -eq "/Microsoft-Server-ActiveSync/Proxy")
        {
            foreach ($QSPart in $arrLog[6].Split("&"))
            {
                if ($QSPart -like "User=*")
                {
                    $Username =($QSPart.Split("="))[1]
                }
            }
           
        } else {
            # Not proxied, just get the username provided direct
            $Username = $arrLog[8]  
        }
       
        # Only process if it's an authenticated user
        if ($Username)
        {
            # Take off domain or UPN suffix
            if ($Username -like "*@*")
            {
                $Username = ($Username.Split("@"))[0]
            } elseif ($Username -like "*\*") {
                $Username = ($Username.Split("\"))[1];
            } elseif ($Username -like "*%40*") {
                $Username = ($Username.Split("%40"))[0]
            } elseif ($Username -like "*/*") {
                $Username = ($Username.Split("/"))[1]
            }
           
            # Make username lower case
            $Username = $Username.ToLower()
           
            # Update data for user
            switch -wildcard ($arrLog[5])
            {
                "/EWS/*"
                {
                    # Get EWS Client
                    $EWSClient = $arrLog[10].Replace("+"," ")
                    # Check if already found or create new hashtable item
                    if (!$Users.Contains($Username))
                    {
                        [hashtable]$obj = @{ActiveSyncUser=0; ActiveSyncProxyUser=0; ActiveSyncClients=@{}; ActiveSyncLastAccess=""; EWSUser=0; EWSClients=@{}; EWSLastAccess=""; WebDavUser=0; WebDavClients=@{}; WebDavLastAccess=""}
                        $Users.Add($Username,$obj)
                    }
                    # Set variables
                    $Users[$Username]["EWSUser"]=1
                   
                    if (!$Users[$Username]["EWSClients"].Contains($EWSClient))
                    {
                        $Users[$Username]["EWSClients"].Add($EWSClient,1)
                    }
                    $Users[$Username]["EWSLastAccess"]=$arrLog[0]
                    break
                }
                "/Microsoft-Server-ActiveSync/*"
                {
                    # Check if already found or create new hashtable item
                    if (!$Users.Contains($Username))
                    {
                        [hashtable]$obj = @{ActiveSyncUser=0; ActiveSyncProxyUser=0; ActiveSyncClients=@{}; ActiveSyncLastAccess=""; EWSUser=0; EWSClients=@{}; EWSLastAccess=""; WebDavUser=0; WebDavClients=@{}; WebDavLastAccess=""}
                        $Users.Add($Username,$obj)
                    }
                    # Set variables
                   
                    # Is a ActiveSync user?
                    $Users[$Username]["ActiveSyncUser"]=1
                    # Is a proxy user?
                    if ($arrLog[5] -eq "/Microsoft-Server-ActiveSync/Proxy")
                    {
                        $Users[$Username]["ActiveSyncProxyUser"]=1
                    }
                    # Client Info
                    foreach ($QSPart in $arrLog[6].Split("&"))
                    {
                        if ($QSPart -like "DeviceType=*")
                        {
                            $ASClient = ($QSPart.Split("="))[1]
                            if (!$Users[$Username]["ActiveSyncClients"].Contains($ASClient))
                            {
                                $Users[$Username]["ActiveSyncClients"].Add($ASClient,1)
                            }
                        }
                    }
                    # Last Access Date
                    $Users[$Username]["ActiveSyncLastAccess"]=$arrLog[0]
                    break
                }
                "/exchange/*"
                {
                    # Check if already found or create new hashtable item
                    if (!$Users.Contains($Username))
                    {
                        [hashtable]$obj = @{ActiveSyncUser=0; ActiveSyncProxyUser=0; ActiveSyncClients=@{}; ActiveSyncLastAccess=""; EWSUser=0; EWSClients=@{}; EWSLastAccess=""; WebDavUser=0; WebDavClients=@{}; WebDavLastAccess=""}
                        $Users.Add($Username,$obj)
                    }
                    # Set variables
                    $Users[$Username]["WebDavUser"]=1
                    $WDClient = $arrLog[10].Replace("+"," ")
                    if (!$Users[$Username]["WebDavClients"].Contains($WDClient))
                    {
                        $Users[$Username]["WebDavClients"].Add($WDClient,1)
                    }
                    $Users[$Username]["WebDavLastAccess"]=$arrLog[0]
                    break
                }
            }
        }
    }
}
Write-Progress -id 1 -activity "Overall Progress" -status "Preparing Output" -percentComplete 95;
[array]$Output=$null
$Users.GetEnumerator() | Foreach {
    $OutputItem = New-Object Object
    $OutputItem | Add-Member NoteProperty Username  $_.Key
    $OutputItem | Add-Member NoteProperty ActiveSyncUser $_.Value["ActiveSyncUser"]
    $OutputItem | Add-Member NoteProperty ActiveSyncProxyUser $_.Value["ActiveSyncProxyUser"]
    $ActiveSyncClients = $null
    $_.Value["ActiveSyncClients"].GetEnumerator() | % { $ActiveSyncClients += "$($_.Key); "}
    $OutputItem | Add-Member NoteProperty ActiveSyncClients $ActiveSyncClients
    $OutputItem | Add-Member NoteProperty ActiveSyncLastAccess $_.Value["ActiveSyncLastAccess"]
    $OutputItem | Add-Member NoteProperty EWSUser $_.Value["EWSUser"]
    $EWSPCOutlook = ""
    $EWSMacMail = ""
    $EWSMacOutlook = ""
    $EWSEntourage = ""
    $EWSOther = ""
    $_.Value["EWSClients"].GetEnumerator() | foreach {
        $EWSClient = $_.Key
        switch -wildcard ($EWSClient)
        {
            "Microsoft Office*"
            {
                $EWSPCOutlook=$EWSClient
                break
            }
            "Mac*ExchangeWebServices*"
            {
                $EWSMacMail=$EWSClient
                break
            }
            "MacOutlook*"
            {
                $EWSMacOutlook=$EWSClient
                break
            }
            "Entourage*"
            {
                $EWSEntourage=$EWSClient
                break
            }
            default
            {
               $EWSOther+="$($EWSClient); "
            }
           
        }
    }
    $OutputItem | Add-Member NoteProperty EWSPCOutlook $EWSPCOutlook
    $OutputItem | Add-Member NoteProperty EWSMacMail $EWSMacMail
    $OutputItem | Add-Member NoteProperty EWSMacOutlook $EWSMacOutlook
    $OutputItem | Add-Member NoteProperty EWSEntourage $EWSEntourage
    $OutputItem | Add-Member NoteProperty EWSOther $EWSOther
    $OutputItem | Add-Member NoteProperty EWSLastAccess $_.Value["EWSLastAccess"]
    $OutputItem | Add-Member NoteProperty WebDavUser $_.Value["WebDavUser"]
    $WebDavClients=$null
    $_.Value["WebDavClients"].GetEnumerator() | % { $WebDavClients += "$($_.Key); "}
    $OutputItem | Add-Member NoteProperty WebDavClients $WebDavClients
    $OutputItem | Add-Member NoteProperty WebDavLastAccess $_.Value["WebDavLastAccess"]
    $Output += $OutputItem
}

if ($SaveStateFile)
{
    $Users = Export-Clixml -Path $SaveStateFile
}

$Output[1..($Output.Count)] | Select Username,ActiveSyncUser,ActiveSyncProxyUser,ActiveSyncClients,ActiveSyncLastAccess,EWSUser,EWSPCOutlook,EWSMacMail,EWSMacOutlook,EWSEntourage,EWSOther,EWSLastAccess,WebDavUser,WebDavClients,WebDavLastAccess | Export-Csv -Path $OutputCSVFile -NoClobber -NoTypeInformation

Script Download

Download ExIISLogParser.zip

3Feb/112

Creating Shared Calendars on Exchange 2010

imageWith Public Folders slowly, painfully making their way out from Exchange, you might find a need to replace shared calendars that traditionally you would have used a public folder for. While Sharepoint is a great option, using Shared Mailboxes isn't a bad idea either.

One of the issues with creating a Shared Mailbox for a Calendar is that as well as creating the mailbox, often you'll need to add folder permissions to the Calendar for Editors, Reviewers and perhaps publish iCal and Web URLs for subscriptions. And the chances are, you probably don't want the Shared Mailbox auto-mapped to the owner's Outlook client.

To make the process a little more simple, I've written a short script that wraps up all this into a single script, allowing you to specify the initial owners (who get full access to the mailbox) and optionally an initial set of people who can edit the calendar and an initial list of calendar viewers. You can choose to publish the iCal and Web URLs (which are provided in the script output), and the manager and department fields are auto-populated from the first owner specified.

Finally, you'll also find a basic set of documentation (in word format, so you can alter to your own needs) that you can provide to the users to help them get started managing their new shared calendar.

Here's a quick example of the script's usage, in it's simplest form:

New-SharedCalendar.ps1 -Name "Test Calendar" -Owners steve,lisa -Editors isabelle,peter -Reviewers liz,drew

And using the full set of options:

New-SharedCalendar.ps1 -Name "Shared Calendar" -SamAccountName sharedcalendar -UserprincipalName sharedcalendar@sgdev.stevieg.org -Database DB01 -OrganizationalUnit sgdev.stevieg.org/People -Owners sgoodman,fstone -Editors epitts,csutton -Reviewers fcarson -WebPublish:$True

The full script follows, along with a download link for a zip file containing the script and user documentation:

<#
    .SYNOPSIS
    Creates a mailbox for use as a shared calendar
   
    Steve Goodman
    .DESCRIPTION
    Creates a shared mailbox, assigns owner(s), with full access, along with editors and viewers. Restricts mail to authenticated users.
    Additionally generates a HTML/iCal published URL if specified.
   
    .PARAMETER Name
    Name of the Shared Calendar. Used for the Display Name
   
    .PARAMETER SamAccountName
    Optional - Pre-Windows 2000 Login name to use when creating the mailbox. If not specified, uses defaults
   
    .PARAMETER UserPrincipalName
    Optional - User Principal Name (UPN) to use when creating the mailbox. If not specified, uses defaults
   
    .PARAMETER Database
    Optional - Mailbox Database to use
   
    .PARAMETER OrganizationalUnit
    Optional - AD Organizational Unit to use when creatng the mailbox. If not specified, uses defaults.
   
    .PARAMETER Owners
    List of the people who should be the owners of the mailbox. The first will be specified as the "manager" field, their department used, and all owners will be given "Full Access" mailbox permissions. The Auto-Mapping will be removed.
   
    .PARAMETER Editors
    Optional - List of the people who should have editor access on the Calendar
   
    .PARAMETER Reviewers
    Optional - List of the people who should have viewer access on the Calendar
   
    .PARAMETER WebPublish
    Optional - Publish the Calendar via private iCal and HTML URLs. Defaults to False. Requires Default Sharing Policy to allow Web Publishing.
   
    .EXAMPLE
    Creates a new calendar, allowing defaults:
    New-SharedCalendar.ps1 -Name "Test Calendar" -Owners steve,lisa -Editors isabelle,peter -Reviewers liz,drew
   
    .EXAMPLE
    Creates a new calendar, many options set
    New-SharedCalendar.ps1 -Name "Shared Calendar" -SamAccountName sharedcalendar -UserprincipalName sharedcalendar@sgdev.stevieg.org -Database DB01 -OrganizationalUnit sgdev.stevieg.org/People -Owners sgoodman,fstone -Editors epitts,csutton -Reviewers fcarson -WebPublish:$True
   
    #>

param(
    [parameter(Position=0,Mandatory=$true,ValueFromPipeline=$false,HelpMessage="Calendar Name")][string]$Name,
    [parameter(Position=1,Mandatory=$false,ValueFromPipeline=$false,HelpMessage="Logon Name (SAM Account Name)")][string]$SamAccountName,
    [parameter(Position=2,Mandatory=$false,ValueFromPipeline=$false,HelpMessage="Logon Name (User Principal Name)")][string]$UserPrincipalName,
    [parameter(Position=3,Mandatory=$false,ValueFromPipeline=$false,HelpMessage="Mailbox Database")]$Database,
    [parameter(Position=4,Mandatory=$false,ValueFromPipeline=$false,HelpMessage="Organizational Unit")][string]$OrganizationalUnit,
    [parameter(Position=5,Mandatory=$true,ValueFromPipeline=$false,HelpMessage="Mailbox Owners")][array]$Owners,
    [parameter(Position=6,Mandatory=$false,ValueFromPipeline=$false,HelpMessage="Calendar Editors")][array]$Editors,
    [parameter(Position=7,Mandatory=$false,ValueFromPipeline=$false,HelpMessage="Calendar Reviewers")][array]$Reviewers,
    [parameter(Position=8,Mandatory=$false,ValueFromPipeline=$false,HelpMessage="Publish (privately) via the web?")][bool]$WebPublish=$false
    )

# Check all pre-reqs
if ((Get-PSSnapin Microsoft.Exchange.Management.PowerShell.Admin -ErrorAction SilentlyContinue))
{
    throw "Exchange 2007 Management Console Not Supported"
}
if (!(Get-Command New-Mailbox -ErrorAction SilentlyContinue))
{
    throw "Please launch the Exchange 2010 Management Shell"
}

# Setup splatting hashtable
$NewSharedMailbox = @{
    "Shared" = $True
    "Name" = $Name
}

# Check parameters OK and add relevant parameters to splatting hashtable
$Recipient=Get-Recipient $Name -ErrorAction SilentlyContinue
if ($Recipient)
{
    $Recipient
    throw "Recipient $($Name) Exists (See above)"
}
if ($SamAccountName)
{
    $Recipient=Get-Mailbox $SamAccountName -ErrorAction SilentlyContinue
    if ($Recipient)
    {
        $Recipient
        throw "Recipient $($SamAccountName) Exists (See above)"
    }
    $NewSharedMailbox.Add("SamAccountName",$SamAccountName)
}
if ($UserPrincipalName)
{
    $Recipient=Get-Mailbox $UserPrincipalName -ErrorAction SilentlyContinue
    if ($Recipient)
    {
        $Recipient
        throw "Recipient $($UserPrincipalName) Exists (See above)"
    }
    $NewSharedMailbox.Add("UserPrincipalName",$UserPrincipalName)
}
if ($Database)
{
    $MailboxDatabase=Get-MailboxDatabase $Database -ErrorAction SilentlyContinue
    if (!$MailboxDatabase)
    {
        throw "Mailbox Database $($Database) not found"
    }
    $NewSharedMailbox.Add("Database",$Database)
}
if ($OrganizationalUnit)
{
    $objOrganizationalUnit = Get-OrganizationalUnit $OrganizationalUnit -ErrorAction SilentlyContinue
    if (!$objOrganizationalUnit)
    {
        throw "Organizational Unit $($OrganizationalUnit) not found"
    }
    $NewSharedMailbox.Add("OrganizationalUnit",$OrganizationalUnit)
}
if ($Owners.Count -eq 0)
{
    throw "You must specify at least one owner"
}
foreach ($Owner in $Owners)
{
    if (!(Get-Mailbox $Owner -ErrorAction SilentlyContinue))
    {
        throw "Owner mailbox $($Owner) not found"
    }
}
if ($Editors)
{
    foreach ($Editor in $Editors)
    {
        if (!(Get-Mailbox $Editor -ErrorAction SilentlyContinue))
        {
            throw "Editor mailbox $($Editor) not found"
        }
    }
}
if ($Reviewers)
{
    foreach ($Reviewer in $Reviewers)
    {
        if (!(Get-Mailbox $Reviewer -ErrorAction SilentlyContinue))
        {
            throw "Reviewer mailbox $($Reviewer) not found"
        }
    }
}

# Create mailbox
Write-Host -ForegroundColor Green "Creating Shared Calendar Mailbox"
$Mailbox = New-Mailbox @NewSharedMailbox
if (!$Mailbox)
{
    throw "An error occurred creating the shared calendar mailbox"
}
$DomainController = $Mailbox.OriginatingServer
$Mailbox

# Set Owner Details including Department to match the first specified Owner
Write-Host -ForegroundColor Green "Setting Shared Calendar Mailbox Owner, Department and Description"
$Mailbox | Set-User -Manager (Get-User $Owners[0]) -Department ((Get-User $Owners[0]).Department) -DomainController $DomainController

# Set Description
$LDAPUser = [ADSI]"LDAP://$($DomainController)/$($Mailbox.DistinguishedName)"
$LDAPUser.description = "Shared Calendar"
$LDAPUser.SetInfo()

$LDAPUser = [ADSI]"LDAP://$($DomainController)/$($Mailbox.DistinguishedName)"
Write-Host "Manager: $($LDAPUser.manager)"
Write-Host "Department: $($LDAPUser.department)"
Write-Host "Description: $($LDAPUser.description)"

# Set authenticated mail only
$Mailbox | Set-Mailbox -RequireSenderAuthenticationEnabled:$true


# Set Owner Permissions
Write-Host -ForegroundColor Green "Adding Shared Calendar Mailbox Owner Permissions and removing Outlook auto-mapping"
foreach ($Owner in $Owners)
{
    # Add Permission
    $Mailbox | Add-MailboxPermission -User $Owner -AccessRights FullAccess -DomainController $DomainController
    # Remove Auto-Mailbox mapping
    $LDAPUser=[ADSI]"LDAP://$($DomainController)/$($Mailbox.DistinguishedName)"
    $LDAPUser.msExchDelegateListLink.Remove(((Get-Mailbox $Owner).DistinguishedName))
    $LDAPUser.SetInfo()
}

if ($Editors -or $Reviewers)
{
    # Wait until Mailbox is ready before adding folder permissions
    Write-Host -ForegroundColor Green "Waiting until mailbox folder structure is available before adding folder permissions"
    $MailboxReady=$False
    while ($MailboxReady -eq $False)
    {
        Write-Host -NoNewline "."
        $Result = Get-MailboxFolderStatistics $Mailbox -ErrorAction SilentlyContinue
        if ($Result)
        {
            $MailboxReady = $True
        }
        sleep 5
    }
    Write-Host
}
# Set Editor Permissions
if ($Editors)
{
    Write-Host -ForegroundColor Green "Adding Editor permissions to Calendar"
    foreach ($Editor in $Editors)
    {
        Add-MailboxFolderPermission "$($Mailbox.SamAccountName):\Calendar" -User $Editor -AccessRights Editor -DomainController $DomainController
    }
}

# Set Reiewer Permissions
if ($Reviewers)
{
    Write-Host -ForegroundColor Green "Adding Reviewer permissions to Calendar"
    foreach ($Reviewer in $Reviewers)
    {
        Add-MailboxFolderPermission "$($Mailbox.SamAccountName):\Calendar" -User $Reviewer -AccessRights Reviewer -DomainController $DomainController
    }
}

# Publish Calendar
if ($WebPublish)
{
    if (((Get-SharingPolicy | Where {$_.Default -eq $True}).Domains|Where {$_.Domain -eq "anonymous"})) {
        Write-Host -ForegroundColor Green "Publishing Calendar using private URL"
        Set-MailboxCalendarFolder -Identity "$($Mailbox.SamAccountName):\Calendar" -DetailLevel FullDetails -PublishDateRangeFrom OneYear -PublishDateRangeTo OneYear -PublishEnabled:$true -SearchableUrlEnabled:$false -DomainController $DomainController
        $MailboxCalendarFolder = Get-MailboxCalendarFolder -Identity "$($Mailbox.SamAccountName):\Calendar" -DomainController $DomainController
        Write-Host -ForegroundColor Yellow -NoNewline "Web URL: "
        Write-Host $MailboxCalendarFolder.PublishedCalendarUrl
        Write-Host -ForegroundColor Yellow -NoNewline "iCal URL: "
        Write-Host $MailboxCalendarFolder.PublishedICalUrl
    } else {
        Write-Host -ForegroundColor Yellow "Skipping Web/iCal Publishing because Default Sharing Policy does not allow web publishing (to anonymous domains)"
    }
}

Download the Script and User Documentation

6Jan/1114

How to batch optimize your Exchange GAL Photos before importing to Active Directory

imageAs I've covered in previous articles, it's possible to import contact photos into Active Directory and display them via the Exchange Global Address List in Outlook clients. This is fully supported against Exchange 2010 and Outlook 2010, however I've covered in previous articles how to import GAL photos into Exchange 2007 and how to enable your Outlook 2003 and Outlook 2007 clients to display the photos via the use of the Outlook Social Connector.

When you import pictures into the Active Directory it's important to make sure you optimise the pictures first. The optimal dimensions for the GAL photos is 96x96 pixels, and the maximum file size for the photos is 10K. For 10,000 users, that adds at least 100MB to the database size, more so if the picture needs replicating to the Global Catalog. By properly optimizing your photos before you import, not only will the pictures look better by virtue of being the correct dimensions, but you can also squish them down to a much smaller size.

Whilst you can use Photoshop, Microsoft Paint or other tools to accomplish this, it's actually fairly easy to batch optimize large numbers of photos at the command line, using an open source product called ImageMagick. It's a tool you can use to create, edit and convert photos. To convert our images we only need to use a small subset of it's conversion functionality.

Optimising a single photo

Later on in the article I've packaged up the script and redistributable binaries for ImageMagick, but before we get there, let's have a quick look at the process required to convert a single image file.

First of all, we need the actual ImageMagick software. I've used the portable version from the ImageMagick website and from that, just copied out the convert.exe and vcomp100.dll files, because that's all we need:

image

Then, with a larger file we want to convert I've used the following options with convert.exe to produce an optimized 96x96 ~2K jpeg file:

.\convert.exe -quality 50 -depth 8 -strip -thumbnail 96x96^ -gravity Center -crop 96x96+0+0 .\input.jpg .\output.jpg

The options are fairly straightforward. Firstly, we set the jpeg quality. The best compromise between size and quality I've found is 50, with 100 producing a 10K+ file, and much lower producing blocky output. The colour depth is reduced and any colour profiles or file comments are stripped out. We resize using the "thumbnail" resize option, which is a great alternative to the "resize" option that's optimised for speed and also removes the image profile. The resize is set to a minimum of 96x96, then we do a centred crop to remove any excess pixels.

Batch optimising photos using GALBatchConvert.ps1

So now we've seen how to convert a single photo, what about if you've got a folder of 10, 100 or even 10,000? That's (naturally) where Powershell comes in. I've put together a little script that you can use to perform the batch conversion.

GALBatchConvert.ps1 takes 2 mandatory parameters -InputFolder, which is the folder with the original photos and -OutputFolder, which is an empty folder to write the optimised files. As a third parameter, you can specify -Quality to override the output jpeg quality with a value between 1 and 100.

Here's a quick example of it in use:

.\GALBatchConvert.ps1 -InputFolder .\Input -OutputFolder .\Output
Converting C:\Users\steve\Documents\Input\isabelle.jpg
Converting C:\Users\steve\Documents\Input\lisa.jpg
Converting C:\Users\steve\Documents\Input\steve.jpg
Converting C:\Users\steve\Documents\Input\steve2.jpg

After the conversion, the output folder contains optimised 96x96, 2KB jpg files and the original files are retained in the input folder, unmodified:

image

The full script and redistributable binaries required for ImageMagick can be downloaded below:

Download Script and ImageMagick

As usual, if you have any problems, questions or suggestions, let me know in the comments section below.

3Jan/1118

Setup and use the GAL Photos feature using Exchange 2007 [Updated]

If you're not using Exchange Server 2010 yet, then you might wonder if you can take advantage of some of the newer features available in Outlook 2010. While mailtips, personal archives and automatic mailbox mappings aren't possible, you can make use of the GAL photo feature.

It's fairly painless to enable this and the setup steps for your organisation are the same as Exchange 2010. Once you've sorted the org pre-requisites, then the only piece missing is the cmdlets to import the photos. Although you can use Sharepoint 2010 to do this (and delegate it to end-users), I thought it would be useful to have a simple drop-in Powershell script that can accomplish this in the same way as you can in Exchange 2010.

Getting Active Directory Ready

The AD pre-requisites are fairly simple. You need to ensure the attribute the photo is stored in is replicated to the Global Catalog, and ensure you have either a Windows Server 2008 or later Domain Controller in your domain, or simply prepare your Windows 2003 forest schema for 2008:

  1. If you are in a multi domain environment, follow "A minor schema change" in the Exchange Team blog post GAL Photos in Exchange 2010 and Outlook 2010. If it's a single-domain environment you're all set.
  2. If you don't have at least one Windows 2008 / 2008 R2 Domain Controller, you need to run adprep /forestprep from the 2008/2008 R2 setup CD as described in the article Prepare a Windows 2000 or Windows Server 2003 Forest Schema for a Domain Controller That Runs Windows Server 2008 or Windows Server 2008 R2. You don't need to install a Windows 2008 / 2008 R2 domain controller, just update the schema. More details on why this is required here.

Importing Photos

In Exchange 2010, you use the Import-RecipientDataProperty cmdlet to import photos. This command isn't provided in Exchange 2007 so I've written a short Powershell script that can do this for you. It's usage is almost identical to the Import-RecipientDataProperty cmdlets, except that you don't need to specify which property to import (obviously) and you don't need to do submit the file as a byte-encoded array - you just give the filename.

Usage is simple:

.\Import-Picture.ps1 -Identity stevegoodman -Path .\Steve_96x96.jpg

Then, once AD has replicated, the Offline Address Book is updated (for example, using Update-OfflineAddressBook "Default Offline Address Book"), and clients have downloaded the new OAB, photos should now show in Outlook 2010 (or 2003 and 2007 - check my previous article!)

Removing Photos [New]

If you want to remove a photo from a contact, you need to clear the relevant attribute in Active Directory. As above, I've provided a script that can do this - for example, to clear the GAL picture:

.\Clear-Picture.ps1 -Identity stevegoodman

The Import-Picture.ps1 and Clear-Picture.ps1 Scripts

param($Identity,$Path);
#
# Import a JPEG file into Active Directory for use as the Exchange / Outlook GAL Photo
# Best results - Under 10K, 96 x 96
#
# Steve Goodman
if (!$Identity)
{
    throw "Identity Missing";
}
if (!$Path)
{
    throw "Path Missing";
}
if (!(Get-Command Get-User))
{
    throw "Exchange Management Shell not loaded";
}
$User = Get-User $Identity -ErrorAction SilentlyContinue
if (!$User)
{
    throw "User $($Identity) not found";
}
if (!(Test-Path -Path $Path))
{
    throw "File $($Path) not found";
}
$FileData = [Byte[]]$(Get-Content -Path $Path -Encoding Byte -ReadCount 0);
if($FileData.Count -gt 10240)
{
    throw "File size must be less than 10K";
}
$adsiUser = [ADSI]"LDAP://$($User.OriginatingServer)/$($User.DistinguishedName)";
$adsiUser.Put("thumbnailPhoto",$FileData);
$adsiUser.SetInfo()
param($Identity);
#
# Clear the GAL Picture attribute
#
# Steve Goodman
if (!$Identity)
{
    throw "Identity Missing";
}
if (!(Get-Command Get-User))
{
    throw "Exchange Management Shell not loaded";
}
$User = Get-User $Identity -ErrorAction SilentlyContinue
if (!$User)
{
    throw "User $($Identity) not found";
}
$adsiUser = [ADSI]"LDAP://$($User.OriginatingServer)/$($User.DistinguishedName)";
$adsiUser.PutEx(1,"thumbnailPhoto",$null)
$adsiUser.SetInfo()

Download E2007-Picture.zip

Finally, if you have any problems or questions, let me know as usual in the comments…