Creating a Catch-all Mailbox using Transport Rules

There are as many reasons to use catch-all mailboxes as there are against, and of course spam is one of the biggest concern. Nevertheless people are still asking for this feature, and therefore I want to show that it is possible also using Exchange Online in Office 365.

I will achieve this by using Transport Rules to control our mail flow. Transport Rules are a very flexible and powerful solution, yet easy to maintain. We have previously here at 365lab.net written a few posts of Transport Rules, but I still think that they deserve a little more attention.

Now to my demo. After establishing a PowerShell session to Exchange Online the first thing we have to do is to change our domain type to Internal Relay.

$domain = '365lab.net'
Set-AcceptedDomain -Identity $domain -DomainType InternalRelay

Next we need a distribution list that contains all our mailboxes. The idea is to redirect all messages sent to unknown addresses, and this distribution list will contain all known addresses, which mean we have something to use in our Transport Rule. Here I prefer to use a Dynamic Distribution List since they don’t require any maintenance after the initial configuration. Maybe you already have a distribution list that you want to re-use instead. If you want to this distribution list can be hidden from the Global Address List.

New-DynamicDistributionGroup `
	-Name 'Everyone' `
	-PrimarySmtpAddress "everyone@$domain" `
	-IncludedRecipients AllRecipients

Finally we create the Transport Rule that will do the actual redirection of the Mail Flow. In my example I want to redirect all emails to an already existing mailbox, $targetAddress.

$targetAddress = "contact@$domain"
New-TransportRule `
	-Name "Catch-all $domain" `
	-RecipientDomainIs $domain `
	-ExceptIfSentToMemberOf "everyone@$domain" `
	-RedirectMessageTo $targetAddress

All emails sent to the domain $domain is hit by this rule, as specified by the RecipientDomainIs parameter. I am also using the Everyone Distribution List in the ExceptIfSentToMemberOf parameter, so that my rule doesn’t hit existing mailboxes. It would also create a loop if messages addressed to the target mailbox were processed, they would be forwarded to the mailbox itself infinitely.

Now we are all set. All messages sent to non-existent addresses in the domain $domain will now be delivered to the mailbox $targetAddress.

/ Andreas

Troubleshooting TLS in an Exchange Online Hybrid Deployment

One of the prerequisites for having a Hybrid relationship established between your on-prem Exchange environment and Office 365 is to have a functioning mail flow using TLS. The Hybrid Configuration Wizard automatically creates the inbound and outbound connectors required both in your on-prem environment and in Office 365 as a part of the setup, and this is used for secure mail transfer between the two environments.

If TLS fails your mail flow will suddenly stop, and outgoing emails are stuck in the queue with error message 451 4.4.0 Primary target IP address responded with: “451 5.7.3 Must issue a STARTTLS command first.”. You may not even be able to complete the Hybrid Configuration Wizard, you just get an error message stating that Subtask ValidateConfiguration execution failed: Configure Mail Flow.

Several blogs on the Internet addresses these problems, but must of them show you the commands how to turn TLS off. This is not something that I recommend! A problem is not solved by hiding it’s symptoms, always try to find the root cause.

There are several possible reasons for malfunctioning TLS. A good start in the troubleshooting is to use good old telnet.exe and connect to smtp.office365.com on port 25. By simply sending the ehlo command you can easily see if the server is accepting TLS connections. If you get a 250-STARTTLS response the problem is most likely with your certificate. It may not be configured to be used with SMTP, or is longer valid. Also make sure that you have installed the latest Root Certificate Updates from Windows Update.

tls1

A response from the SMTP server without the STARTTLS extension listed might indicate that your IP address is on a blacklist. Use one of the online tools available for free to check the status of your IP address. In your telnet session you will find the IP address you are connecting from.

This IP address must also be in list of Sender IP Addresses in the Hybrid Mail Flow Inbound Connector in Office 365 created by the Hybrid Configuration Wizard. If it isn’t there it must be added. If the IP is incorrect the connector will not be used, and the mail flow will use the MX record instead, without enforcing TLS.

tls2

Another result from your telnet session could be this:

tls3

In this case a firewall is configured to filter some protocols, which effectively stops all TLS communication. In for example Cisco firewalls the solution is to turn off ESMTP inspection.

This is not a complete guide on how to solve your TLS problems, but it shows some common solutions, and hopefully it gives you some input to start your troubleshooting.

/ Andreas

Exchange Online: How to create a dirsynced Resource Mailbox

The idea with DirSync is to keep your user administration on-prem. A problem arise when you decomission the on-premises Exchange server and want to create a Shared Mailbox or a Resource Mailbox. There is no simple way to create such mailbox without assigning a license. It is possible to create a new regular user, assign a license, and then convert it to a Shared Mailbox or a Resource Mailbox, but the drawback with this method is that it requires a license during the process. On the other hand your user account will be fully managed in your on-prem environment, and the goal is achieved.

Another possibility is to create a Resource Mailbox with a Cloud Identity, and then connect it to an account synced from your Active Directory. This is what I will show you now. Lets start with disabling DirSync. This step is not necessary, but we might get some problems if our accounts are synced before they are ready.

Stop-Service MSOnlineSyncScheduler

Then we create a user account in Active Directory that we will later sync to Office 365:

Import-Module ActiveDirectory
$ADUserProperties = @{
    Name =               'Meeting Room 1'
    Path =               'CN=Users,DC=365lab,DC=net'
    SamAccountName =     'room1'
    UserPrincipalName =  'room1@365lab.net'
    DisplayName =        'Meeting Room 1'
    EmailAddress =       'room1@365lab.net'
    OtherAttributes = @{
        ProxyAddresses = 'SMTP:room1@365lab.net'
    }
}
$ADUser = New-ADUser @ADUserProperties -PassThru

The next step is to create a new Resource Mailbox in Office 365. This can be done either with GUI or PowerShell, I prefer PowerShell.

$O365cred = Get-Credential
$O365sess = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://ps.outlook.com/powershell -Credential $O365cred -Authentication Basic -AllowRedirection
$importcmd = Import-PSSession $O365sess

$O365UserProperties = @{
    DisplayName =        'Meeting Room 1'
    Name =               'room1'
}

$RoomMailbox = New-Mailbox @O365UserProperties -Room

Now we have two separate accounts, one in Active Directory with the managed attributes, and one in the cloud that we want to connect to our on-prem identity. The connection is done by populating the ImmutableID attribute with the corresponding ObjectGuid from Active Directory. Also, we change the UserPrincipalName in Office 365 to match our domain account.

$ObjectGuid = $ADUser.objectGuid
$ImmutableId = [System.Convert]::ToBase64String($ObjectGuid.ToByteArray())

Import-Module MSOnline
Connect-MsolService 

Set-MsolUserPrincipalName -UserPrincipalName $RoomMailbox.UserPrincipalName -NewUserPrincipalName $ADUser.UserPrincipalName -ImmutableId $ImmutableId

Now our UserPrincipalNames are the same in both our Active Directory and in Office 365, and we have linked then together using the ObjectGuid/ImmutableId. Time to start our DirSync service again and force a synchronization to run.

Start-Service MSOnlineSyncScheduler

Import-Module DirSync
Start-OnlineCoexistenceSync -FullSync

Now the Cloud Identity is converted to a DirSynced Identity, and the attributes in Active Directory are syned to our new Resource Mailbox. From now on all user administration tasks for this account can be managed in our on-prem Active Directory.

/ Andreas

Lync Online: External Communiations/Federation not working – “Do it over again…”

Working with IT can sometimes be a pain. You do everything by the book, but it is still not working. (if I got a penny every time i ran in to that… 🙂 )

Enabling External Communiations/Federation in Lync Online is one of the easiest things to do, enable a tick box and make sure that you have all dns records added for Lync Online to work properly.

2014-04-07 07-58-15 2014-04-07 07-56-50

What if its not working anyway? Recently that was exactly my case, I had done the extremely difficult operations above, yet external federation was not working.

Solution
After troubleshooting forth and back by enabling logging in the Lync clients, it seemed like my external communiations settings had not been provisioned to back end. The solution I tried then was the following:
Simply disable external communications in the portal and wait 24 hours before you enable it again.
2014-04-07 08-03-50
24%20hours_lit 2014-04-07 08-08-03
That did the trick! Sometimes there are easy ways to solve problems. 🙂

/Johan

Automating the creation of Lync Online DNS Records

Recently I implemented Lync Online with a customer that had a lot of custom domain names. Adding the required DNS records manually would literary take hours, but luckily the customer used Windows DNS both internally and externally, so it was easy for me to write a script to add these DNS records.

In my case both DNS servers (internal and external) are running Windows Server 2008 R2. That gives me the option to use dnscmd.exe to add the records. Here is the script I used to add the records. Please note that you may need to modify the script if you are using subdomains.

#Get all domains registered in Office 365
Import-Module MSOnline
Connect-MsolService
$domains = Get-MsolDomain | Where-Object { $_.Name -notlike '*.onmicrosoft.com' }
$dnsserver = 'srv-dns01'

#Loop through all domains and add the required DNS records for all domains
foreach ($domain in $domains) {
    Write-Verbose "Adding records for domain $($domain.Name)"
    &"dnscmd.exe $dnsserver /RecordAdd $($domain.Name) sip CNAME sipdir.online.lync.com"
    &"dnscmd.exe $dnsserver /RecordAdd $($domain.Name) lyncdiscover CNAME webdir.online.lync.com"
    &"dnscmd.exe $dnsserver /RecordAdd $($domain.Name) _sipfederationtls._tcp SRV 100 1 5061 sipfed.online.lync.com."
    &"dnscmd.exe $dnsserver /RecordAdd $($domain.Name) _sip._tls SRV 100 1 443 sipdir.online.lync.com."
} 

Now I reduced this time-consuming (and boring) task to just a few minutes!

/ Andreas

How to handle SMTP Relay after migrating to Exchange Online

When decomissioning your on-premises Exchange server after moving to Office 365 you need a new solution for SMTP relay to use with for example multi-functional printers. In some cases your internet provider can offer this service, but if you want control over your mail flow I recommend using Office 365 also for outgoing e-mail.

Normally you need a licensed user to be able to send e-mails using SMTP with Office 365. Your applications also need support for TLS encryption. If your application doesn’t support TLS, or if you need to send e-mails from another address than the licensed user’s address you need another solution. Luckily Office 365 can help you. The solution is to set up an inbound connector in Exchange Online Protection.

Setting up an Inbound Connector

An Inbound Connector is easily set up with just a few lines of PowerShell code. First we have to connect to Exchange Online.

$UserCredential = Get-Credential
$Session = New-PSSession -ConfigurationName Microsoft.Exchange `
    -ConnectionUri 'https://outlook.office365.com/powershell-liveid/' `
    -Credential $UserCredential -Authentication Basic -AllowRedirection
Import-PSSession $Session -CommandName 'New-InboundConnector'

Now we can create the Inbound Connector. All we have to specify is a Name for our connector, SenderIPAddresses which is the IP addresses to allow relaying from (your external IP address), and SenderDomains which is the domains to accept messages from.

You can also get your external IP address with PowerShell, here I will use a free IP detection tool from Dyn.

$ip = (Invoke-WebRequest -Uri http://checkip.dyndns.com).content `
    -replace '[^\d\.]'
