|
| 1 | +#Requires -Version 5.1 |
| 2 | + |
| 3 | +<# |
| 4 | +.SYNOPSIS |
| 5 | + Create or update Exchange Online transport rule for external email disclaimers |
| 6 | +.DESCRIPTION |
| 7 | + Creates or updates a transport rule to prepend security banners to external emails. |
| 8 | + Based on methodology from ArchiTech Labs: https://www.architechlabs.io |
| 9 | +
|
| 10 | + Blog post: https://www.architechlabs.io/articles/external-email-banner/ |
| 11 | +.PARAMETER OrgPrefix |
| 12 | + Your organization prefix/name for the header (2-50 characters). Spaces will be removed. |
| 13 | + Examples: 'Contoso Corp' becomes 'X-ContosoCorp-Disclaimer-External' |
| 14 | + 'ACME Industries' becomes 'X-ACMEIndustries-Disclaimer-External' |
| 15 | + 'CONTOSO' becomes 'X-CONTOSO-Disclaimer-External' |
| 16 | +.PARAMETER Priority |
| 17 | + Transport rule priority (0 = highest). Default: 0 |
| 18 | +.PARAMETER RuleName |
| 19 | + Transport rule name. Default: "Security – Inbound External – Prepend Disclaimer" |
| 20 | +.PARAMETER Disabled |
| 21 | + Create the rule in disabled state (safer for testing) |
| 22 | +.EXAMPLE |
| 23 | + .\New-EXOExternalDisclaimerTransportRule.ps1 -OrgPrefix "Contoso Corp" |
| 24 | +.EXAMPLE |
| 25 | + .\New-EXOExternalDisclaimerTransportRule.ps1 -OrgPrefix "ACME" -Priority 2 -WhatIf |
| 26 | +.EXAMPLE |
| 27 | + .\New-EXOExternalDisclaimerTransportRule.ps1 -OrgPrefix "MyOrg" -Disabled |
| 28 | +.NOTES |
| 29 | + Requires Exchange Online PowerShell connection (Connect-ExchangeOnline) |
| 30 | +
|
| 31 | + Author: Travis McDade |
| 32 | + Organization: ArchiTech Labs |
| 33 | + Website: https://www.architechlabs.io |
| 34 | + Version: 1.1.0 |
| 35 | +
|
| 36 | + Based on security methodology developed by ArchiTech Labs. |
| 37 | +
|
| 38 | +.LINK |
| 39 | + https://www.architechlabs.io/articles/external-email-banner/ |
| 40 | +
|
| 41 | +.LINK |
| 42 | + https://learn.microsoft.com/en-us/exchange/security-and-compliance/mail-flow-rules/mail-flow-rules |
| 43 | +
|
| 44 | +.LINK |
| 45 | + https://learn.microsoft.com/en-us/exchange/security-and-compliance/mail-flow-rules/conditions-and-exceptions |
| 46 | +#> |
| 47 | + |
| 48 | +[CmdletBinding(SupportsShouldProcess)] |
| 49 | +param( |
| 50 | + [Parameter(Mandatory = $true, HelpMessage = "Your organization prefix/name for the header (2-50 characters, letters/numbers/spaces only)")] |
| 51 | + [ValidateLength(2, 50)] |
| 52 | + [ValidatePattern('^[A-Za-z0-9\s]+$')] |
| 53 | + [ValidateScript({ |
| 54 | + $cleaned = $_ -replace '\s+', '' -replace '[^A-Za-z0-9]', '' |
| 55 | + if ($cleaned.Length -lt 2 -or $cleaned.Length -gt 50) { |
| 56 | + throw "Organization prefix results in '$cleaned' which must be 2-50 characters after cleanup." |
| 57 | + } |
| 58 | + $true |
| 59 | + })] |
| 60 | + [string]$OrgPrefix, |
| 61 | + |
| 62 | + [Parameter(HelpMessage = "Transport rule priority (0 = highest priority)")] |
| 63 | + [ValidateRange(0, 100)] |
| 64 | + [int]$Priority = 0, |
| 65 | + |
| 66 | + [Parameter(HelpMessage = "Transport rule name")] |
| 67 | + [ValidateNotNullOrEmpty()] |
| 68 | + [string]$RuleName = "Security – Inbound External – Prepend Disclaimer", |
| 69 | + |
| 70 | + [Parameter(HelpMessage = "Create the rule in disabled state (safer for testing)")] |
| 71 | + [switch]$Disabled |
| 72 | +) |
| 73 | + |
| 74 | +# Convert organization prefix to header-safe format |
| 75 | +$HeaderSafePrefix = $OrgPrefix -replace '\s+', '' -replace '[^A-Za-z0-9]', '' |
| 76 | +$HeaderName = "X-$HeaderSafePrefix-Disclaimer-External" |
| 77 | +$HeaderValue = "Applied" |
| 78 | + |
| 79 | +#region Functions |
| 80 | + |
| 81 | +function Install-RequiredModules { |
| 82 | + [CmdletBinding()] |
| 83 | + param([string[]]$ModuleNames) |
| 84 | + |
| 85 | + foreach ($Module in $ModuleNames) { |
| 86 | + if (-not (Get-Module -ListAvailable -Name $Module)) { |
| 87 | + Write-Information "Installing required module: $Module" -InformationAction Continue |
| 88 | + try { |
| 89 | + Install-Module -Name $Module -Force -AllowClobber -Scope CurrentUser -ErrorAction Stop |
| 90 | + Write-Information "Successfully installed $Module" -InformationAction Continue |
| 91 | + } |
| 92 | + catch { |
| 93 | + throw "Failed to install required module '$Module': $($_.Exception.Message). Please run 'Install-Module -Name $Module' manually or ensure you have appropriate permissions." |
| 94 | + } |
| 95 | + } |
| 96 | + } |
| 97 | +} |
| 98 | + |
| 99 | +function Test-ExchangeOnlineConnection { |
| 100 | + [CmdletBinding()] |
| 101 | + param() |
| 102 | + |
| 103 | + try { |
| 104 | + Get-OrganizationConfig -ErrorAction Stop | Out-Null |
| 105 | + Write-Verbose "Connected to Exchange Online" |
| 106 | + } |
| 107 | + catch { |
| 108 | + Write-Information "Not connected to Exchange Online. Attempting to connect..." -InformationAction Continue |
| 109 | + try { |
| 110 | + Connect-ExchangeOnline -ShowProgress:$false -ErrorAction Stop |
| 111 | + Write-Information "Successfully connected to Exchange Online" -InformationAction Continue |
| 112 | + } |
| 113 | + catch { |
| 114 | + throw "Failed to connect to Exchange Online: $($_.Exception.Message). Please ensure you have the necessary permissions and network connectivity." |
| 115 | + } |
| 116 | + } |
| 117 | +} |
| 118 | + |
| 119 | +function Set-ExternalDisclaimerRule { |
| 120 | + [CmdletBinding()] |
| 121 | + param( |
| 122 | + [string]$RuleName, |
| 123 | + [int]$Priority, |
| 124 | + [string]$HeaderName, |
| 125 | + [string]$HeaderValue, |
| 126 | + [string]$BannerHtml, |
| 127 | + [switch]$Disabled |
| 128 | + ) |
| 129 | + |
| 130 | + $existing = Get-TransportRule -Identity $RuleName -ErrorAction SilentlyContinue |
| 131 | + |
| 132 | + $ruleParams = @{ |
| 133 | + Comments = "External email disclaimer per ArchiTech Labs methodology (https://www.architechlabs.io). Prevents duplicates via header stamp. Blog: https://www.architechlabs.io/articles/external-email-banner/" |
| 134 | + Priority = $Priority |
| 135 | + FromScope = 'NotInOrganization' |
| 136 | + SentToScope = 'InOrganization' |
| 137 | + ApplyHtmlDisclaimerLocation = 'Prepend' |
| 138 | + ApplyHtmlDisclaimerText = $BannerHtml |
| 139 | + ApplyHtmlDisclaimerFallbackAction = 'Wrap' |
| 140 | + SetHeaderName = $HeaderName |
| 141 | + SetHeaderValue = $HeaderValue |
| 142 | + ExceptIfHeaderMatchesMessageHeader = $HeaderName |
| 143 | + ExceptIfHeaderMatchesPatterns = $HeaderValue |
| 144 | + Enabled = -not $Disabled |
| 145 | + } |
| 146 | + |
| 147 | + $action = if (-not $existing) { "Creating" } else { "Updating" } |
| 148 | + Write-Information "$action transport rule: $RuleName" -InformationAction Continue |
| 149 | + |
| 150 | + if (-not $existing) { |
| 151 | + New-TransportRule -Name $RuleName @ruleParams -ErrorAction Stop |
| 152 | + } |
| 153 | + else { |
| 154 | + Set-TransportRule -Identity $RuleName @ruleParams -ErrorAction Stop |
| 155 | + } |
| 156 | + |
| 157 | + $stateMsg = if ($Disabled) { " (DISABLED)" } else { " (ENABLED)" } |
| 158 | + Write-Information "Successfully $($action.ToLower()) transport rule: $RuleName$stateMsg" -InformationAction Continue |
| 159 | +} |
| 160 | + |
| 161 | +#endregion Functions |
| 162 | + |
| 163 | +# Install required modules and verify connection |
| 164 | +Install-RequiredModules -ModuleNames @('ExchangeOnlineManagement') |
| 165 | +Test-ExchangeOnlineConnection |
| 166 | + |
| 167 | +# Display configuration |
| 168 | +Write-Information "MODE: Deploy/Update Rule" -InformationAction Continue |
| 169 | +Write-Information "Organization: $OrgPrefix" -InformationAction Continue |
| 170 | +Write-Information "Header Name: $HeaderName" -InformationAction Continue |
| 171 | +Write-Information "Rule Name: $RuleName" -InformationAction Continue |
| 172 | +Write-Information "Priority: $Priority" -InformationAction Continue |
| 173 | +Write-Information "Rule State: $(if ($Disabled) { 'Disabled' } else { 'Enabled' })" -InformationAction Continue |
| 174 | +Write-Information "Duplicate Prevention: Enabled (via $HeaderName header)" -InformationAction Continue |
| 175 | + |
| 176 | +if ($Disabled) { |
| 177 | + Write-Warning "Rule will be created in DISABLED state for safe testing" |
| 178 | +} |
| 179 | + |
| 180 | +# Banner HTML content (using single-quoted here-string to prevent variable expansion) |
| 181 | +$BannerHtml = @' |
| 182 | +<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0" style="mso-table-lspace:0;mso-table-rspace:0;"> |
| 183 | + <tr> |
| 184 | + <td align="left" style="mso-table-lspace:0;mso-table-rspace:0;"> |
| 185 | + <table role="presentation" border="0" cellspacing="0" cellpadding="0" width="760" style="width:100%;max-width:760px;mso-table-lspace:0;mso-table-rspace:0;"> |
| 186 | + <tr> |
| 187 | + <td style="mso-table-lspace:0;mso-table-rspace:0;"> |
| 188 | + <div dir="ltr" lang="en" role="note" aria-label="External email warning" |
| 189 | + style="-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-moz-text-size-adjust:100%; |
| 190 | + mso-line-height-rule:exactly;border:2px solid #d79c2b; |
| 191 | + padding:8px;background:transparent;color:inherit; |
| 192 | + font-family:Arial,Helvetica,sans-serif;font-size:15px;line-height:1.5;"> |
| 193 | + <strong>⚠️ External Email – Check Before You Act</strong><br><br> |
| 194 | + This email is from <strong>outside our organization</strong>.<br> |
| 195 | + • Do not reply, click links, or open attachments unless you trust the sender.<br> |
| 196 | + • If it looks like it came from someone inside, confirm another way before acting.<br> |
| 197 | + • Report suspicious messages using the <strong>REPORT</strong> button. |
| 198 | + </div> |
| 199 | + <div style="line-height:0;font-size:0;" aria-hidden="true"> |
| 200 | + <table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="mso-table-lspace:0;mso-table-rspace:0;"> |
| 201 | + <tr><td style="height:8px;line-height:8px;font-size:8px;"> </td></tr> |
| 202 | + </table> |
| 203 | + </div> |
| 204 | + </td> |
| 205 | + </tr> |
| 206 | + </table> |
| 207 | + </td> |
| 208 | + </tr> |
| 209 | +</table> |
| 210 | +'@ |
| 211 | + |
| 212 | +# Deploy the rule |
| 213 | +try { |
| 214 | + if ($PSCmdlet.ShouldProcess($RuleName, "Create/update transport rule")) { |
| 215 | + Set-ExternalDisclaimerRule -RuleName $RuleName -Priority $Priority -HeaderName $HeaderName -HeaderValue $HeaderValue -BannerHtml $BannerHtml -Disabled:$Disabled |
| 216 | + } |
| 217 | + |
| 218 | + # Display completion message |
| 219 | + Write-Information "Configuration complete!" -InformationAction Continue |
| 220 | + if ($Disabled) { |
| 221 | + Write-Warning "The rule '$RuleName' is created but DISABLED. Enable it when ready to activate." |
| 222 | + Write-Information "To enable: Set-TransportRule -Identity '$RuleName' -Enabled `$true" -InformationAction Continue |
| 223 | + } |
| 224 | + else { |
| 225 | + Write-Information "The rule '$RuleName' is now active with NO authentication exceptions." -InformationAction Continue |
| 226 | + } |
| 227 | +} |
| 228 | +catch { |
| 229 | + Write-Error "Failed to configure transport rule: $($_.Exception.Message)" |
| 230 | + Write-Warning "Common issues: Insufficient Exchange Online permissions, network connectivity, rule name conflicts, or transport rule size limits" |
| 231 | + throw |
| 232 | +} |
0 commit comments