Outlook Signatures

To use this script, you must place the template .docx file in the same location as the script (can be run from a GPO). Microsoft Word must be installed on the target machine, which should be the case if Outlook is installed.

Template replacement format is [Title] which translates into the job title of the user in AD, eg. SharePoint Designer

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
<#
    .SYNOPSIS
    Script to set Outlook 2010/2013 e-mail signature using Active Directory information
    
    .DESCRIPTION
    This script will set the Outlook 2010/2013 e-mail signature on the local client using Active Directory information. 
    The template is created with a Word document, where images can be inserted and AD values can be provided.

    Author: George Paton
    Version 2.1
#>

###  Global Vars ###
## Company Variables
$COMPANY_LONG_NAME  = "Upstream Production Solutions"
$COMPANY_SHORT_NAME = "Upstream PS"
$COMPANY_INITIALS   = "UPS"
$COMPANY_VAR_NAME   = "upstreamps"
$COMPANY_EMAIL_EXT  = "@UpstreamPS.com"
$COMPANY_EMAIL_ENC  = "_upstreamps_com"

### Logging Variables
$LOG_FOLDER = "\\Log-Server\Log-Share\Logs\"
$SIGNATURE_LOG_FOLDER = "Set-Outlook-Signature\"

## Signature Variables
# Signature name as it shows in Outlook
$SIGNATURE_COMPOSE_NAME = $COMPANY_VAR_NAME
$SIGNATURE_REPLY_NAME   = $COMPANY_VAR_NAME + "-reply"

# Separate signatures for compose/reply emails
$SIGNATURE_SEPARATE = $true

# Signature template filenames
$SIGNATURE_COMPOSE_TEMPLATE = "template-compose.docx"
$SIGNATURE_REPLY_TEMPLATE   = "template-reply.docx"

$SIGNATURE_FORCE         = $false  #Set to $true if you don't want the users to be able to change signature in Outlook
$SIGNATURE_FORCE_THEME   = $true   #Set to $true to force Outlook default message theme within registry - ONLY IN Office 2013
$SIGNATURE_MARK_COMMENTS = $true   #Enable marking comments in emails with initials eg. [GP] - ONLY IN Office 2013

# Default email compose and reply styling
# Colour is in BGR format, not RGB
$SIGNATURE_COMPOSE_FONT  = "Calibri"
$SIGNATURE_COMPOSE_SIZE  = 11
$SIGNATURE_COMPOSE_COLOR = 0x5c3405

$SIGNATURE_REPLY_FONT    = $SIGNATURE_COMPOSE_FONT
$SIGNATURE_REPLY_SIZE    = $SIGNATURE_COMPOSE_SIZE
$SIGNATURE_REPLY_COLOR   = $SIGNATURE_COMPOSE_COLOR

# Maximum amount of days to elapse before autorenewing signature
$SIGNATURE_MAX_AGE       = 60

# Specify which lines to remove from completed template to handle blank fields in AD
# This is an ARRAY LITERAL, please read:
# * http://blogs.msdn.com/b/powershell/archive/2007/01/23/array-literals-in-powershell.aspx
# ORDER MATTERS, Items higher up will be evaluated first
# ^? matches any character, in this case, a newline character (so if an entire line is removed,
# the newline is also removed, which ensures no weird blank lines show up)
$SIGNATURE_TEMPLATE_BLANKS =
    "a: [StreetAddress], [l] [st] [postalCode]^?",
    "d: [TelephoneNumber] | r: [HomePhone] | m: [Mobile]^?",
    "d: [TelephoneNumber] | r: [HomePhone] | ",
    "| r: [HomePhone] | m: [Mobile]",
    "| m: [Mobile]",
    "d: [TelephoneNumber] | ",
    "r: [HomePhone] | ",
    "[l] ",
    "[st] ",
    "[postalCode]"
### /Global Vars ###

if (-not $SIGNATURE_SEPARATE) {
    $SIGNATURE_REPLY_NAME = $SIGNATURE_COMPOSE_NAME
}

# Stop execution on unhandled errors
$ErrorActionPreference = "Stop"

# Environment variables
$appData = (Get-Item env:appdata).value
$signaturePath = "\Microsoft\Signatures"
$localSignaturePath = $appData + $signaturePath