New-InboundConnector `
    -Name 'SMTP Relay' `
    -SenderIPAddresses $ip `
    -SenderDomains '365lab.net'

The settings for Mail Flow are also found in Exchange admin center, navigate to mail flow, and then go to the connectors section. Of course you can also use the GUI to create your connector.

smtprelay

When configuring your application to connect to this SMTP server, use your MX server name as SMTP server and connect using port 25. The server name can be found in Office 365 admin center, and looks something like domain-com.mail.protection.outlook.com.

Increasing security

With this method all computers on your network sharing the same external IP address can use your Inbound Connector. To strengthen the security I recommend only opening port 25 in the firewall from your devices that actually sends email. An even better alternative would be to set up an internal SMTP server that only accept connections from approved devices, and then uses your Inbound Connector as smart host. This can be achieved with the SMTP Service provided with Internet Information Services (IIS) in Windows Server.

Configuring IIS SMTP Service

First we have to install the SMTP Service in Windows. This can be done with PowerShell.

Note that the Add-WindowsFeature cmdlet in Windows Server 2012 is called Install-WindowsFeature, but Add-WindowsFeature still exists as alias for backward compatibility.

Import-Module ServerManager
Add-WindowsFeature SMTP-Server

This will also install the required dependencies for example the IIS 6 Management Console. When the installation is done we are ready to start configuring the service. Let’s set the service to start automatically when Windows starts:

Set-Service SMTPSVC -StartupType Automatic

The next step is to set Office 365 as Smart Host. Replace the Smart Host name with your own MX server.

$smtpsvc = [ADSI]'IIS://localhost/smtpsvc/1'
$smtpsvc.SmartHost = '365lab-net.mail.protection.outlook.com'
$smtpsvc.SetInfo()

We also have to set a Fully Qualified Domain Name to identify our SMTP Server.

$hostname = (Resolve-DnsName $ip).NameHost
$smtpsvc.FullyQualifiedDomainName = $hostname
$smtpsvc.SetInfo()

Finally we have to add our internal IP addresses that are allowed to use the SMTP Service. I prefer using the GUI for this. You will find these settings in the Internet Information Services (IIS) 6.0 Manager. Expand the local computer node, and right click on “SMTP Virtual Server #1” and choose Properties. On the Access tab click on Relay restrictions, and then add your local IP addresses that are allowed to use the SMTP Server. While in the GUI settings I also suggest having a look at the message size limits and logging settings.

Now we are all set. Don’t forget to configure your perimeter firewall to block outgoing SMTP traffic from all your computers except the SMTP Server.

/ Andreas

Office 365: Assign licenses based on groups using PowerShell – Advanced version

Important note: The end of an era with licensing scripts is near… and the beginning of a new one with Azure AD Group Based Licensing is here. Group Based Licensing is now in preview and currently requires a paid Azure AD Subscription. Try it out and give Microsoft your feedback on how they can make it even better! 

The never ending story about Office 365 licensing continues… This time it’s an extension of my script to assign licenses based on groups, with additional functionality to remove and change licenses for users.
I’ve come across scenarios where this have been a requirement a couple of times, and wanted to see how much work that was required to get the job done. 🙂

If you just want to assign licenses for users based on groups, plain and simple, this is not the script for you…

Running the script
The script is tested in a tenant with two different license types (E1 and E3). Therefore, the functionality has been verified against that, so if you have three or four different licenses to assign in your tenant, you have to do your own testing! 🙂

The following functions are included in the script:

  • Assignment of licenses for new users based on groups/licenseSKU’s in the $licenses hashtable
  • Switch of licensetype if a user is moved from one group to another
  • Removal of license if the user no longer is a member in any of the license assignment groups

LicenseFix

IMPORTANT: Since the script actually will remove licenses for users that are not in any of the groups, you have to make sure that you populate the license assignment groups prior to first time running the script.

Apart from the above, the script requirements and setup details are the same as in this post.

LicenseO365Users.ps1

<#   .SYNOPSIS      Script that assigns Office 365 licenses based on Group membership in WAAD. .DESCRIPTION     The script assigns of licenses for new users based on groups/licenseSKUs in the $licenses hashtable.     It switch licensetype if a user is moved from one group to Another.     It removes the license if the user no longer is a member in any of the license assignment Groups.     Updated 2015-03-25 to support multiple skus for each user.     The script REQUIRES PowerShell 3.0 or later! .NOTES      Author: Johan Dahlbom      Blog: 365lab.net      Email: johan[at]dahlbom.eu      The script are provided “AS IS” with no guarantees, no warranties, and they confer no rights.      #>
#Import Required PowerShell Modules
Import-Module MSOnline

#Office 365 Admin Credentials
$CloudUsername = 'admin@365lab.net'
$CloudPassword = ConvertTo-SecureString 'Password' -AsPlainText -Force
$CloudCred = New-Object System.Management.Automation.PSCredential $CloudUsername, $CloudPassword

#Connect to Office 365
Connect-MsolService -Credential $CloudCred

$Licenses = @{
                 'E1' = @{
                          LicenseSKU = 'mstlabs:STANDARDPACK'
                          Group = 'E1_Users'
                        }                        

                 'E3' = @{
                          LicenseSKU = 'mstlabs:ENTERPRISEPACK'
                          Group = 'E3_Users'
                        }
            }

$UsageLocation = 'SE'

#Get all currently licensed users and put them in a custom object
$LicensedUserDetails = Get-MsolUser -All | Where-Object {$_.IsLicensed -eq 'True'} | ForEach-Object {
 [pscustomobject]@{
            UserPrincipalName = $_.UserPrincipalName
            License = $_.Licenses.AccountSkuId
            }
 }

#Create array for users to change or delete
$UsersToChangeOrDelete = @()

foreach ($license in $Licenses.Keys) {

  #Get current group name and ObjectID from Hashtable
  $GroupName = $Licenses[$license].Group
  $GroupID = (Get-MsolGroup -All | Where-Object {$_.DisplayName -eq $GroupName}).ObjectId
  $AccountSKU = Get-MsolAccountSku | Where-Object {$_.AccountSKUID -eq $Licenses[$license].LicenseSKU}

  Write-Output "Checking for unlicensed $license users in group $GroupName with ObjectGuid $GroupID..."
  #Get all members of the group in current scope
  $GroupMembers = (Get-MsolGroupMember -GroupObjectId $GroupID -All).EmailAddress
  #Get all already licensed users in current scope
  $ActiveUsers = ($LicensedUserDetails | Where-Object {$_.License -eq $licenses[$license].LicenseSKU}).UserPrincipalName
  $UsersToHandle = ''

    if ($GroupMembers) {
        if ($ActiveUsers) {
            #Compare $Groupmembers and $Activeusers
            #Users which are in the group but not licensed, will be added
            #Users licensed, but not, will be evaluated for deletion or change of license
            $UsersToHandle = Compare-Object -ReferenceObject $GroupMembers -DifferenceObject $ActiveUsers -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
            $UsersToAdd = ($UsersToHandle | Where-Object {$_.SideIndicator -eq '<='}).InputObject             $UsersToChangeOrDelete += ($UsersToHandle | Where-Object {$_.SideIndicator -eq '=>'}).InputObject
        } else {
            #No licenses currently assigned for the license in scope, assign licenses to all group members.
            $UsersToAdd = $GroupMembers
        }

    } else {
      Write-Warning  "Group $GroupName is empty - will process removal or move of all users with license $($AccountSKU.AccountSkuId)"
      #If no users are a member in the group, add them for deletion or change of license.
      $UsersToChangeOrDelete += $ActiveUsers
    }

  #Check the amount of licenses left...
  if ($AccountSKU.ActiveUnits - $AccountSKU.consumedunits -lt $UsersToAdd.Count) {
        Write-Warning 'Not enough licenses for all users, please remove user licenses or buy more licenses'
  }

     foreach ($User in $UsersToAdd){

        #Process all users for license assignment, if not already licensed with the SKU in order.
          if ((Get-MsolUser -UserPrincipalName $User).Licenses.AccountSkuId -notcontains $AccountSku.AccountSkuId) {
            try {
                  #Assign UsageLocation and License.
                  Set-MsolUser -UserPrincipalName $User -UsageLocation $UsageLocation -ErrorAction Stop -WarningAction Stop
                  Set-MsolUserLicense -UserPrincipalName $User -AddLicenses $AccountSKU.AccountSkuId -ErrorAction Stop -WarningAction Stop
                  Write-Output "SUCCESS: Licensed $User with $license"
            } catch {
                  Write-Warning "Error when licensing $User"

            }

          }
     }
}

#Process users for change or deletion
if ($UsersToChangeOrDelete -ne $null) {
        foreach ($User in $UsersToChangeOrDelete) {
          if ($user -ne $null) {

            #Fetch users old license for later usage
            $OldLicense = ($LicensedUserDetails | Where-Object {$_.UserPrincipalName -eq $User}).License

             #Loop through to check if the user group assignment has been changed, and put the old and the new license in a custom object.
             #Only one license group per user is currently supported.
             $ChangeLicense = $Licenses.Keys | ForEach-Object {
                  $GroupName = $Licenses[$_].Group
                  if (Get-MsolGroupMember -All -GroupObjectId (Get-MsolGroup -All | Where-Object {$_.DisplayName -eq $GroupName}).ObjectId | Where-Object {$_.EmailAddress -eq $User}) {
                     [pscustomobject]@{
                        OldLicense = $OldLicense
                        NewLicense = $Licenses[$_].LicenseSKU
                     }
                  } 

              }

              if ($ChangeLicense) {
                    #The user were assigned to another group, switch license to the new one.
                    try {
                          Set-MsolUserLicense -UserPrincipalName $User -RemoveLicenses $ChangeLicense.OldLicense -AddLicenses $ChangeLicense.NewLicense -ErrorAction Stop -WarningAction Stop
                          Write-Output "SUCCESS: Changed license for user $User from $($ChangeLicense.OldLicense) to $($ChangeLicense.NewLicense)"
                    } catch {
                          Write-Warning "Error when changing license on $User`r`n$_"
                    }

              } else {  

                    #The user is no longer a member of any license group, remove license
                    Write-Warning "$User is not a member of any group, license will be removed... "
                    try {
                          Set-MsolUserLicense -UserPrincipalName $User -RemoveLicenses $OldLicense -ErrorAction Stop -WarningAction Stop
                          Write-Output "SUCCESS: Removed $OldLicense for $User"
                    } catch {
                          Write-Warning "Error when removing license on user`r`n$_"
                    }
              }
         }
    }
}

Hope this helps you if having this scenario, please let me know if you have features requests or other things that can improve the script!

/Johan

Conditional mail routing in Exchange Online Protection

In this post we will go through how to achieve conditional mail routing in Exchange Online Protection, without the need of having forwarding addresses per user. This means we can deliver incoming emails sent to the same domain to different back end servers, based on other criterias than forwarding addresses, like certain attributes or group membership.

If interested in how to do this using the mail users and forwarding addresses method, Andreas wrote a post a while ago about using Exchange Online as a mail gateway for decentalized email domains.

Scenario:

  • Exchange Online Protection is in place for all incoming emails.
  • You have a single Exchange organization, but different servers across the globe. You want to have external incoming emails directly to the correct mailbox server without transferring them trough your WAN connection.
  • DirSync from your tenant to EOP is already activated and in place, which means all users and Groups are already in WAAD.
  • Our incoming email routing will look like in the sketch below, based on AD Group membership.

Setup

As we in this case already have decided to base our email routing on AD groups which is already in sync with WAAD, the things we need to configure in EOP for each target server are following:

1. Create an outbound connector with support for Criteria Based Routing (CBR). Of course more than one smart host can be added for redundancy purposes. cbr

New-OutboundConnector -Name "CBR - eumx.365lab.net" `
                      -ConnectorType OnPremises `
                      -SmartHosts "eumx.365lab.net" `
                      -UseMXRecord $false `
                      -IsTransportRuleScoped $true 

2. Create a mail flow/transport rule for each destination connector/ad group.cbr-transport

New-TransportRule -Name "CBR - eumx.365lab.net" ` 
                  -SentToMemberOf "Europe@365lab.net" `
                  -RouteMessageOutboundConnector "CBR - eumx.365lab.net"

To avoid mail flow issues for users that are not in any of the groups, make sure you keep your existing default outbound connector.

/Johan

PowerShell GUI application to set thumbnailPhoto

A while ago I started experimenting with writing GUI applications in PowerShell. This script that updates the thumbnailPhoto attribute of a user was my first attempt, and since it works quite well I thought that I might share it with you.

The thumbnailPhoto attribute is used more and more, and it is now supported by Outlook, SharePoint and Lync. It is also supported by Office 365, and when using DirSync the data is synced to Windows Azure Active Directory.

This script uses Windows Presentation Foundation (WPF) which is part of .Net 3.0. The window form is easily built with Microsoft Visual Studio Express 2013 for Windows Desktop and can be saved as a XAML document, which we can import in our PowerShell script.

Data are saved and retrieved from Active Directory using ADSI, which means no additional PowerShell modules must be installed. The only requirements are PowerShell 2.0 or newer with .Net Framework 3.0. If running PowerShell version 2.0 you also need the -STA parameter to powershell.exe.

The application itself is very simple, just type the username in the search box, and browse for a photo. When clicking Apply the image data is saved to Active Directory. By default all users has permission to change their own thumbnailPhoto. To change other users, permissions needs to be delegated.

There is a limit in picture size, maximum size is 100 kB, but since Office 365 cannot handle that large image I have set the limit in the script to 10 kB. Also recommended resolution is 96×96 pixels.

thumbnailphotoguiapp

Here’s the full script, please note that you need a valid path to the .xaml document.

Set-ThumbnailPhotoGUI-mainform.xaml

<Window x:Class="MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Thumbnail photo changer app" Height="350" Width="525" ResizeMode="NoResize">
    <Grid x:Name="list1" Margin="0,0,0,2">
        <Label Content="Username:" HorizontalAlignment="Left" Width="105" Margin="20,23,0,250" />
        <TextBox x:Name="txtSearch" Margin="138,27,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" Width="122"/>
        <Button x:Name="btnSearch" Content="Search" HorizontalAlignment="Left" VerticalAlignment="Top" Width="50" Margin="271,27,0,0"/>
        <Label Content="Surname:" HorizontalAlignment="Left" Height="30" Margin="20,92,0,0" VerticalAlignment="Top" Width="92"/>
        <Label x:Name="lblSn" Content="" Height="30" VerticalAlignment="Top" HorizontalAlignment="Left" Width="315" Margin="134,92,0,0"/>
        <Label Content="UserPrincipalName:" HorizontalAlignment="Left" Height="30" Margin="20,123,0,0" VerticalAlignment="Top" Width="115"/>
        <Label x:Name="lblUPN" Content="" Height="30" VerticalAlignment="Top" HorizontalAlignment="Left" Width="346" Margin="134,123,0,0"/>
        <Label Content="Givenname:" HorizontalAlignment="Left" Height="30" Margin="20,61,0,0" VerticalAlignment="Top" Width="92"/>
        <Label x:Name="lblGivenName" Content="" Height="30" VerticalAlignment="Top" HorizontalAlignment="Left" Width="315" Margin="134,61,0,0"/>
        <Image x:Name="imgThumb" HorizontalAlignment="Left" Height="96" Margin="29,203,0,0" VerticalAlignment="Top" Width="96"/>
        <Button Name="btnImg" Content="Change photo..." HorizontalAlignment="Left" Height="28" Margin="149,267,0,0" VerticalAlignment="Top" Width="111" IsEnabled="False"/>
        <Label Content="DistinguishedName:" HorizontalAlignment="Left" Height="30" Margin="20,156,0,0" VerticalAlignment="Top" Width="126"/>
        <Label x:Name="lblDN" Content="" Height="30" VerticalAlignment="Top" HorizontalAlignment="Left" Width="373" Margin="134,156,0,0"/>
        <Button x:Name="btnApply" Content="Apply" HorizontalAlignment="Left" Height="28" Margin="278,267,0,0" VerticalAlignment="Top" Width="111" IsEnabled="False"/>
    </Grid>
</Window>

Set-ThumbnailPhotoGUI.ps1

if ([threading.thread]::CurrentThread.ApartmentState.ToString() -eq 'MTA') {
    Write-Error "This script requires PowerShell to run in Single-Threaded Apartment. Please run powershell.exe with parameter -STA"
    Exit
}

Add-Type -AssemblyName PresentationFramework 
[xml]$XAML = Get-Content "Set-ThumbnailPhotoGUI-mainform.xaml"
$XAML.Window.RemoveAttribute("x:Class")
 
$global:userinfo = @{
    dn = $null
    photo = [byte[]]$null
}
 
function Invoke-FileBrowser {
    param([string]$Title,[string]$Directory,[string]$Filter="jpeg (*.jpg)|*.jpg")
    [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null
    $FileBrowser = New-Object System.Windows.Forms.OpenFileDialog
    $FileBrowser.InitialDirectory = $Directory
    $FileBrowser.Filter = $Filter
    $FileBrowser.Title = $Title
    $Show = $FileBrowser.ShowDialog()
    If ($Show -eq "OK") { Return $FileBrowser.FileName }
}
 
function Get-LDAPUsers($sam) {
    $activeDomain = New-Object DirectoryServices.DirectoryEntry
    $domain = $activeDomain.distinguishedName
    $searcher = [System.DirectoryServices.DirectorySearcher]"[ADSI]LDAP://$domain"
    $searcher.filter = "(&(samaccountname=$sam)(objectClass=user)(objectClass=person))"
    $result = $searcher.findall()
     
    if ($result.count -gt 1) {
        Write-Warning "More than one user found. Please refine your search."
    } elseif ($result.count -eq 1) {
        $result = $searcher.findone().getDirectoryEntry()
        [pscustomobject]@{
            UserPrincipalName = $result.userprincipalname[0]
            DistinguishedName = $result.distinguishedname[0]
            samAccountName = $result.samaccountname[0]
            Givenname = $result.givenname[0]
            Surname = $result.sn[0]
            ThumbnailPhoto = $result.thumbnailphoto
        }
    }
}
 
function Update-WPFForm($sam) {
   $selecteduser = Get-LDAPUsers $sam
   if ($selecteduser) {
        $global:userinfo.dn = $selecteduser.DistinguishedName
        $global:userinfo.photo = [byte[]]$($selecteduser.thumbnailphoto)
 
        $lblGivenName.Content = $selecteduser.GivenName
        $lblSN.Content = $selecteduser.SurName
        $lblUPN.Content = $selecteduser.UserPrincipalName
        $lblDN.Content = $global:userinfo.dn
        $txtSearch.Text = $selecteduser.SamAccountName
        $imgThumb.Source = $global:userinfo.photo
        $btnImg.IsEnabled = $true
    }
}
 
#region GUI
    $reader=(New-Object System.Xml.XmlNodeReader $xaml)
    $window=[Windows.Markup.XamlReader]::Load($reader)
 
    $btnSearch = $window.FindName("btnSearch")
    $btnImg = $window.FindName("btnImg")
    $btnApply = $window.FindName("btnApply")
    $txtSearch = $window.FindName("txtSearch")
    $lblGivenName = $window.FindName("lblGivenName")
    $lblSn = $window.FindName("lblSn")
    $lblUPN = $window.FindName("lblUPN")
    $lblDN = $window.FindName("lblDN")
    $imgThumb = $window.FindName("imgThumb")
#endregion GUI
 
#region Events
 
    #region btnSearchClick
        $btnSearch_click = $btnSearch.add_Click
        $btnSearch_click.Invoke({
            Update-WPFForm($txtSearch.Text)
        })
    #endregion btnSearchClick
 
    #region btnImgClick
        $btnImg_click = $btnImg.add_Click
        $btnImg_click.Invoke({
            $file = Invoke-FileBrowser -Title "Choose a thumbnail photo" -Filter "Jpeg pictures (*.jpg)|*.jpg"
            if ($file) {
                if ((Get-Item $file).length -gt 10kB) {
                    Write-Warning "Picture too large. Maximum size is 10 kB."
                } else {
                    $global:userinfo.photo = [byte[]](Get-Content $file -encoding byte)
                    $imgThumb.Source = $global:userinfo.photo
                    $btnApply.IsEnabled = $true
                }
            }
        })
    #endregion btnImgClick
     
    #region btnApplyClick
        $btnApply_click = $btnApply.add_Click
        $btnApply_click.Invoke({
            $user = New-Object DirectoryServices.DirectoryEntry "LDAP://$($global:userinfo.dn)"
            $user.put("thumbnailPhoto", $global:userinfo.photo)
            $user.setinfo()
            $btnApply.IsEnabled = $false
            Write-Verbose "User $($global:userinfo.dn) updated."
        })
    #endregion btnApplyClick
     
    #region textChange
        $txtSearch_Change = $txtSearch.add_KeyDown
        $txtSearch_Change.Invoke({
            if ($_.Key -ieq 'Return') {
                Update-WPFForm($txtSearch.Text)
            }
        })
    #endregion textChange
 
#endregion Events
 
$window.ShowDialog() | Out-Null

This is just an example how a useful script with GUI can be written with quite simple means. Now let’s get the camera and take some photos!

/ Andreas