├── .gitattributes
├── AzureAutomation
└── ConnectEXOwithMSIRunbookExample.ps1
├── Configuration
└── invoke-EXOSettingsSwitcher.ps1
├── Email authentication
├── Invoke-0365DKIMEnable.ps1
├── Invoke-DMARCReport.ps1
├── Test-DMARC365.ps1
├── get-o365DkimDNS.ps1
├── invoke-GenerateNewDMARCPolicyRecords.ps1
├── invoke-SPFSuggest.ps1
└── invoke-injectNewDMARCvendor.ps1
├── Hybrid Exchange
├── Create-20HybridExchangeMigrationBatches.ps1
├── Invoke-GrantCloudMailboxAccessToOnPremMailbox.ps1
├── invoke-AddMissingExoTargetDomain.ps1
├── invoke-FixSyncedAccountType.ps1
├── invoke-RemoveDotLocalDomain.ps1
└── invoke-logPurge.ps1
├── LICENSE
├── Queries
└── invoke-GetMismatchedUPNAndEmail.ps1
└── README.md
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/AzureAutomation/ConnectEXOwithMSIRunbookExample.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .DESCRIPTION
3 | An example runbook which connects to Exchange Online using the Managed Identity
4 |
5 | .NOTES
6 | AUTHOR: Michael Mardahl
7 | LASTEDIT: Nov 7, 2021
8 |
9 | ************
10 | UPDATE: Nov 2022. The new EXO v3 PS module now support Managed Identity so this script is obsolete.
11 | https://learn.microsoft.com/en-us/powershell/module/exchange/connect-exchangeonline?view=exchange-ps
12 | ************
13 |
14 | .NOTES
15 | On line 52 there is a filter on the command names (cmdlets) that get imported for use.
16 | You must adjust this to include the cmdlets you need.
17 | Please keep to a minimum as Automation has limited memory.
18 |
19 | .NOTES
20 | This script requires quite excessive permission to Exchange Online in order to work with a Managed Identity.
21 | Assignment of these permissions is done through Azure Cloud shell using the following script.
22 | Remember: Set the correct ObjectID of the $MSIObjectID variable before running the script.
23 | NB: I used Global Reader role, but you might need Exhcange Admin role depending on your needs you can change it.
24 |
25 | #Begin script
26 | Connect-AzureAD
27 | $MSIObjectID = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
28 | $EXOServicePrincipal = Get-AzureADServicePrincipal -Filter "displayName eq 'Office 365 Exchange Online'"
29 | $Approle=$EXOServicePrincipal.AppRoles.Where({$_.Value -eq 'Exchange.ManageAsApp'})
30 | New-AzureADServiceAppRoleAssignment -ObjectId $MSIObjectID -Id $Approle[0].Id -PrincipalId $MSIObjectID -ResourceId $EXOServicePrincipal.ObjectId
31 | $AADRole = Get-AzureADDirectoryRole | where DisplayName -eq 'Exchange Administrator'
32 | Add-AzureADDirectoryRoleMember -ObjectId $AADRole.ObjectId -RefObjectId $MSIObjectID
33 | #End script
34 | #>
35 |
36 | #region declarations
37 | $Script:tenantDomain = "xxxxxxxxxxx.onmicrosoft.com" #please enter the defualt domain of the tenant the managed identity belongs to.
38 | #endregion declarations
39 |
40 | #region functions
41 | function makeMSIOAuthCred () {
42 | $accessToken = Get-AzAccessToken -ResourceUrl "https://outlook.office365.com/"
43 | $authorization = "Bearer {0}" -f $accessToken.Token
44 | $Password = ConvertTo-SecureString -AsPlainText $authorization -Force
45 | $tenantID = (Get-AzTenant).Id
46 | $MSIcred = New-Object System.Management.Automation.PSCredential -ArgumentList ("OAuthUser@$tenantID",$Password)
47 | return $MSICred
48 | }
49 |
50 | function connectEXOAsMSI ($OAuthCredential) {
51 | #Function to connect to Exchange Online using OAuth credentials from the MSI
52 | $psSessions = Get-PSSession | Select-Object -Property State, Name
53 | If (((@($psSessions) -like '@{State=Opened; Name=RunSpace*').Count -gt 0) -ne $true) {
54 | Write-Verbose "Creating new EXOPSSession..." -Verbose
55 | try {
56 | $Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "https://outlook.office365.com/PowerShell-LiveId?BasicAuthToOAuthConversion=true&email=SystemMailbox%7bbb558c35-97f1-4cb9-8ff7-d53741dc928c%7d%40$tenantDomain" -Credential $OAuthCredential -Authentication Basic -AllowRedirection
57 | $null = Import-PSSession $Session -DisableNameChecking -CommandName "*mailbox*", "*unified*" -AllowClobber
58 | Write-Verbose "New EXOPSSession established!" -Verbose
59 | } catch {
60 | Write-Error $_
61 | }
62 | } else {
63 | Write-Verbose "Found existing EXOPSSession! Skipping connection." -Verbose
64 | }
65 | }
66 | #endregion functions
67 |
68 | #region execute
69 | $null = Connect-AzAccount -Identity
70 | connectEXOAsMSI -OAuthCredential (makeMSIOAuthCred)
71 |
72 | #Do some exchange scripting here!
73 |
74 | Get-PSSession | Remove-PSSession
75 | #endregion execute
76 |
--------------------------------------------------------------------------------
/Configuration/invoke-EXOSettingsSwitcher.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .SYNOPSIS
3 | An on/off switch for Exchange Online Organization Configuration settings
4 |
5 | .DESCRIPTION
6 | This script will iterate through a list of useful Exchange Online Settings, and allow the admin to turn them on or off while showing the current setting.
7 | Can be used as a base script for switching other simmilar settings in Microsoft 365 Powershell modules.
8 |
9 | .INPUTS
10 | None
11 |
12 | .NOTES
13 | Version : 1.0a
14 | Author : Michael Mardahl
15 | Twitter : @michael_mardahl
16 | Blogging on : www.msendpointmgr.com
17 | Creation Date : 26 April 2021
18 | Updated Date : -
19 | Purpose/Change: Initial development
20 | License : MIT (Leave author credits)
21 |
22 | .EXAMPLE
23 | Execute script
24 | .\invoke-EXOSettingsSwitcher.ps1
25 | (Needs to be executed interactively by a user with the Exchange Administrator role)
26 | #>
27 | #Requires -Modules ExchangeOnlineManagement
28 |
29 | #region declarations
30 |
31 | #You can add more setting here if you like, all settings can be gathered using the "Get-OrganizationConfig" cmdlet with ExchangeOnline
32 | $settings = @("SendFromAliasEnabled","OAuth2ClientProfileEnabled","AllowPlusAddressInRecipients","FocusedInboxOn")
33 |
34 | #endregion declarations
35 | #region execute
36 |
37 | connect-exchangeonline -ShowBanner:$false
38 | $welcomeArt = @"
39 | ____ __ ____ ___
40 | / __/_ ______/ / ___ ____ ___ ____ / __ \___ / (_)__ ___
41 | / _/ \ \ / __/ _ \/ _ ``/ _ \/ _ ``/ -_) /_/ / _ \/ / / _ \/ -_)
42 | /___//_\_\\__/_//_/\_,_/_//_/\_, /\__/\____/_//_/_/_/_//_/\__/
43 | / __/__ / /_/ /_(_)__ ___/___/ / __/ __(_) /_____/ / ___ ____
44 | _\ \/ -_) __/ __/ / _ \/ _ ``(_-< _\ \| |/|/ / / __/ __/ _ \/ -_) __/
45 | /___/\__/\__/\__/_/_//_/\_, /___/ /___/|__,__/_/\__/\__/_//_/\__/_/
46 | /___/
47 | "@
48 | Write-Host $welcomeArt -ForegroundColor Yellow
49 |
50 | foreach($option in $settings) {
51 | #Read each setting individually and allow user to choose what to do
52 | try {
53 | [bool]$currentSetting = Get-OrganizationConfig -ErrorAction Stop| Select $option -ExpandProperty $option
54 | } catch {
55 | $currentSetting = $false
56 | }
57 | Write-Host " "
58 | Write-Host "$option"
59 | #Add some readable text output
60 | $settingText = if ($currentSetting) {"Enabled"} Else {"Disabled"}
61 | Write-Host "`t ^^^^^^^^^^^^ `t is currently $settingText"
62 | Write-Host " "
63 | Do {
64 | $choise = (Read-Host "Do you want to flip the switch for option: $option [y/n]?")
65 | if (($choise -ne "y") -and ($choise -ne "n")){
66 | Write-Host "Please enter (y)es or (n)o." -ForegroundColor Yellow
67 | }
68 | } While(($choise -ne "y") -and ($choise -ne "n"))
69 | Write-Host " "
70 | if($choise -eq "y") {
71 | #Flip the setting from true to false and vice versa
72 | $newSetting = if ($currentSetting) {$false} Else {$true}
73 | Write-Verbose "Configuring $option as $([string]$newSetting)" -Verbose
74 | #Define the parameter set for the option and setting
75 | $parameters = @{
76 | $option = $newSetting
77 | }
78 | Set-OrganizationConfig @parameters
79 | $validateSetting = Get-OrganizationConfig | Select $option -ExpandProperty $option
80 | Write-Verbose "The new setting of $option is: $validateSetting" -Verbose
81 | }
82 |
83 | Write-Host " "
84 | }
85 | #endregion execute
86 | Disconnect-ExchangeOnline
87 |
--------------------------------------------------------------------------------
/Email authentication/Invoke-0365DKIMEnable.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .SYNOPSIS
3 | Enables DKIM for all domains in Office 365 Exchange Online
4 |
5 | .DESCRIPTION
6 | This script will get all domains in Exchange Online and enable DKIM and rotate the key to 2048bit.
7 |
8 | .INPUTS
9 | None
10 |
11 | .NOTES
12 | Version : 1.2
13 | Author : Michael Mardahl
14 | Twitter : @michael_mardahl
15 | Blogging on : www.msendpointmgr.com
16 | Creation Date : 02 December 2020
17 | Updated Date : 04 February 2021
18 | Purpose/Change: Moved from GIST
19 | License : MIT (Leave author credits)
20 |
21 | .EXAMPLE
22 | Execute script
23 | .\Invoke-O365DKIMEnable.ps1
24 | (Needs to be executed interactively)
25 |
26 | #>
27 |
28 |
29 | #region execute
30 |
31 | try{
32 | Connect-ExchangeOnline -ErrorAction Stop
33 | } catch {
34 | Throw "Failed to logon to Exchange Online, make sure you have installed the Exchange Online Management v2 module"
35 | }
36 |
37 | $dkim = Get-DkimSigningConfig
38 |
39 | foreach($obj in $dkim){
40 | Write-Host "Enabling 2048-bit DKIM for $($obj.Domain)" -ForegroundColor Green
41 | Write-Verbose "Enable - DKIM" -Verbose
42 | Set-DkimSigningConfig -Identity $($obj.Domain) -Enabled $true
43 | if($obj.Enabled){
44 | Write-Verbose "Rotating key to 2048-bit" -Verbose
45 | Rotate-DkimSigningConfig -KeySize 2048 -Identity $($obj.Domain)
46 | }
47 | Write-Output " "
48 | pause
49 | }
50 |
51 | #endregion execute
52 |
--------------------------------------------------------------------------------
/Email authentication/Invoke-DMARCReport.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .SYNOPSIS
3 | Scans list of domains for DMARC, SFP and Office 365 DKIM records.
4 | .DESCRIPTION
5 | This script will give you a CSV and nice HTML output for easy overview of multiple domains DMARC, SPF and Office 365 DKIM status.
6 | It will aid you in gaining a nice overview for getting to the goal of DMARC reject policy on all your domains.
7 | .INPUTS
8 | None
9 | Just have a txt file called domains.txt in the same directory as the script, with one domain per line in the file.
10 | .NOTES
11 | Version : 1.3
12 | Author : Michael Mardahl
13 | Twitter : @michael_mardahl
14 | Blogging on : www.msendpointmgr.com
15 | Creation Date : 19 February 2022
16 | Updated Date : 01 March 2022
17 | Purpose/Change: Added progress output instead of text. added info output to log instead of screen.
18 | License : MIT (Leave author credits)
19 | .EXAMPLE
20 | Execute script
21 | .\Invoke-DMARCReport.ps1
22 | (Needs to be executed interactively)
23 | #>
24 |
25 | #region declarations
26 |
27 | #Output to CSV location
28 | $resultOutput = ".\DMARCScanResults-$(get-date -format ddMMyyy)" # Do NOT define file extention as several will be used by the script!
29 |
30 | #get list of domains to process from this file
31 | $domainFile = Get-Content .\domains.txt #multiple domains one per line
32 |
33 | #DNS server (should not be local DNS to avoid split brain DNS errors)
34 | $DNSServer = "1.1.1.1" #CloudFlare DNS One One One One is default
35 |
36 | #Progressbar enabled. Set as "SilentlyContinue" to disable
37 | $ProgressPreference = "Continue"
38 |
39 | #endregion declarations
40 |
41 | #region execute
42 |
43 | #log execution to file
44 | Start-Transcript ".\Invoke-DMARCReport_ExecutionLog-$(get-date -format ddMMyyy).txt" -Force
45 |
46 | #fix possible duplicates and sort domains
47 | $domains = $domainFile | sort | Get-Unique
48 |
49 | #Array for holding results
50 | $results = @()
51 |
52 | #progress bar counter
53 | $i = 0
54 |
55 | #process domains
56 | foreach ($domain in $domains){
57 | #trim whitespace
58 | $dom = $domain.Trim()
59 | #main progress
60 | Write-Progress -Id 1 -Activity "Scanning DNS records" -Status "$([math]::Round($i/$domains.count*100))% Complete:" -PercentComplete $([math]::Round($i/$domains.count*100))
61 | #sub progress
62 | Write-Progress -Id 2 -Activity "$dom" -Status "0% Complete:" -CurrentOperation "DMARC" -PercentComplete 0 -ParentId 1
63 |
64 | $dompad = $(($dom + ".").PadRight(36))
65 |
66 |
67 | $dmarc = Resolve-DnsName "_dmarc.$dom" -Server $DNSServer -Type TXT -ErrorAction SilentlyContinue
68 | if (!$dmarc.Strings){
69 | Write-Information "_dmarc.$dompad is missing DMARC!"
70 | $DMARCRecordValue = 'N/A'
71 | $DMARCRecordPolicy = 'N/A'
72 | } else {
73 | Write-Information "_dmarc.$dompad $($dmarc.Strings)"
74 | $DMARCRecordValue = [string]$dmarc.Strings
75 | if($DMARCRecordValue -ilike '*p=none;*'){
76 | $DMARCRecordPolicy = "none"
77 | } elseif($DMARCRecordValue -ilike '*p=quarantine;*'){
78 | $DMARCRecordPolicy = "quarantine"
79 | } elseif($DMARCRecordValue -ilike '*p=reject;*'){
80 | $DMARCRecordPolicy = "reject"
81 | } else {
82 | $DMARCRecordPolicy = "Error"
83 | }
84 | }
85 |
86 | Write-Progress -Id 2 -Activity "$dom" -Status "25% Complete:" -CurrentOperation "SPF" -PercentComplete 25 -ParentId 1
87 |
88 | $spf = Resolve-DnsName $dom -Server $DNSServer -Type txt -ErrorAction SilentlyContinue | where strings -Like "v=spf1*"
89 | if (!$spf.Strings){
90 | Write-Information "$dompad is missing SPF!"
91 | $SPFRecordValue = 'N/A'
92 | $SPFRecordPolicy = 'N/A'
93 | } else {
94 | Write-Information "$dompad $($spf.Strings)"
95 | $SPFRecordValue = [string]$spf.Strings
96 | if($SPFRecordValue -ilike "*`-all"){
97 | $SPFRecordPolicy = "Fail"
98 | } elseif($SPFRecordValue -ilike "*`~all"){
99 | $SPFRecordPolicy = "SoftFail"
100 | } elseif($SPFRecordValue -ilike "*`?all"){
101 | $SPFRecordPolicy = "Neutral"
102 | } elseif($SPFRecordValue -ilike "*`+all"){
103 | $SPFRecordPolicy = "Pass"
104 | } else {
105 | $SPFRecordPolicy = "Error"
106 | }
107 | }
108 |
109 | Write-Progress -Id 2 -Activity "$dom" -Status "50% Complete:" -CurrentOperation "DKIM o365" -PercentComplete 50 -ParentId 1
110 |
111 | $o365DKIM = Resolve-DnsName "selector1._domainkey.$dom" -Server $DNSServer -Type CNAME -ErrorAction SilentlyContinue
112 | if (-not ($o365DKIM.NameHost -like "*.onmicrosoft.com")){
113 | Write-Information "$dompad does not have an Office 365 DKIM selector defined."
114 | $DKIMRecordExists = $false
115 | } else {
116 | Write-Information "selector1._domainkey.$dompad $($o365DKIM.NameHost)"
117 | $DKIMRecordExists = $true
118 | }
119 |
120 | Write-Progress -Id 2 -Activity "$dom" -Status "75% Complete:" -CurrentOperation "Finalizing" -PercentComplete 75 -ParentId 1
121 |
122 | #add results to array as custom object
123 | $results += [PSCustomObject]@{
124 | 'Domain' = $dom
125 | 'DMARCRecordExists' = $(if($DMARCRecordValue -eq 'N/A'){$false} else {$true})
126 | 'DMARCRecordValue' = [string]$DMARCRecordValue
127 | 'DMARCRecordPolicy' = [string]$DMARCRecordPolicy
128 | 'O365DKIMExists' = $DKIMRecordExists
129 | 'SPFRecordExists' = $(if($SPFRecordValue -eq 'N/A'){$false} else {$true})
130 | 'SPFRecordValue' = [string]$SPFRecordValue
131 | 'SPFRecordPolicy' = [string]$SPFRecordPolicy
132 |
133 | }
134 |
135 | #Attempt to avoid rate limiting for DNS - decrease "divisible by" number to adjust.
136 | if(($i -ne 0) -and ($i%100) -eq 0){
137 | Write-Progress -Id 2 -Activity "$dom" -Status "100% Complete:" -CurrentOperation "Done! - Pausing for 10 seconds to avoid rate limiting." -PercentComplete 100 -ParentId 1
138 | Start-Sleep -Seconds 10
139 | } else {
140 | Write-Progress -Id 2 -Activity "$dom" -Status "100% Complete:" -CurrentOperation "Done!" -PercentComplete 100 -ParentId 1
141 | }
142 |
143 |
144 | #main progress increase
145 | $i++
146 |
147 | }
148 | Write-Information "Finished processing domains"
149 | Clear-Host
150 | Write-Information "Generating output CSV"
151 |
152 | #Progress for finalization
153 | Write-Progress -Id 3 -Activity "Generating output." -Status "0% Complete:" -PercentComplete 0
154 |
155 | $results | Export-Csv -NoTypeInformation -Path "$resultOutput.csv" -Force
156 |
157 | #Progress for finalization
158 | Write-Progress -Id 3 -Activity "Generating output." -Status "50% Complete:" -PercentComplete 50
159 |
160 | Write-Information "Generating output HTML"
161 | #Generate some nice HTML
162 | $js=@"
163 |
164 | "@
165 | $Header = @"
166 |
167 |
169 | "@
170 | $htmlParams = @{
171 | Title = "DMARC status report $(get-date -format ddMMMyyy)"
172 | Head = "$Header"
173 | Body = "
DMARC status report
"
174 | PreContent = "$(get-date -format ddMMMyyy) - $($results.count) domains queried via DNS server $DNSServer
"
175 | PostContent = "$js"
176 | }
177 | #fix the output so we can sort the table
178 | $HTML = $results | ConvertTo-Html @htmlParams
179 | $HTML = $HTML -replace '',''
180 | $HTML = $HTML -replace '.*',''
181 | $HTML = $HTML -replace '',""
182 | $HTML = $HTML -replace '
','
'
183 | $HTML = $HTML -replace 'True | ','True | '
184 | $HTML = $HTML -replace 'False | ','False | '
185 | $HTML = $HTML -replace 'none | ','none | '
186 | $HTML = $HTML -replace 'quarantine | ','quarantine | '
187 | $HTML = $HTML -replace 'reject | ','reject | '
188 | $HTML = $HTML -replace 'Error | ','Error | '
189 | $HTML = $HTML -replace 'Fail | ','Fail | '
190 | $HTML = $HTML -replace 'SoftFail | ','SoftFail | '
191 | $HTML = $HTML -replace 'Neutral | ','Neutral | '
192 | $HTML = $HTML -replace 'Pass | ','Pass | '
193 | $HTML | Out-File "$resultOutput.html" -Force
194 |
195 | #Progress for finalization
196 | Write-Progress -Id 3 -Activity "Generating output." -Status "100% Complete:" -PercentComplete 100
197 |
198 | Stop-Transcript
199 | #endregion execute
200 |
--------------------------------------------------------------------------------
/Email authentication/Test-DMARC365.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .SYNOPSIS
3 | Check domains for DMARC readiness with Exchange Online
4 |
5 | .DESCRIPTION
6 | This script will verify the presence of the required records for using DMARC with a domain in Exchange Online.
7 | It's a very simple check to let you know if anything was missed on one or more domains.
8 | The output is sent to a GridView where it is easy to copy to a spreadsheet for further work.
9 |
10 | .INPUTS
11 | None
12 |
13 | .NOTES
14 | Version : 2.0
15 | Author : Michael Mardahl
16 | Twitter : @michael_mardahl
17 | Blogging on : iphase.dk & www.msendpointmgr.com
18 | Creation Date : 02 December 2020
19 | Purpose/Change: Added record output
20 | License : MIT (Leave author credits)
21 |
22 | .EXAMPLE
23 | Execute script after modification.
24 | .\Test-DMARC365.ps1
25 | (Needs to be executed interactively)
26 |
27 | .NOTES
28 | You ned to edit the here string list of domains in the "declarations" region of the script.
29 | If you are not familiar with here strings, please notice and keep the formatting.
30 | For more advanced cases, you can modify to use a CSV file.
31 |
32 | #>
33 |
34 | #region declarations
35 |
36 | $domainList = @"
37 | microsoft.com
38 | msendpointmgr.com
39 | apento.com
40 | iphase.dk
41 | "@
42 |
43 | #endregion declarations
44 |
45 | #region functions
46 |
47 | #Custom function to generate object with domain specific data about DKIM, DMARC, SPF and MX
48 | function getDomainInfo {
49 | [cmdletbinding()]
50 | param (
51 |
52 | [Parameter(Mandatory = $true)]
53 | [String]$FQDN
54 |
55 | )
56 | #Hide errors
57 | $prevErrPref = $ErrorActionPreference
58 | $ErrorActionPreference = "SilentlyContinue"
59 |
60 | #Set values to not found
61 | $resultMX = "N/A";$resultDMARC = "N/A"; $resultDKIM = "N/A"; $resultSPF = "N/A"
62 | $resultMXRecord = "N/A";$resultDMARCRecord = "N/A"; $resultDKIMRecord = "N/A"; $resultSPFRecord = "N/A"
63 |
64 | #Testing MX, SPF, DMARC and DKIM
65 |
66 | #MX
67 | if(Resolve-DnsName $FQDN -Type MX | select NameExchange -First 1 -ExpandProperty NameExchange){
68 |
69 | $resultMX = "Present"
70 | $resultMXRecord = Resolve-DnsName $FQDN -Type MX | select NameExchange -First 1 -ExpandProperty NameExchange
71 |
72 | }
73 |
74 | #SPF
75 | if(Resolve-DnsName $FQDN -Type TXT | Where-Object Strings -ILike "v=spf1*"){
76 |
77 | $resultSPF ="Present"
78 | $resultSPFRecordArr = Resolve-DnsName $FQDN -Type TXT | Where-Object Strings -ILike "v=spf1*" | select Strings -ExpandProperty Strings
79 | $resultSPFRecord = $resultSPFRecordArr -join ""
80 |
81 | }
82 |
83 | #DMARC
84 | if(Resolve-DnsName "_dmarc.$FQDN" -Type TXT | Where-Object Strings -ILike "v=DMARC1*"){
85 |
86 | $resultDMARC = "Present"
87 | $resultDMARCRecord = Resolve-DnsName "_dmarc.$FQDN" -Type TXT | Where-Object Strings -ILike "v=DMARC1*" | select Strings -ExpandProperty Strings
88 |
89 | }
90 |
91 | #DKIM
92 | $DKIM1 = Resolve-DnsName "selector1._domainkey.$FQDN" -Type CNAME
93 | $DKIM2 = Resolve-DnsName "selector2._domainkey.$FQDN" -Type CNAME
94 | if(($DKIM1.NameHost -ilike "selector1.*") -or ($DKIM2.NameHost -ilike "selector2.*")){
95 |
96 | $resultDKIM = "Present"
97 | $resultDKIMRecord = "$($DKIM1 | select NameHost -ExpandProperty NameHost -ErrorAction SilentlyContinue) | $($DKIM2 | select NameHost -ExpandProperty NameHost -ErrorAction SilentlyContinue)"
98 |
99 | }
100 |
101 | $statusObject = [PSCustomObject]@{
102 |
103 | DomainName = $FQDN
104 | DMARC = $resultDMARC
105 | DMARCRecord = $resultDMARCRecord
106 | MX = $resultMX
107 | MXRecord = $resultMXRecord
108 | SPF = $resultSPF
109 | SPFRecord = $resultSPFRecord
110 | DKIM = $resultDKIM
111 | DKIMRecord = $resultDKIMRecord
112 |
113 | }
114 |
115 | #Reset error messages and return object
116 | $ErrorActionPreference = $prevErrPref
117 | Return $statusObject
118 | }
119 |
120 | #endregion functions
121 |
122 | #region execute
123 |
124 | Write-Host "[INFO] Processing domains for DMARC status..."
125 |
126 | #Array with all the domains data
127 | [System.Collections.ArrayList]$statusArray = @()
128 |
129 | #Iterate throguh the domains with the custom function
130 | $domainArray = $domainList -split "`r`n"
131 | foreach($dom in $domainArray){
132 |
133 | $trimDom = $dom.Trim()
134 | $statusArray.Add((getDomainInfo -FQDN $trimDom)) | Out-Null
135 |
136 | }
137 |
138 | Write-Host "[INFO] Launching GridView."
139 | #Output to gridview (can be copied directly to spreadsheet
140 | $statusArray | Out-GridView -Title "DMARC test result" -OutputMode Multiple
141 |
142 | Write-Host "[INFO] Finished."
143 |
144 | #endregion execute
145 |
--------------------------------------------------------------------------------
/Email authentication/get-o365DkimDNS.ps1:
--------------------------------------------------------------------------------
1 | # By Michael Mardahl
2 | # github.com/mardahl
3 |
4 | #requires -RunAsAdministrator
5 | Set-ExecutionPolicy Bypass -Confirm:$false -Force
6 | Install-Module ExchangeOnlineManagement
7 | Import-Module ExchangeOnlineManagement
8 |
9 | try{
10 | Connect-ExchangeOnline -ErrorAction Stop
11 | } catch {
12 | Throw "Failed to logon to Exchange Online"
13 | }
14 |
15 | $dkim = Get-DkimSigningConfig | Where-Object Identity -NotLike "*onmicrosoft*"
16 |
17 | foreach($obj in $dkim){
18 | Write-Host "DNS records for $($obj.Domain)" -ForegroundColor Green
19 | Write-Output " TYPE = CNAME"
20 | Write-Output " TTL = 900"
21 | Write-Output " HOSTNAME = selector1._domainkey.$($obj.Domain)"
22 | Write-Output " VALUE = $($obj.Selector1CNAME)."
23 | Write-Output " TYPE = CNAME"
24 | Write-Output " TTL = 900"
25 | Write-Output " HOSTNAME = selector2._domainkey.$($obj.Domain)"
26 | Write-Output " VALUE = $($obj.Selector2CNAME)."
27 | Write-Output " "
28 | }
29 |
--------------------------------------------------------------------------------
/Email authentication/invoke-GenerateNewDMARCPolicyRecords.ps1:
--------------------------------------------------------------------------------
1 | #Script by mum@apento.com to change DMARC policy on existing valid records.
2 | Start-Transcript .\lastrunLog.txt -Force
3 | $output = ".\output$(get-date -format ddMMyyy).txt"
4 | Write-Host "DMARC RECORDS:" | Out-File -FilePath $output
5 |
6 | $RUA = "xxx@rua.domain.com" #must be the rUA adress that is currently in use, since we match validity based on this.
7 |
8 | #set the policy you want to have - none,quarantine,reject
9 | $policy = "reject"
10 |
11 | #one domain per line.
12 | $domains = @"
13 | domain.com
14 | domain.dk
15 | domain.co.uk
16 | "@ -split "`r`n"
17 |
18 | #fix possible duplicates and sort domains
19 | $domains = $domains | sort | Get-Unique
20 |
21 |
22 | foreach ($dom in $domains){
23 | Write-Host ""
24 | $dompad = $(($dom + ".").PadRight(32))
25 | try {
26 | $dmarc = Resolve-DnsName "_dmarc.$dom" -Type TXT -ErrorAction SilentlyContinue
27 | if (!$dmarc.Strings){
28 | throw "no value"
29 | } elseif (($dmarc.Strings).ToLower() -notlike "v=dmarc1*"){
30 | throw "invalid value"
31 | } else {
32 | if ($dmarc.Strings -notlike "*$RUA*"){
33 | Write-Host "_dmarc.$dom currently has no valid existing company DMARC policy (skipping):" -ForegroundColor Red
34 | Write-Host "Value : $($dmarc.Strings)" -ForegroundColor DarkGray
35 | continue
36 | }
37 | else {
38 | #Generating new DMARC value string
39 | $newValueArray = $dmarc.Strings -split ";"
40 | #find policy indexe
41 | $policyIndex = ""
42 | foreach ($item in $newValueArray){
43 | $index = $newValueArray.IndexOf($item)
44 | if ($item -ilike "* p=*"){
45 | $policyIndex = $index
46 | }
47 | }
48 |
49 | #add new policy
50 |
51 | $newValueArray[$policyIndex] = " p=$policy"
52 | $newValue = $newValueArray -join ";"
53 | }
54 |
55 | }
56 | Write-Host "$dom currently has the following DMARC policy:" -ForegroundColor Cyan
57 | Write-Host "Old Value : $($dmarc.Strings)" -ForegroundColor DarkGray
58 | Write-Host "$dom needs to be updated - Please add:" -ForegroundColor Yellow
59 | $msg = "_dmarc.$dompad`t IN TXT `"$newValue`""
60 | Write-Host $msg
61 | $msg | Out-File -FilePath $output -Append
62 | } catch {
63 | write-error $_
64 | continue
65 | }
66 |
67 | }
68 | Stop-Transcript
69 | Get-Content $output | Out-GridView
70 |
--------------------------------------------------------------------------------
/Email authentication/invoke-SPFSuggest.ps1:
--------------------------------------------------------------------------------
1 | #quick Script by github.com/mardahl to inject to suggest SPF records for domains that are missing them.
2 | Start-Transcript .\lastrunLog.txt -Force
3 | $output = ".\output$(get-date -format ddMMyyy).txt"
4 |
5 |
6 | #one domain per line.
7 | $domains = @"
8 | domain.com
9 | domain.dk
10 | domain.co.uk
11 | "@ -split "`r`n"
12 |
13 | foreach ($dom in $domains){
14 | Write-Host ""
15 | $dompad = $(($dom + ".").PadRight(32))
16 |
17 | #if no SPF suggest softfail SPF
18 | $spf = ""
19 | $spf = Resolve-DnsName "$dom" -Type TXT -ErrorAction SilentlyContinue | Where-Object Strings -ILike "*spf1*"
20 | if (!$spf.Strings){
21 | $msg = "$dompad`t IN TXT `"v=spf1 ~all`""
22 | Write-Host $msg
23 | $msg | Out-File -FilePath $output -Append
24 | continue
25 | }
26 |
27 | Write-Verbose "Existing SPF found for $dom" -Verbose
28 | Write-Verbose "$($spf.Strings)" -Verbose
29 |
30 | #if no MX suggest fail SPF
31 | $mx = ""
32 | $mx = Resolve-DnsName "$dom" -Type MX -ErrorAction SilentlyContinue
33 | if (!$mx.Name){
34 | Write-Verbose "No MX record found though! Suggesting no-sending SPF." -Verbose
35 |
36 | $msg = "###$dompad`t IN TXT `"v=spf1 -all`""
37 | Write-Host $msg
38 | $msg | Out-File -FilePath $output -Append
39 |
40 | }
41 |
42 | }
43 | Stop-Transcript
44 |
--------------------------------------------------------------------------------
/Email authentication/invoke-injectNewDMARCvendor.ps1:
--------------------------------------------------------------------------------
1 | #Script by github.com/mardahl to inject new DMARC vendor into existing records, to avoid disrupting existing work with DMARC.
2 | #NB: the script will generate DMARC DNS values for domains without a DMARC policy and set it as none, the same goes for invalid records.
3 |
4 | Start-Transcript .\lastrunLog$(get-date -format ddMMyyy).txt -Force
5 | $output = ".\output$(get-date -format ddMMyyy).txt"
6 |
7 | #region declarations
8 |
9 | #enter your desired new RUA mail
10 | $newRUA = "xxxxx@ag.eu.dmarcian.com"
11 |
12 | #options to remove other elements of the policy
13 | $removeRUF = $true #most systems don't send RUF, so best get rid of it because of PII.
14 | $removeFO = $true #usually only used if you know exactly what you are doing.
15 |
16 |
17 | #list of domains to process, (only one per line)
18 | $domains = @"
19 | iphase.dk
20 | apento.com
21 | microsoft.com
22 | google.com
23 | github.com
24 | "@ -split "`r`n"
25 |
26 | #filtering for duplicates just in case.
27 | $domains = $domains | sort | Get-Unique
28 |
29 | foreach ($dom in $domains){
30 | Write-Host ""
31 | $dompad = $(($dom + ".").PadRight(32))
32 | try {
33 | $dmarc = Resolve-DnsName "_dmarc.$dom" -Type TXT -ErrorAction SilentlyContinue
34 | if (!$dmarc.Strings){
35 | throw "no value in $dom"
36 | } elseif (($dmarc.Strings).ToLower() -notlike "v=dmarc1*"){
37 | throw "invalid value in $dom"
38 | } else {
39 | if ($dmarc.Strings -like "*$newRUA*"){
40 | Write-Host "_dmarc.$dom already has the new RUA (skipping):" -ForegroundColor Red
41 | Write-Host "Value : $($dmarc.Strings)" -ForegroundColor DarkGray
42 | continue
43 | }
44 | else {
45 | #Generating new DMARC values array
46 | $newValueArray = $dmarc.Strings -split ";"
47 | #find RUA index
48 | $ruaIndex = ""
49 | foreach ($item in $newValueArray){
50 | $index = $newValueArray.IndexOf($item)
51 | if ($item -ilike " rua=*"){
52 | $ruaIndex = $index
53 | }
54 | }
55 | #find RUF index
56 | $rufIndex = ""
57 | foreach ($item in $newValueArray){
58 | $index = $newValueArray.IndexOf($item)
59 | if ($item -ilike " ruf=*"){
60 | $rufIndex = $index
61 | }
62 | }
63 | #find FO index
64 | $foIndex = ""
65 | foreach ($item in $newValueArray){
66 | $index = $newValueArray.IndexOf($item)
67 | if ($item -ilike " fo=*"){
68 | $foIndex = $index
69 | }
70 | }
71 |
72 | #add new policy
73 |
74 | #replace RUA
75 | $newValueArray[$ruaIndex] = " rua=mailto:$newRUA"
76 | #Remove RUF
77 | if($removeRUF){
78 | $newValueArray = $newValueArray | Where-Object { $_ -ne $newValueArray[$rufIndex] }
79 | }
80 | #Remove FO
81 | if($removeFO){
82 | $newValueArray = $newValueArray | Where-Object { $_ -ne $newValueArray[$foIndex] }
83 | }
84 | #Create new DNS record
85 | $newValue = $newValueArray -join ";"
86 | }
87 |
88 | }
89 | Write-Host "$dom currently has the following DMARC policy:" -ForegroundColor Cyan
90 | Write-Host "Current Value : $($dmarc.Strings)" -ForegroundColor DarkGray
91 | Write-Host "$dom DNS needs to be updated - Please replace current record with:" -ForegroundColor Yellow
92 | $msg = "$dom,TXT,_dmarc,$newValue;,Change value"
93 | Write-Host $msg
94 | $msg | Out-File -FilePath $output -Append
95 | } catch {
96 | if($_ -like "*no value*"){
97 | Write-Host "$dom currently has NO DMARC policy:" -ForegroundColor Cyan
98 | $newValue = "v=DMARC1; p=none; rua=mailto:$newRUA;"
99 | $msg = "$dom,TXT,_dmarc,$newValue,Add"
100 | Write-Host $msg
101 | $msg | Out-File -FilePath $output -Append
102 | } elseif($_ -like "*invalid*"){
103 | Write-Host "$dom currently has the following INVALID DMARC policy:" -ForegroundColor Cyan
104 | Write-Host "Current Value : $($dmarc.Strings)" -ForegroundColor DarkGray
105 | Write-Host "$dom DNS needs to be fixed - Please replace current record with:" -ForegroundColor Yellow
106 | $newValue = "v=DMARC1; p=none; rua=mailto:$newRUA;"
107 | $msg = "$dom,TXT,_dmarc,$newValue,Change invalid record"
108 | Write-Host $msg
109 | $msg | Out-File -FilePath $output -Append
110 | } else {
111 | write-error $_
112 | }
113 | continue
114 | }
115 |
116 | }
117 | notepad.exe $output
118 | Stop-Transcript
119 |
--------------------------------------------------------------------------------
/Hybrid Exchange/Create-20HybridExchangeMigrationBatches.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .SYNOPSIS
3 | Creates about 20 Exchange Online Migration batches containing all the eligeble users in the On-prem Exchange Organization
4 |
5 | .DESCRIPTION
6 | Can be used by admins to simply create and start about 20 migration batches containing all the user synced
7 | to Office 365 with the "MailUser" recipient type set.
8 | UPN's containing onmicrosoft.com will be skipped!
9 | The script can be run from your local computer as it connects onnly to exchange online.
10 | If some users are missing, then make sure you have actually synced them from on-prem with AAD Connect.
11 |
12 | .OUTPUTS
13 | A bunch of CSV files wit the users are left in your temp folder, depending on how you configure the declarations.
14 |
15 | .EXAMPLE
16 | Run the script without any parameters
17 | .\Create-20HybridExchangeMigrationBatches.ps1
18 |
19 |
20 | .NOTES
21 | The script must be run interactively.
22 | This script requires the Exchange Online V2 Powershell module, which it will try to install.
23 | Modify the declarations region of the script in order to succeed with the execution.
24 |
25 | Licensed under MIT, feel free to modify and distribute freely, but leave author credits.
26 |
27 | Created by Michael Mardahl
28 | Twitter: @michael_mardahl
29 | Github : github.com/mardahl
30 | #>
31 |
32 | #region Declarations
33 | $TargetDeliveryDomain = "REPLACE-CAPS-WITH-TENANTNAME.mail.onmicrosoft.com"
34 | $notificationEmail = "myuser@mydomain.com" #will receive batch completion and failure notifications
35 | $badItemLimit = "1000" #Adjust as you see fit
36 | $csvTempPath = join-path "$env:temp" "\CSVTemp"
37 | #endregion Declarations
38 |
39 | #region Execute
40 | Write-Verbose "Connecting to Exchange Online" -Verbose
41 | try {
42 | #Verify Exchange Online V2 modules are installed
43 | if(!(Get-InstalledModule ExchangeOnlineManagement)){
44 | Install-Module ExchangeOnlineManagement -Force
45 | }
46 | #Connect to exchange online using modern authentication
47 | Connect-ExchangeOnline -ShowBanner:$false -ErrorAction Stop
48 | } catch {
49 | Throw "Could not connect to exchange online, make sure to get the exchange online v2 module installed"
50 | }
51 | Write-Verbose "Connecting to Exchange Online completed." -Verbose
52 |
53 | #Build sorted list of users
54 | Write-Verbose "Building list of users that can be migrated." -Verbose
55 | $batchUsers = Get-User | Where-Object { $_.RecipientType -eq "MailUser" -and $_.Name -notlike "Health*" -and $_.IsDirSynced -eq $true -and $_.UserPrincipalName -notlike "*onmicrosoft.com" } | select UserPrincipalName -ExpandProperty UserPrincipalName | sort UserPrincipalName
56 | Write-Verbose "List creation completed." -Verbose
57 |
58 | #Calculate how many users need to be in each batch to fit them all in 20 batches
59 | $batchMembersCount = [math]::floor($batchUsers.Count / 20)
60 |
61 | Write-Verbose "Creating temporary CSV files for batch import." -Verbose
62 |
63 | #Clear out old CSV files and create temp directory
64 | if (Test-Path $csvTempPath) { Remove-Item $csvTempPath -Recurse -Force}
65 | $createDir = New-Item -ItemType Directory -Path $csvTempPath
66 |
67 | #Create the batch csv files
68 | $upnCount = 0
69 | $fileCount = 1
70 | foreach($upn in $batchUsers){
71 |
72 | if($upnCount -eq 0) {
73 | #Create temporary CSV file on first iteration
74 | $csvTempFile = Join-Path $csvTempPath "\batch$($fileCount).csv"
75 | Add-Content -Path $csvTempFile -Value 'EmailAddress'
76 | }
77 |
78 | #add users UPN to the initial and subsequent iterations
79 | Add-Content -Path $csvTempFile -Value "$upn"
80 |
81 | $upnCount++
82 | if($batchMembersCount -eq $upnCount){
83 | #reset iteration as we have reached the calculated maximum users of this batch.
84 | $upnCount = 0
85 | #Increase filename for next batch iteration.
86 | $fileCount++
87 | }
88 |
89 | }
90 | Write-Verbose "CSV file creation completed." -Verbose
91 |
92 | #Create migration batches in Exchange Online
93 |
94 | Write-Verbose "Opening migration endpoint select window..." -Verbose
95 | $SourceEndpoint = Get-MigrationEndpoint | Out-GridView -Title "Select 1 migration endpoint for batches..." -PassThru | select Identity -ExpandProperty Identity
96 |
97 | Write-Verbose "Adding $fileCount batches to Exchange Online and starting initial sync." -Verbose
98 | $batchCount = 1
99 | Do {
100 | #Create migration batch for each CSV
101 | $csvTempFile = Join-Path $csvTempPath "\batch$($batchCount).csv"
102 | $niceNumber = ([string]$batchCount).PadLeft(2,'0')
103 | New-MigrationBatch -Name "AutoBatch$($niceNumber)" -SourceEndpoint "$SourceEndpoint" -CSVData ([System.IO.File]::ReadAllBytes("$csvTempFile")) -AutoStart -BadItemLimit $badItemLimit -TargetDeliveryDomain $TargetDeliveryDomain -NotificationEmails $notificationEmail
104 | $batchCount++
105 | } While ($batchCount -ne $fileCount)
106 |
107 | Write-Verbose "Completed batch creation in Exchange Online." -Verbose
108 |
109 | Disconnect-ExchangeOnline -Confirm:$false
110 |
111 | Write-Verbose "End script." -Verbose
112 | #endregion Execute
113 |
--------------------------------------------------------------------------------
/Hybrid Exchange/Invoke-GrantCloudMailboxAccessToOnPremMailbox.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .DESCRIPTION
3 | Simple PowerShell script to grant an Exchange Online Mailbox user FullAccess and Send As permission to an On-Prem Mailbox
4 |
5 | .NOTES
6 | Author: @michael_mardahl on Twitter
7 | Github: github.com/mardahl
8 | License: MIT
9 | #>
10 |
11 | #Exchange on-prem part (must be run on a server with Exchange management tools installed)
12 | $mailbox = Read-Host "Enter UPN of on-prem mailbox you need to grant access TO"
13 | $delegate = Read-Host "Enter UPN of the cloud mailbox that will access the on-prem mailbox"
14 | Add-PSSnapin Microsoft.Exchange.Management.PowerShell.SnapIn
15 | Add-MailboxPermission –Identity $mailbox –User $delegate –AccessRights FullAccess –AutoMapping $false
16 | Add-MailboxPermission –Identity $mailbox –User $delegate –AccessRights ExtendedRight -ExtendedRights "Send As" –AutoMapping $false
17 |
18 | #Exchange Online part (requires EXO Powershell V2 module)
19 | connect-exchangeonline
20 | Add-RecipientPermission $mailbox -AccessRights SendAs -Trustee $delegate
21 |
--------------------------------------------------------------------------------
/Hybrid Exchange/invoke-AddMissingExoTargetDomain.ps1:
--------------------------------------------------------------------------------
1 | #Script to be executed on local exchange servers management console.
2 | #Adds a target domain for mailboxes that did not get one during the run of the hybrid configuration wizard, due to them having e-mail address policy processing disabled.
3 |
4 |
5 | # Fetch all mailboxes
6 | $mailboxes = Get-Mailbox -ResultSize Unlimited
7 |
8 | # Define the target domain
9 | $targetDomain = "xxxxxxx.mail.onmicrosoft.com"
10 |
11 | #generate random suffix for target alias, so to avoid conflicts.
12 | $random = (Get-Date).Ticks % 36 -as [char]
13 | $random += (Get-Random -Minimum 10 -Maximum 36) -as [char]
14 | $random += (Get-Random -Minimum 10 -Maximum 36) -as [char]
15 | $random += (Get-Random -Minimum 10 -Maximum 36) -as [char]
16 |
17 | # Loop through each mailbox and check/add the proxyAddress
18 | foreach ($mailbox in $mailboxes) {
19 | $hasProxyAddress = $false
20 |
21 | # Check if the mailbox has a proxyAddress with the target domain
22 | foreach ($proxyAddress in $mailbox.EmailAddresses) {
23 | if ($proxyAddress.PrefixString -eq "smtp" -and $proxyAddress.AddressString -like "*@$targetDomain") {
24 | $hasProxyAddress = $true
25 | break
26 | }
27 | }
28 |
29 | # If missing, add the proxyAddress with the target domain
30 | if (-not $hasProxyAddress) {
31 | $alias = $mailbox.Alias
32 | $newProxyAddress = "smtp:$($alias)-$random@$targetDomain"
33 | $mailbox.EmailAddresses += $newProxyAddress
34 | Set-Mailbox -Identity $mailbox.Identity -EmailAddresses $mailbox.EmailAddresses
35 | Write-Host "Added proxyAddress $newProxyAddress to $($mailbox.DisplayName)"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Hybrid Exchange/invoke-FixSyncedAccountType.ps1:
--------------------------------------------------------------------------------
1 | # Quick and dirty FIX for azure ad connect synced accounts that dont show up as remote mailbox after you have fully migrated all mailboxes to exchange online and just have the local exchange server for management.
2 | #This will for example, change a user account type from mail user to remote recipient, so that you can use the targetAddress atributte. which is required if you are relaying emails through your on-prem exchange.
3 | #The script might need to be run twice, as it will try and make sure that the Exchange GUID is aligned between on-prem and EXO.
4 | #This script requires exchange online powershell management module and local active directory powershell modules.
5 | #NB: you might need to review the target address after applying, as teh normal tagert address should be alias@tenantdomain.mail.onmicrosoft.com
6 |
7 | #Author: Michael Mardahl (github.com/mardahl)
8 | #License: MIT
9 |
10 | connect-exchangeonline
11 |
12 | $UserList = get-mailbox userToFix@domain.com #(can also be a query for multiple users)
13 |
14 | foreach ($user in $UserList) {
15 | $upn, $tempusr = $null
16 | $upn = $user.UserPrincipalName
17 | $tempusr = get-aduser -filter "UserPrincipalName -eq '$upn'" -Properties * #| Select Name,MSExchMailboxGuid,Username,Identity
18 | write-Verbose $tempusr.Name -Verbose
19 | write-Verbose $upn -Verbose
20 | if($tempusr.MSExchMailboxGuid){
21 | $guid = [GUID]$tempusr.MSExchMailboxGuid
22 | if ( $guid -eq $user.ExchangeGuid ) {
23 |
24 | Write-Host "EXO GUID MATCH! $guid" -ForegroundColor Green
25 | Write-Host "------ existing proxyAddresses"
26 | $tempusr.ProxyAddresses
27 | Write-Host "------ added proxyAddresses"
28 | #pause
29 |
30 | #Add PrimarySMTP which is required
31 | $SMTP = "SMTP:" + $user.PrimarySmtpAddress
32 | if(-not ($tempusr.ProxyAddresses -contains "$SMTP")){
33 | Write-Host "Adding $SMTP" -ForegroundColor Yellow
34 | $tempusr | Set-ADUser -add @{ProxyAddresses="$SMTP"}
35 | }
36 |
37 | #Add legacyDN which is required
38 | $X500 = "X500:" + $user.legacyExchangeDN
39 | if(-not ($tempusr.ProxyAddresses -contains "$X500")){
40 | Write-Host "Adding $X500" -ForegroundColor Yellow
41 | $tempusr | Set-ADUser -add @{ProxyAddresses="$X500"}
42 | }
43 |
44 | #Set mailNickname
45 | $alias = $user.Alias
46 | if(-not ($tempusr.mailNickname)){
47 | Write-Host "Adding mailNickname $alias" -ForegroundColor Yellow
48 | pause
49 | $tempusr | Set-ADUser -replace @{mailNickname="$alias"}
50 | }
51 |
52 | #Set recipientDisplaytype
53 |
54 | if(-not ($tempusr.msExchRecipientDisplayType -contains "-2147483642")){
55 | Write-Host "Adding msExchRecipientDisplayType" -ForegroundColor Yellow
56 | pause
57 | $tempusr | Set-ADUser -replace @{msExchRecipientDisplayType="-2147483642"}
58 | }
59 |
60 | #Set msExchRecipientTypeDetails
61 |
62 | if(-not ($tempusr.msExchRecipientTypeDetails -contains "2147483648")){
63 | Write-Host "Adding msExchRecipientTypeDetails" -ForegroundColor Yellow
64 | pause
65 | $tempusr | Set-ADUser -replace @{msExchRecipientTypeDetails="2147483648"}
66 | }
67 |
68 | #Set recipientRemotetype
69 |
70 | if(-not ($tempusr.msExchRemoteRecipientType -contains "4")){
71 | Write-Host "Adding msExchRemoteRecipientType" -ForegroundColor Yellow
72 | pause
73 | $tempusr | Set-ADUser -replace @{msExchRemoteRecipientType="4"}
74 | }
75 |
76 | #Set targetAddress
77 | $target = "SMTP:" + $user.PrimarySMTPAddress
78 | if(-not ($tempusr.targetAddress)){
79 | Write-Host "Adding targetAddress $target" -ForegroundColor Yellow
80 | pause
81 | $tempusr | Set-ADUser -replace @{targetAddress="$target"}
82 | }
83 |
84 | } else {
85 | Write-Host "FAIL MATCH!" -ForegroundColor red
86 |
87 | Write-Host $guid
88 | Write-Host $user.ExchangeGuid
89 |
90 | $Params = @{msExchMailboxGUID = [GUID]$user.ExchangeGuid;}
91 |
92 | $tempusr | Set-ADUser -replace $Params
93 |
94 | }
95 | } else {
96 | Write-Host "no mailbox guid found for on-prem account - setting the one from exchange online. please run script again afterwards." -ForegroundColor Yellow
97 |
98 | $Params = @{msExchMailboxGUID = [GUID]$user.ExchangeGuid;}
99 | $tempusr | Set-ADUser -replace $Params
100 | #pause
101 | }
102 |
103 | }
104 |
--------------------------------------------------------------------------------
/Hybrid Exchange/invoke-RemoveDotLocalDomain.ps1:
--------------------------------------------------------------------------------
1 | #Script to remove illegal .local domain from Exchange on-prem accounts before hybrid migration can take place.
2 | #This script must be run in the local exchange servers management console. It will do a backup of the original data first, but this file can be quite large, so you can comment out that like if you like.
3 |
4 | #read all mailboxes (if you have more than a few hundred, this might take some time, and you might consider doing some more advanced scripting).
5 | $mailboxes = get-mailbox -Resultsize Unlimited
6 |
7 | #do backup
8 | $mailboxes | Export-Clixml .\Desktop\mailboxdata.xml
9 |
10 | foreach ($mailbox in $mailboxes) {
11 | write-host "Evaluating : $($mailbox.DisplayName)"
12 | $emailaddresses = $mailbox.emailaddresses;
13 | $badaddress = ""
14 |
15 | for ($i=0; $i -lt $emailaddresses.count; $i++) {
16 | #removing .local domain if found
17 | if ($emailaddresses[$i].smtpaddress -like "*.local") {
18 | $badaddress = $emailaddresses[$i];
19 | write-host "found $badaddress - removing" -foregroundcolor yellow
20 | $emailaddresses = $emailaddresses - $badaddress;
21 | $mailbox | set-mailbox -emailaddresses $emailaddresses;
22 | }
23 | if ($badaddress) {
24 | continue
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Hybrid Exchange/invoke-logPurge.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .SYNOPSIS
3 | Quick script to cleanup excess logging on an Exchange server purely used for Hybrid Exchange Management.
4 |
5 | .DESCRIPTION
6 | This script can be run on a daily scheduled task as SYSTEM, and will cleanup the log files that Exchange 2013+ creates during normal operations.
7 | If you care about debugging, you should not be so quick to purge all these logs.
8 |
9 | .OUTPUTS
10 | Creates an transcript of the last run in F:\scripts\lastcleanupoutput.txt - change this in the begining of the script code.
11 | .NOTES
12 | Credits: Original script found here: https://www.alitajran.com/cleanup-logs-exchange-2013-2016-2019/
13 | Author: Michael Mardahl
14 | Twitter: @michael_mardahl
15 | Blog: www.msendpointmgr.com
16 |
17 | .DISCLAIMER
18 | Use of this script is entirely up to your discression, the author takes no responsability for anything it does - test and evaluate on your own.
19 | #>
20 | #Requires -RunAsAdministrator
21 |
22 | # Set the age in days that log are retained. Anything older will be deleted.
23 | $days = 2
24 |
25 | # Log file paths
26 | $IISLogPath = "C:\inetpub\logs\LogFiles\"
27 | $ExchangeLogPath = "f:\Exchange Server\V15\Logging\"
28 | $ETLLogPath = "f:\Exchange Server\V15\Bin\Search\Ceres\Diagnostics\ETLTraces\"
29 | $ETLLogPath2 = "f:\Exchange Server\V15\Bin\Search\Ceres\Diagnostics\Logs\"
30 | $logOutputFile = "f:\scripts\lastcleanupoutput.txt"
31 |
32 | # Start transcript log output
33 | Start-Transcript $logOutputFile -Force
34 |
35 | #region functions
36 |
37 | Function CleanLogfiles($TargetFolder) {
38 | #Function that cleans out Exchange related log files in the TargetFolder and all subfolders
39 | Write-Host "INFO: Processing $TargetFolder"
40 |
41 | if (Test-Path $TargetFolder) {
42 | $Now = Get-Date
43 | $LastWrite = $Now.AddDays(-$days)
44 | $Files = Get-ChildItem $TargetFolder -Recurse | Where-Object { $_.Name -like "*.log" -or $_.Name -like "*.blg" -or $_.Name -like "*.etl" } | Where-Object { $_.lastWriteTime -le "$lastwrite" } | Select-Object FullName
45 | foreach ($File in $Files) {
46 | $FullFileName = $File.FullName
47 | Write-Host "INFO: Deleting file $FullFileName"
48 | Remove-Item $FullFileName -ErrorAction SilentlyContinue | out-null
49 | }
50 | }
51 | Else {
52 | Write-Host "ERROR: The folder $TargetFolder doesn't exist! Check the folder path!"
53 | }
54 | }
55 |
56 | #endregion functions
57 |
58 | #region execute
59 |
60 | CleanLogfiles($IISLogPath)
61 | CleanLogfiles($ExchangeLogPath)
62 | CleanLogfiles($ETLLogPath)
63 | CleanLogfiles($ETLLogPath2)
64 |
65 | Stop-Transcript
66 |
67 | #endregion execute
68 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Michael Mardahl
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Queries/invoke-GetMismatchedUPNAndEmail.ps1:
--------------------------------------------------------------------------------
1 | <# .DESCRIPTION
2 | This script searches Azure AD for users with mismatched UserPrincipalName and Mail,
3 | and outputs the results as a CSV file for further investigation.
4 |
5 | .NOTES
6 | - This script assumes that the Azure AD PowerShell module is installed and you are connected with enough permissions.
7 |
8 | Author: Michael Mardahl (github.com/mardahl)
9 | Licensing: MIT
10 | #>
11 |
12 | # Create an empty array to store the user data
13 | $UserData = @()
14 |
15 | # Populate all users in a sorted array
16 | $Users = Get-AzureADUser -All $true
17 | $Users = $Users | Sort-Object DisplayName | Get-Unique -AsString
18 |
19 | # Search for users with mismatched UserPrincipalName and Mail
20 | foreach ($User in $Users) {
21 | $User.DisplayName
22 | if ($User.UserPrincipalName -ne $User.Mail) {
23 | # Store the user's relevant data in a hash table
24 | $UserInfo = @{
25 | 'DisplayName' = $User.DisplayName
26 | 'UserPrincipalName' = $User.UserPrincipalName
27 | 'Mail' = $User.Mail
28 | 'Company' = $User.CompanyName
29 | 'DirSyncEnabled' = $User.DirSyncEnabled
30 | }
31 | }
32 | # Add the user data to the array
33 | $UserData += New-Object PSObject -Property $UserInfo
34 | }
35 |
36 | # Export the user data to a CSV file
37 | $UserData | Export-Csv -Path .\MismatchedUserDataXXX.csv -NoTypeInformation -Force
38 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Exchange Online Scripts
2 | Powershell scripts meant to assist administrators of Exchange Online
3 |
4 | - Administration of Hybrid Exchange
5 | - Email authentication security (SPF, DKIM, DMARC)
6 |
7 | If you have usefull scripts to share please add them to this repo by using a pull request :)
8 |
--------------------------------------------------------------------------------