Update Active Directory Thumbnail Photo for All Users Without a Pic to Show Their Initials

We recently upgraded to Lync 2015 (aka Skype for Business). One feature which frustrated me was the group conversations; you can either see people’s names; in which case they’re listed in such a way as to use up a large amount of screen space, or you can see people’s photos/thumbnails, which is great except when most people don’t have their photo associated with their account.

To resolve this quickly I came up with the below script which:

  • Gets all AD users under a given OU / SearchBase
  • Filters for those without a thumbnail photo associated
  • Determines the users’ initials
  • Creates a JPG of their initials
  • Assigns this JPG as their thumbnail image

Thus giving an easier way to tell who you’re talking to until they get around to uploading a photo.

NB: The below script is currently untested as I don’t have sufficient rights to update AD / will have to wait for our infrastructure team to assist there… but those parts that I could test have been tested.

Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName System.IO
Add-Type -AssemblyName Microsoft.ActiveDirectory.Management
Import-Module ActiveDirectory

function Convert-InitialsToJpegImageByteArray {
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [Parameter(Mandatory = $false)]
        [int]$WidthInPixels = 96  #Ref decided not to go 648x648 just for text
        [Parameter(Mandatory = $false)]
        [int]$HeightInPixels = 96
        [Parameter(Mandatory = $false)]
        [System.Drawing.Font]$Font = (new-object System.Drawing.Font ('Proxima Nova Alt',32, [System.Drawing.FontStyle]::Bold))
        [Parameter(Mandatory = $false)]
        [System.Drawing.Color]$BackgroundColor = "#FF000434" #ARGB Colors
        [Parameter(Mandatory = $false)]
        [System.Drawing.Color]$ForegroundColor = "#FF0099D8"
    process {

        [System.Drawing.Bitmap]$bmp = new-object System.Drawing.Bitmap ($widthInPixels, $heightInPixels)
        [System.Drawing.Brush]$BrushBg = (new-object System.Drawing.SolidBrush ($BackgroundColor))
        [System.Drawing.Brush]$brushFg = (new-object System.Drawing.SolidBrush ($ForegroundColor))

        [System.Drawing.Graphics]$graphics = [System.Drawing.Graphics]::FromImage($bmp) 

        [System.IO.MemoryStream]$stream = new-object System.IO.MemoryStream


function Get-AdUserInitials {
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
    process {
        #$User = Get-ADUser $Identity #we could do parameter sets to allow passing only an identity
        $User | select-object @{Name='User';Expression={$User}}, @{Name='Initials';Expression={("{0}{1}" -f ("{0}?" -f $_.GivenName)[0],("{0}?" -f $_.Surname)[0]).ToUpperInvariant()}}

function Get-AdUsersWithNoThumbnail {
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [Parameter(Mandatory = $false)]
        [string]$SearchScope = 'Subtree'
        [Parameter(Mandatory = $false)]
        [string]$Filter = {Enabled -eq $true}
    process {
        get-aduser -filter {Enabled -eq $true} -SearchBase $SearchBase -SearchScope $SearchScope -Properties thumbnailPhoto | ?{!$_.thumbnailPhoto} 

@('OU=UK,DC=myCompany,DC=com') `
| Get-AdUsersWithNoThumbnail `
| Get-AdUserInitials `
| %{
    $User = $_.User
    [byte[]]$Image = $_.Initials | Convert-InitialsToJpegImageByteArray 
    Set-Aduser $user -replace @{thumbnailPhoto=$Image}


PowerShell Script :: Get AD Users by Email (Advanced)

Filed under: Microsoft, Technology — Tags: , , , , , , , — Developer42 @ 22:19

Here’s a script I knocked up today.

Auto.ps1 allows me to host the script on a server (or wherever), whilst others can use it by dropping input files into a queue folder, without needing to touch powershell (which may be scary to non-programmers, or may require additional setup or permissions).
ADGetUsersByEmailAdvanced.ps1 gets AD info based on email addresses; without requiring exchange modules, and includes workarounds to cope with missing information.


Monitors a folder for new text files. Once found, passes that file to a script to be processed. On completion moves the source file to the same directory & renames to begin with the same timestamp as the generated output file.

$infile = "\\myServer\myShare\Scripts\powershell\ADGetUsersByEmail\in\*.txt"

while (1 -eq 1) {
	#wait for a new file
	while(!(Test-Path $infile)) {Start-Sleep -s 30;}
	write-host "new input file found"
	Get-ChildItem $infile | %{
		$fileTimestamp = "{0:yyyy-MM-dd_HHmmss}" -f (get-date).ToUniversalTime()
		$inputFile = $_.fullname
		$exportFile = "{0}\out\{1}_output.csv" -f $PSScriptRoot,$fileTimestamp
		$inputFileMoved = "{0}\out\{1}_{2}" -f $PSScriptRoot,$fileTimestamp,$
		write-host ("source: {0}" -f $inputFile)
		write-host ("output: {0}" -f $exportFile) 
		write-host ("archive: {0}" -f $inputFileMoved) 
		.\ADGetUsersByEmailAdvanced.ps1 -sourceFile $_.fullname -exportFile $exportFile 
		write-host "processed"
		Move-Item $inputFile $inputFileMoved
		write-host "archived"


Given a text file containing a list of email addresses, attempts to resolve those to corresponding AD users, taking advantage of email information in AD where available, then gracefully degrading to more hacky methods. Works its way through a list of domains in case the users are in the same company but on a different domain.

#$sourceFile = '.\sourceEmails.txt'
#$exportFile = ".\output_{0:yyyy-MM-dd_HHmmss}.csv" -f (get-date).ToUniversalTime()
$domains = 'eu','usa','myDomain','anotherDomain' # domain points to the GC; could equally list GC server names here, though this version's more user friendly

#create dummy; allows us to put in values for any unfound items (currently just using null, but we can easily amend if desired)
$adDummy = New-Object –TypeName PSObject –Prop @{
	emailSearched	= $null;
	notFound		= $true;
	sAmAccountName 	= $null;
	fullname		= $null;
	firstname		= $null;
	lastname		= $null;
	cn				= $null;
	countryCode		= $null;
	country			= $null;
	#title			= $null;
	title			= $null;
	department		= $null;
	company			= $null;
	email			= $null;	
	adEmail			= $null;
	proxyEmail		= $null;

function RemoveEmailDomain($email) {
  return $email -replace "(\S*)@\S*", '$1'
function IsFirstDotLast($name) {
	return $name -like '*.*'
function GetFirstName($name) {
	return $name -replace "(\S*?)\.\S*", '$1'
function GetLastName($name) {
	return $name -replace "\S*?\.(\S*)", '$1'
function GetFirstNamePartial($name) {
	return $name.substring(0,[system.math]::min(3,$name.length))
function GetLastNamePartial($name) {
	return $name.substring([system.math]::max($name.length-3,0),[system.math]::min(3,$name.length))
function GetADUserByIdentity($id, $domain) {
	#trycatch since erroraction not recognised on this type of command & I don't want error messages polluting my output
	write-host "searching for id '$id' on domain '$domain'"
	try { 
		Get-ADUser -Identity $id -Server $domain -Properties sAmAccountName, displayName, givenName, surname, distinguishedName, countryCode, c, title, department, company, emailAddress, proxyAddresses
	} catch {}
function GetADUserFiltered($filter, $domain) {
	Get-ADUser -Filter $filter -Server $domain -Properties sAmAccountName, displayName, givenName, surname, distinguishedName, countryCode, c, title, department, company, emailAddress, proxyAddresses
function GetADUserByEmailAddress($email, $domain) {
	write-host "searching for email '$email' on domain '$domain'"
	$filter = {emailAddress -eq $email} 
	GetADUserFiltered $filter $domain
function GetADUserByProxyAddress($email, $domain) {
	write-host "searching for proxy '$email' on domain '$domain'"
	$psBugFixSearchUser = "*:$_*"
	$filter = {proxyAddresses -like $psBugFixSearchUser}
	GetADUserFiltered $filter $domain
function GetADUserByFullName($name, $domain) {
	$fn = GetFirstName $name
	$ln = GetLastName $name
	write-host "searching for name '$fn', '$ln' on domain '$domain'"
	$filter = {(givenname -eq $fn) -and (surname -eq $ln)}
	GetADUserFiltered $filter $domain
function GetADUserByPartialName($name, $domain) {
	$fn = "{0}*" -f (GetFirstNamePartial $name)
	$ln = "*{0}" -f (GetLastNamePartial $name)
	write-host "searching for partial name '$fn', '$ln' on domain '$domain'"
	$filter = {(givenname -like $fn) -and (surname -like $ln)}
	GetADUserFiltered $filter $domain | where { ($_.givenname + $_.surname) -eq $name }
function FindBestMatch($email) {
	$result = $null
	#$domains | %{ $result=GetADUserByEmailAddress $email $_; if($result) {return $result;} } #doesn't play as expected
	foreach($domain in $domains) { $result=GetADUserByEmailAddress $email $domain; if($result) {return $result;} }
	foreach($domain in $domains) { $result=GetADUserByProxyAddress $email $domain; if($result) {return $result;} }
	$name = RemoveEmailDomain $email
	foreach($domain in $domains) { $result=GetADUserByIdentity $name $domain; if($result) {return $result;} }
	if(IsFirstDotLast($name)) {
		foreach($domain in $domains) { $result=GetADUserByFullName $name $domain; if($result) {return $result;} }
	} else {
		foreach($domain in $domains) { $result=GetADUserByPartialName $name $domain; if($result) {return $result;} }
	return $adDummy;

#define a function for later use
#get list of emails (ignore blanks)
$emails = (Get-Content $sourceFile) | where{ $_ -gt '' }

#get data from ad and stick it in an csv (or error to console if not found)
$emails | %{ 
	#get ad user by email address
	$adUser = FindBestMatch($_);
	if($adUser.notFound) {
		write-host ":(" -ForegroundColor Red
	} else {
		write-host ":)" -ForegroundColor Green
	#return object replresenting results.
	New-Object –TypeName PSObject –Prop @{
		emailSearched	= $_;
		found			= if($adUser.notFound){$false} else {$true};
		sAmAccountName 	= $adUser.sAmAccountName;
		fullname		= $adUser.displayName;
		firstname		= $adUser.givenname;
		lastname		= $adUser.surname;
		cn				= $adUser.distinguishedName;
		countryCode		= $adUser.countryCode;
		country			= $adUser.c;
		#title			= $adUser.personalTitle;
		title			= $adUser.title;
		department		= $adUser.department;
		company			= $;
		adEmail			= $adUser.emailAddress;
		proxyEmail		= [string]$adUser.proxyAddresses; #string joins the array down to a single string value
} | export-csv $exportFile -notype #stick output to file

Script could be improved by allowing auto to kick off jobs so multiple instances of the worker script can be run simultaneously. Also changing the main script to make use of workflows and take advantage of the parallel foreach method should significantly improve it’s performance. However I’m still pretty new to PowerShell, so those steps will have to come later.

Blog at

%d bloggers like this: