<# Signature Buddy - Microsoft 365 deploy Sets each mailbox's Outlook signature from the signatures.json file exported by Signature Buddy's "Copy deploy file" button. One-time setup (Exchange admin): Install-Module ExchangeOnlineManagement -Scope CurrentUser Connect-ExchangeOnline -UserPrincipalName admin@yourcompany.com Microsoft's newer "roaming signatures" ignore signatures set by admins. To let this script's signatures take effect, turn them off once: Set-OrganizationConfig -PostponeRoamingSignaturesUntilLater $true Run: .\deploy_m365.ps1 -DeployFile .\signatures.json # dry run .\deploy_m365.ps1 -DeployFile .\signatures.json -Apply # write signatures Scheduled campaigns: the deploy file holds each person's signature for the next 90 days. Schedule this script daily in Windows Task Scheduler (or an Azure Automation runbook) and banners switch on and off by themselves. Signatures land in Outlook on the web and new Outlook. Classic desktop Outlook keeps its own local signatures; people there can paste theirs from the Team tab. Images: Outlook blocks in-browser (data:) images in signatures. Signature Buddy normally hosts them before you copy the deploy file. If any are left, pass your image service and upload key and this script hosts them first: .\deploy_m365.ps1 -DeployFile .\signatures.json -HostUrl https://sig.yourcompany.com -UploadKey KEY (or set SIGNATURE_BUDDY_HOST_URL and SIGNATURE_BUDDY_UPLOAD_KEY). #> param( [Parameter(Mandatory = $true)][string]$DeployFile, [switch]$Apply, [string]$Date = (Get-Date -Format "yyyy-MM-dd"), # override to test a schedule [string]$HostUrl = $env:SIGNATURE_BUDDY_HOST_URL, [string]$UploadKey = $env:SIGNATURE_BUDDY_UPLOAD_KEY ) $dataImg = 'src="(data:image/[^"]+)"' if (-not (Get-Command Set-MailboxMessageConfiguration -ErrorAction SilentlyContinue)) { Write-Error "Not connected to Exchange Online. Run Connect-ExchangeOnline first." exit 1 } $data = Get-Content -Raw -Encoding UTF8 $DeployFile | ConvertFrom-Json $people = @() if ($data.people) { foreach ($p in $data.people) { foreach ($w in $p.windows) { if ($w.from -le $Date -and ($null -eq $w.to -or $Date -le $w.to)) { $people += [pscustomobject]@{ email = $p.email; html = $w.html }; break } } } } else { $people = @($data.signatures) } if ($data.valid_until -and $Date -gt $data.valid_until) { Write-Host "Note: this deploy file was scheduled through $($data.valid_until). Copy a fresh one from Signature Buddy.`n" } if ($people.Count -eq 0) { Write-Error "No signatures in the deploy file. Add people in the Team tab and copy it again." exit 1 } # Host any in-browser images (the service stores by content hash, so repeats cost nothing) if ($people | Where-Object { $_.html -match $dataImg }) { if ($HostUrl -and $UploadKey) { $cache = @{}; $hostedCount = 0; $errors = @() $api = $HostUrl.TrimEnd('/') + '/api/upload' foreach ($p in $people) { $p.html = [regex]::Replace($p.html, $dataImg, { param($m) $d = $m.Groups[1].Value if (-not $cache.ContainsKey($d)) { try { $r = Invoke-RestMethod -Method Post -Uri $api -ContentType 'application/json' ` -Headers @{ Authorization = "Bearer $UploadKey" } -Body (@{ data = $d } | ConvertTo-Json -Compress) $cache[$d] = $r.url; $script:hostedCount++ } catch { $cache[$d] = $null; $script:errors += $_.Exception.Message } } if ($cache[$d]) { 'src="' + $cache[$d] + '"' } else { $m.Value } }) } $msg = "Hosted $hostedCount image(s)" if ($errors.Count) { $msg += ", $($errors.Count) failed: $(($errors | Select-Object -Unique) -join '; ')" } Write-Host "$msg.`n" } else { Write-Host "Note: some signatures have in-browser images, which Outlook blocks. Turn on Image hosting in Signature Buddy, or pass -HostUrl and -UploadKey.`n" } } $mode = if ($Apply) { "APPLY" } else { "DRY RUN" } Write-Host "$mode for $Date : $($people.Count) signature(s)`n" $ok = 0; $failed = 0 foreach ($p in $people) { try { $null = Get-Mailbox -Identity $p.email -ErrorAction Stop if ($Apply) { Set-MailboxMessageConfiguration -Identity $p.email ` -SignatureHtml $p.html ` -AutoAddSignature $true ` -AutoAddSignatureOnReply $true -ErrorAction Stop Write-Host " OK $($p.email)" } else { Write-Host " ~ $($p.email) (would update)" } $ok++ } catch { $failed++ Write-Host " X $($p.email) $($_.Exception.Message)" -ForegroundColor Yellow } } $verb = if ($Apply) { "updated" } else { "to update" } Write-Host "`n$ok $verb, $failed failed." if (-not $Apply -and $ok -gt 0) { Write-Host "Run again with -Apply to make these changes." } if ($failed -gt 0) { exit 1 }