# Check signature path, and create if necessary
if (-not(Test-Path -Path $localSignaturePath)) {
    New-Item $localSignaturePath -Type Directory
}

# Logging
$logPath = $localSignaturePath + "\set_outlook_signature.log"
if (-not (Test-Path $logPath) -or (Get-Item $logPath).Length -gt 100kb) {
    New-Item $logPath -type file -force
}
Start-Transcript -path $logPath -Append

# Get Active Directory information for current user
$username = $env:username
$filter = "(&(objectCategory=User)(samAccountName=$username))"
$searcher = New-Object System.DirectoryServices.DirectorySearcher
$searcher.Filter = $filter
$ADUserPath = $searcher.FindOne()
$ADUser = $ADUserPath.GetDirectoryEntry()

### Timestamping - lets not run this more than we need to
# Get last run timestamp
$timestampPath = $localSignaturePath + "\set_outlook_signature_timestamp.txt"
if (-not (Test-Path $timestampPath)) {
    # If no timestamp is found, the script hasn't run
    # So lets set last run date to the very distant past
    $epoch = Get-Date -Year 1970
    New-Item $timestampPath -type file
    Add-Content $timestampPath $epoch.ToFileTimeUtc()
}
$timestamp = [DateTime]::FromFileTimeUtc((Get-Content $timestampPath))
$currentTime = (Get-Date).ToUniversalTime()

# Get time since the last run of this script
$timeSinceLastRun = $currentTime.Subtract($timestamp)

if ($timeSinceLastRun.TotalDays -lt $SIGNATURE_MAX_AGE) {
    # Get time since last user modification
    $userTimestamp = $ADUser.whenChanged[0]
    $timeSinceLastUserChange = $userTimestamp.Subtract($timestamp)

    # Get time since last template change
    $composeTimestamp = (Get-Item $SIGNATURE_COMPOSE_TEMPLATE).LastWriteTimeUtc
    $replyTimestamp = (Get-Item $SIGNATURE_REPLY_TEMPLATE).LastWriteTimeUtc
    $timeSinceLastComposeChange = $composeTimestamp.Subtract($timestamp)
    $timeSinceLastReplyChange = $replyTimestamp.Subtract($timestamp)

    $changeDetected = $false

    # Positive seconds means the timestamp is older
    if ($timeSinceLastUserChange.TotalSeconds -gt 0) {
        Write-Host "User has been modified, renewing signature"
        $changeDetected = $true
    }

    if ($timeSinceLastComposeChange.TotalSeconds -gt 0) {
        Write-Host "Compose template has been modified, renewing signature"
        $changeDetected = $true
    }

    if ($timeSinceLastReplyChange.TotalSeconds -gt 0) {
        Write-Host "Reply template has been modified, renewing signature"
        $changeDetected = $true
    }

    if (-not $changeDetected) {
        Write-Host "No changes detected, exiting..."
        Stop-Transcript
        Copy-Item $logPath -Destination ($LOG_FOLDER + $SIGNATURE_LOG_FOLDER + "signature-" + $username + ".log")
        exit
    }
} else {
    Write-Host "Signature is old, lets renew it"
}


# Load word.application enums
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Interop.Word") | Out-Null

# Text manipulation settings
$replaceAll = [Enum]::Parse([Microsoft.Office.Interop.Word.WdReplace], "wdReplaceAll")
$matchCase = $false
$matchWholeWord = $true
$matchWildcards = $false
$matchSoundsLike = $false
$matchAllWordForms = $false
$forward = $true
$wrap = [Enum]::Parse([Microsoft.Office.Interop.Word.WdFindWrap], "wdFindContinue")
$format = $true

