├── .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 | --------------------------------------------------------------------------------