# Create instance of Office Word
try {
    $MSWord = New-Object -ComObject word.application
    Write-Host "Word version is: " $MSWord.BuildFull
} catch {
    Write-Host $_.Exception.Message
    Write-Host "Could not load Word, checking for office install"
    
    $comObject = Get-ChildItem HKLM:\Software\Classes -ea 0 | Where-Object { $_.PSChildName -eq 'Word.Application' -and (Get-ItemProperty "$(_.PSPath)\CLSID" -ea 0) }
    if ($comObject -ne $null) {
        if (Test-Path "C:\program files\Microsoft Office 15\ClientX86") {
            Write-Host "Repairing Office 32 bit"
            # CMD Prompt style command for background office repair
            & "C:\program files\Microsoft Office 15\ClientX86\OfficeClickToRun.exe" scenario=Repair DisplayLevel=False
        } elseif (Test-Path "C:\program files\Microsoft Office 15\ClientX64") {
            Write-Host "Repairing Office 64 bit"
            # CMD Prompt style command for background office repair
            & "C:\program files\Microsoft Office 15\ClientX64\OfficeClickToRun.exe" scenario=Repair DisplayLevel=False
        } else {
            Write-Host "OfficeClickToRun not found, repair failed"
        }
    } else {
        Write-Host "Word COM object not found, please check office is installed"
    }

    # Finish transcription to log file
    Stop-Transcript

    # Send log file to Logs folder
    Copy-Item $logPath -Destination ($LOG_FOLDER + $SIGNATURE_LOG_FOLDER + "signature-" + $username + ".log")

    exit
}

# Check for pass by reference or not
# This is a disgusting hack, thanks Microsoft...
$isPassByRef = $true
try {
    $MSWord.Repeat([ref]0)
} catch {
    $isPassByRef = $false
}
Write-Host "Pass by ref: " $isPassByRef


function ReplaceText($find, $replace) {
    $currentDocument.Select()
    try {
        $isMatched = $MSWord.Selection.Find.Execute(
            $find,
            $matchCase,
            $matchWholeWord,
            $matchWildcards,
            $matchSoundsLike,
            $matchAllWordForms,
            $forward,
            $wrap,
            $format,
            $replace,
            $replaceAll
        )
        if ($isMatched) {
            Write-Host $find $replace
            return $true
        }
    } catch {
        Write-Host "Replace failed for:" $find
    }
    return $false
}

function FindText($find) {
    $currentDocument.Select()
    if ($MSWord.Selection.Find.Execute(
        $find,
        $matchCase,
        $matchWholeWord,
        $matchWildcards,
        $matchSoundsLike,
        $matchAllWordForms,
        $forward,
        $wrap,
        $format
    )) {
        return $true
    }
    return $false
}

function RemoveText($text) {
    ReplaceText $text ""
}

function SetSignature($aTemplateFile, $aTemplateName) {

    Write-Host "Copying Signature file: $aTemplateFile"
    try {
        Copy-Item $aTemplateFile $localSignaturePath -Recurse -Force
    } catch {
        Write-Host "Couldn't load template, error: " $_.Exception.Message
        return $false
    }
    
    # Insert variables from Active Directory into file
    $fullPath = $localSignaturePath + "\" + $aTemplateFile
    $currentDocument = $MSWord.Documents.Open($fullPath, $false, $false, $false) #filename, show convert file, open read-only, add to recent files

    Write-Host "Setting signature values for $aTemplateName"
    foreach ($property in $ADUser.psobject.properties) {
        $propertyName = "[" + $property.name + "]"
        if ($property.value -ne $null) {
            ReplaceText $propertyName $property.value.toString() > $null
        }
    }

    
    # Set mail hyperlink
    foreach ($hyperlink in $MSWord.ActiveDocument.Hyperlinks) {
        if ($hyperlink.Name -eq "mailto:[mail]") {
            $mailtoLink = "mailto:" + $ADUser.mail
            $hyperlink.Address = $mailtoLink
        }
    }

    # Remove empty lines if necessary
    Write-Host "Removing empty template lines"
    foreach ($line in $SIGNATURE_TEMPLATE_BLANKS) {
        RemoveText $line > $null
    }

    # If any blanks failed to be removed, don't save
    if (FindText "[") {
        Write-Host "Blank template lines found, saving failed, Please check Powershell-Vars.ps1 string removal array"

        $currentDocument.Close()
        return $false
    }
    
    # Save new message signature 
    Write-Host "Saving signature $aTemplateName"

    # Save HTML
    $saveFormat = [Enum]::Parse([Microsoft.Office.Interop.Word.WdSaveFormat], "wdFormatHTML")
    $path = $localSignaturePath + "\" + $aTemplateName + ".htm"
    try {
        #filename, file format, lock comments, password, add to recent files
        if ($isPassByRef) {
            $currentDocument.SaveAs([ref]$path, [ref]$saveFormat, [ref]$false, [ref]"", [ref]$false)
        } else {
            $currentDocument.SaveAs($path, $saveFormat, $false, "", $false)
        }
    } catch {
        Write-Host "Failed to save as HTML, error: " $_.Exception.Message
        $currentDocument.Close()
        return $false
    }
        
    # Save RTF 
    $saveFormat = [Enum]::Parse([Microsoft.Office.Interop.Word.WdSaveFormat], "wdFormatRTF")
    $path = $localSignaturePath + "\" + $aTemplateName + ".rtf"
    try {
        if ($isPassByRef) {
            $currentDocument.SaveAs([ref]$path, [ref]$saveFormat, [ref]$false, [ref]"", [ref]$false)
        } else {
            $currentDocument.SaveAs($path, $saveFormat, $false, "", $false)
        }
    } catch {
        Write-Host "Failed to save as RTF, error: " $_.Exception.Message
        $currentDocument.Close()
        return $false
    }
    	
    # Save TXT    
    $saveFormat = [Enum]::Parse([Microsoft.Office.Interop.Word.WdSaveFormat], "wdFormatText")
    $path = $localSignaturePath + "\" + $aTemplateName + ".txt"
    try {
        if ($isPassByRef) {
            $currentDocument.SaveAs([ref]$path, [ref]$saveFormat, [ref]$false, [ref]"", [ref]$false)
        } else {
            $currentDocument.SaveAs($path, $saveFormat, $false, "", $false)
        }
    } catch {
        Write-Host "Failed to save as TXT, error: " $_.Exception.Message
        $currentDocument.Close()
        return $false
    }

    $currentDocument.Close()
    return $true
}

function ExitGracefully() {
    $saveOptions = [Enum]::Parse([Microsoft.Office.Interop.Word.WdSaveOptions],  "wdDoNotSaveChanges")
    if ($isPassByRef) {
        $MSWord.Quit([ref]$saveOptions)
    } else {
        $MSWord.Quit($saveOptions)
    }

    # Finish transcription to log file
    Stop-Transcript

    # Send log file to Logs folder
    Copy-Item $logPath -Destination ($LOG_FOLDER + $SIGNATURE_LOG_FOLDER + "signature-" + $username + ".log")
}

Write-Host "Waiting 2 seconds for AD object"
Start-Sleep 2

# Run signature filler
if (-not (SetSignature $SIGNATURE_COMPOSE_TEMPLATE $SIGNATURE_COMPOSE_NAME)) {
    Write-Host "Compose Template creation failed, exiting"
    ExitGracefully
    exit
}
if ($SIGNATURE_SEPARATE) {
    if (-not (SetSignature $SIGNATURE_REPLY_TEMPLATE $SIGNATURE_REPLY_NAME)) {
        Write-Host "Reply Template creation failed, exiting"
        ExitGracefully
        exit
    }
}

# All Office versions
Write-Host "Adding signature to Office"
$emailOptions = $MSWord.EmailOptions
$emailSignature = $emailOptions.EmailSignature
$emailCompose = $emailOptions.ComposeStyle
$emailReply = $emailOptions.ReplyStyle

try {
    $emailSignature.NewMessageSignature = $SIGNATURE_COMPOSE_NAME
    $emailSignature.ReplyMessageSignature = $SIGNATURE_REPLY_NAME
} catch {
    Write-Host "Setting default signature failed, exiting"
    Write-Host $_.Exception.Message
    ExitGracefully
}


if ($SIGNATURE_FORCE) {
    # Office 2010
    If (Test-Path HKCU:Software\Microsoft\Office\14.0) {
        Write-Host "Forcing set signature for Outlook 2010"
        New-ItemProperty HKCU:'\Software\Microsoft\Office\14.0\Common\MailSettings' -Name "ReplySignature" -Value $SIGNATURE_REPLY_NAME -PropertyType "String" -Force
        New-ItemProperty HKCU:'\Software\Microsoft\Office\14.0\Common\MailSettings' -Name "NewSignature" -Value $SIGNATURE_COMPOSE_NAME -PropertyType "String" -Force
    } else {
        Write-Host "Office 2010 not installed, 2010 signatures skipped"
    }

    # Office 2013
    if (Test-Path HKCU:Software\Microsoft\Office\15.0) {
        Write-Host "Forcing set signature for Outlook 2013"
        New-ItemProperty HKCU:'\Software\Microsoft\Office\15.0\Common\MailSettings' -Name "ReplySignature" -Value $SIGNATURE_REPLY_NAME -PropertyType "String" -Force
        New-ItemProperty HKCU:'\Software\Microsoft\Office\15.0\Common\MailSettings' -Name "NewSignature" -Value $SIGNATURE_COMPOSE_NAME -PropertyType "String" -Force
    } else {
        Write-Host "Office 2013 not installed, 2013 signatures skipped"
    }
} else {
    # Office 2010
    If (Test-RegistryValue HKCU:'\Software\Microsoft\Office\14.0\Common\MailSettings' "NewSignature") {
        Write-Host "Removing signature enforcement for Outlook 2010"
        Remove-ItemProperty HKCU:'\Software\Microsoft\Office\14.0\Common\MailSettings' -Name "ReplySignature" -Force
        Remove-ItemProperty HKCU:'\Software\Microsoft\Office\14.0\Common\MailSettings' -Name "NewSignature" -Force
    }
    
    # Office 2013
    if (Test-RegistryValue HKCU:'\Software\Microsoft\Office\15.0\Common\MailSettings' "NewSignature") {
        Write-Host "Removing signature enforcement for Outlook 2013"
        Remove-ItemProperty HKCU:'\Software\Microsoft\Office\15.0\Common\MailSettings' -Name "ReplySignature" -Force
        Remove-ItemProperty HKCU:'\Software\Microsoft\Office\15.0\Common\MailSettings' -Name "NewSignature" -Force
    }
}

if ($SIGNATURE_FORCE_THEME) {
    Write-Host "Themes are enforced, setting font for compose and reply"
    
    $emailCompose.Font.Name = $SIGNATURE_COMPOSE_FONT
    $emailCompose.Font.Size = $SIGNATURE_COMPOSE_SIZE
    $emailCompose.Font.Color = $SIGNATURE_COMPOSE_COLOR
    
    $emailReply.Font.Name = $SIGNATURE_REPLY_FONT
    $emailReply.Font.Size = $SIGNATURE_REPLY_SIZE
    $emailReply.Font.Color = $SIGNATURE_REPLY_COLOR
    
    # Ensure no custom themes are used
    $emailOptions.ThemeName = "none"
}

if ($SIGNATURE_MARK_COMMENTS) {
    Write-Host "Email comments are marked, setting mark as user initials"
    
    $emailOptions.MarkCommentsWith = $MSWord.UserInitials
    $emailOptions.MarkComments = $true
}

# Timestamp the last run time
$newTime = (Get-Date).ToFileTimeUtc()
New-Item $timestampPath -type file -force
Add-Content $timestampPath $newTime

ExitGracefully

Repair SharePoint apps after theme change

On SharePoint, if you change the theme of an App on the App Catalog, or change a theme of a site that contains an Apps, all the Apps in the App Catalog will fail catastrophically.

This script is a quick fixme that requires zero configuration, just upload it to the SharePoint server and run it to repair the App Catalog entirely.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/powershell
# Written by George Paton
## This little number automatically fixes that annoying app breaking bug to do with themes
## If an app is complaining about being unable to open a file, run me on the SP server


# Add the sharepoint snapin to the current session
Add-PSSnapin Microsoft.Sharepoint.Powershell

# Get the app prefix
$prefix = Get-SPAppSiteSubscriptionName
# Use the app prefix to get all app urls
$allappurls = Get-SPSite | Get-SPWeb -Limit All | where {$_.url -like "http://$prefix*"}

foreach ($app in $allappurls)
{
    # Loop through all apps within the default site with the app prefix of $prefix
	$name = $app.name
	$app.CustomMasterUrl = "/$name/_catalogs/masterpage/app.master"
	$app.MasterUrl = "/$name/_catalogs/masterpage/app.master"
	$app.Update()
    echo "Updating masterpage for $app"
}