<< A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

–A–

address lists, list

Get-AddressList | ogv

If you get not recognized as the name of a cmdlet, make sure you are a member of a group that has the right permissions

aliases, route incoming emails among various aliases using rule

assume you have an ID with two aliases

then

New-InboxRule -Name "BatMan@wayne.com - Move to BatMan direcctory" -Mailbox payables -HeaderContainsWords "BatMan@wayne.com" -MoveToFolder ":\Inbox\BatMan" -StopProcessingRules$false

alternateEmailAddresses, users that have at least one

Get-MsolUser -all | where {($_.AlternateEmailAddresses.count -gt 0)} | ft UserPrincipalName,AlternateEmailAddresses

arrays embedded in results, deal with - see audit log, find rule creations for an example using JSON

audit deleted emails

Verify that you have auditing turned on for the two main roles. Usually it's “owner” (for individual email boxes) and “delegate” for shared mailboxes.

$AccountWithTooManyEmailsDeleted = Get-Mailbox FastAndLoose@DeletingTheEmails.com
$AccountWithTooManyEmailsDeleted.AuditOwner
$AccountWithTooManyEmailsDeleted.AuditDelegate

If values for these turn out to be false, you first need to enable auditing for mailboxes before any of this will work. But assuming these come up to be true, grab records for that mailbox for a date range you're interested in. Unless you've changed defaults, you can only go back 90 days.

$auditAccountWithTooManyEmailsDeleted = Search-UnifiedAuditLog -StartDate "9/25/2020" -EndDate "9/26/2020" -UserIds FastAndLoose@DeletingTheEmails.com
$auditAccountWithTooManyEmailsDeleted.Count

Make sure you actually have some display records pertaining to Exchange (specify “ExchangeItemGroup” to filter out viewing SharePoint files and other extraneous stuff). Sometimes, you won't get any returned even if you know you have some.

($auditAccountWithTooManyEmailsDeleted | ? {$_.RecordType -eq "ExchangeItemGroup"}).count

If you find you have some, display records them.

$auditAccountWithTooManyEmailsDeleted | ? {$_.RecordType -eq "ExchangeItemGroup"} | ogv

If you do find records using Search-UnifiedAuditLog

$auditOperationsUnified = Search-UnifiedAuditLog -StartDate "9/25/2020" -EndDate "9/26/2020" -UserIds FastAndLoose@DeletingTheEmails.com
$auditOperationsUnified.count

Unfortunately, most of the stuff we care about is in the hard-to-read AuditData field which is actually a hash of many sub-fields.

$auditOperationsUnified | ogv

This is useful to expand the AuditData hash:

$ConvertOperationsAudit = $auditOperationsUnified | Select-Object -ExpandProperty AuditData | ConvertFrom-Json

$ConvertOperationsAudit | select CreationTime, Operation, ResultStatus, ClientIP, ActorIPAddress, LogonError | ogv

If, instead, you do not find records using Search-UnifiedAuditLog

What to do when you know you have some deleted records but the Search-UnifiedAuditLog command above doesn't find any? Resort to Search-MailboxAuditLog instead of Search-UnifiedAuditLog:

$auditUsingMailAudit = Search-MailboxAuditLog -Identity FastAndLoose@DeletingTheEmails.com -ShowDetails -StartDate "9/25/2020" -EndDate "9/26/2020"
$auditUsingMailAudit | Where-Object {$_.Operation -eq "MoveToDeletedItems" -or $_.Operation -eq "HardDelete" -or $_.Operation -eq "Move" -or $_.Operation -eq "MoveToDeletedItems" -or $_.Operation -eq"SoftDelete"} | select MailboxOwnerUPN, LogonUserDisplayName, Operation, DestFolderPathName, FolderPathName, ClientIP, SourceItemSubjectsList, SourceItemPathsList, MailboxResolvedOwnerName, LastAccessed, SourceItemAttachmentsList | ogv

Note that the first two fields - MailboxOwnerUPN and LogonUserDisplayName are most useful to find out who is deleting emails. But they're only useful if the email account in question is a shared, non-licensed mailbox. If instead, you have one licensed mailbox and let a whole bunch of people use that same ID, this isn't quite as useful; everyone's logging on using the same value for these two fields!

audit log, find rule creations

This example finds all new email rule creations on a certain day

$auditEventsForUser = Search-UnifiedAuditLog -StartDate '2020-12-14' -EndDate '2020-12-15' -UserIds [someUser] -RecordType ExchangeAdmin -Operations New-InboxRule
$ConvertedOutput = $auditEventsForUser | Select-Object -ExpandProperty AuditData | ConvertFrom-Json

$ConvertedOutput | Select-Object CreationTime,Operation,Workload,ClientIP,Parameters | ft -Wrap -a

audit log record, find details

If you use the GUI to find an audit record for a user, you won’t get too much detail.  You will get an inscrutable “Item” which might look something like:

<FEC724FE-21BB-4EA1-AAF1-78EC08023831@yourDomain.com>

But this information isn’t too useful.  So, use PowerShell instead.  Start by looking at a target user for a given date range:

$audit = Search-UnifiedAuditLog -StartDate "7/1/2020 8 AM" -EndDate "7/1/2020 1 PM" -UserIds someUser@yourDomain.com

Assume this returns just one record.  This $audit variable has a lot of fields. The field we’re interested in is “AuditData”, which isn’t too readable because it’s a hash.  So, expand it:

$ConvertedOutput = $audit | Select-Object -ExpandProperty AuditData | ConvertFrom-Json

In this variable, we first encounter a field that resembles the “Item” we found through the audit using the web tools:

$ConvertedOutput | fl ObjectId

yields

ObjectId : <FEC724FE-21BB-4EA1-AAF1-78EC08023831@yourDomain.com>

Which we recognize.  But this still isn’t too interesting. What we’re probably interested in is the “ExchangeDetails” of this same variable. It is, again, a somewhat difficult-to-read hash called “ExchangeDetails”.  So again, expand it:

$ConvertedOutput | Select-Object -ExpandProperty ExchangeDetails

This yields

Directionality    : Originating
From              : sender@yourDomain.com
InternetMessageId : <FEC724FE-21BB-4EA1-AAF1-78EC08023831@yourDomain.com>
MessageTime       : 2020-07-01T10:09:49
NetworkMessageId  : 0355bf10-1001-477e-4f40-08d81da6e39c
Recipients        : {recipient1@hotmail.com, recipient2@hotmail.fr}
Subject           : Fwd: Rappel de paiement_BurbleFrop

which is a lot closer to what we're looking for.

audit log AuditLogRecordTypes

audit mailboxes, enable auditing for mailboxes

I think there's a way to globally enable auditing for all mailboxes. Failing that, enable one mailbox at a time. And if you add a new user, you have to make sure to do it for him as well. Here's at least a command to enable auditing on all mailboxes:

Get-Mailbox -ResultSize Unlimited -Filter {RecipientTypeDetails -eq "UserMailbox" } | Set-Mailbox -AuditEnabled $true

automap a shared mailbox, remove for several users - see also

You can't just remove automapping.  Instead, you must remove full access rights and then add full access again but this time with the automapping set to $false instead of default $true

specify the shared mailbox:

$sharedMailbox = "wantToShare@yourDomain.com"

verify the existing permissions on this shared mailbox.  I normally use this in order to get rid of inherited permissions:

Get-Mailbox $sharedMailbox | Get-MailboxPermission | ? {$_.user.tostring() -ne "NT AUTHORITY\SELF" -and $_.IsInherited -eq $false} | Select-Object user, AccessRights

But if you run the simpler version, you might see a SID tucked in there somewhere:

Get-Mailbox $sharedMailbox | Get-MailboxPermission

In at least one instance, I did get a SID and it was inherited.   This may cause a problem.

populate array with delegates who you're going to remove and then add back permissions (but without automapping):

$delegates = @("user1@yourDomain.com", "user2@yourDomain.com", "user3@yourDomain.com")
$delegates | %{Remove-MailboxPermission -Identity $_ -User $sharedMailbox -AccessRights FullAccess -Confirm:$false}

I sometimes get the following message:

WARNING: Can't remove the access control entry on the object "CN=your user,OU=yourTenant.onmicrosoft.com,OU=Microsoft Exchange Hosted Organizations,DC=NAMPR10A004,DC=PROD,DC=OUTLOOK,DC=COM" for account "NAMPR10A004\share53598-1921233685" because the ACE doesn't exist on the object.

which doesn't make sense.  But I proceed anyway.

$delegates | %{Add-MailboxPermission -Identity $_ -User $sharedMailbox  -AccessRights FullAccess -AutoMapping:$false}

verify:

$delegates | %{Get-Mailbox | Get-MailboxPermission -user $_}

I have yet to find any PowerShell command that actually tells the status of automapping. The command above merely verifies that the individuals still are delegates to the target shared mailbox with full access.

Azure security group, add members - see security group, add members

–B–

Behalf - as in SendOnBehalfTo - see

delegated mailboxes that a user has SendOnBehalfTo, SendOnBehalfTo, add this permission for a user on a shared mailbox

bypass spam settings to whitelist outside domain - see whitelisted domains for all transport rules

–C–

calendar activity, examine log

Get-CalendarDiagnosticLog -Identity someUser -StartDate "10/3/2018 6:00:00 AM" -EndDate "10/3/2018 5:00:00 PM" | ogv

I use this to test whether adding calendar items is reflected in another user who may have been inadvertently added as a delegate at the Outlook (not Exchange) level.

calendar, allow someone else to view - see also calendar, allow several other people to edit, public folders, add permission

Add-MailboxFolderPermission whoseCalYouWantRead@iwannaknow.com:\Calendar -User whoNeedsAccess@iwannaknow.com -AccessRights FolderVisible, ReadItems

If the person already has one permission - say AvailabilityOnly, you might get an error.

An existing permission entry was found for user: Who NeedsAccess

So, use the Set-MailboxFolderPermission instead of the Add-MailboxFolderPermission:

Set-MailboxFolderPermission -Identity whoseCalYouWantRead@iwannaknow.com:\Calendar -User whoNeedsAccess@iwannaknow.com -AccessRights FolderVisible, ReadItems

or for a bunch

$delegates = @("henchman@DestroyAustinPowers.com","shark@DestroyAustinPowers.com","laserTechnician@DestroyAustinPowers.com")
$sharedCalendars = @("Dr.Evil@DestroyAustinPowers.com","NumberTwo@DestroyAustinPowers.com")
$i=0
foreach ($delegate in $delegates) {
    $i++
    $j=0
    foreach ($shared in $sharedCalendars) {
        $j++
        "$i of $($delegates.Count) delegates; $j of $($sharedCalendars.Count) calendar to share: add $delegate as delegate to $shared"
        #Add-MailboxFolderPermission -Identity "$($shared):\Calendar" -User $delegate -AccessRights Editor
    }
}

Curiously, after I run the command above, when I look to see what permission

Get-MailboxFolderPermission -Identity whoseCalWantToKnowPerms@iwannaknow.com:\Calendar

It shows the user as now having Reviewer permissions. I would have expected an array of the two separate permissions we actually tried to add: FolderVisible,ReadItems

I've also occasionally found that, for some people, merely giving the target person FolderVisible and ReadItems AccessRights doesn't seem to suffice - even if all the person needs is only to read. Instead, not only must I give that person Editor AccessRights, I must also set SharingPermissionFlags to Delegate

Add-MailboxFolderPermission -Identity whoseCalWantToKnowPerms@iwannaknow.com:\Calendar -User whoNeedsAccess@iwannaknow.com -AccessRights Editor -SharingPermissionFlags Delegate

(and if you need to set SharingPermissionFlag to Delegate, you must also set AccessRights to Editor)

To grant one user access to most of the folks in his own OU (optionally filtering out anyone whose name starts with someString):

Get-ADUser -SearchBase "OU=yourOU,DC=yourDomain,DC=com" -Filter * -SearchScope Subtree | ? {($_.Name -notlike "someString*") -and ($null -ne $_.UserPrincipalName) -and ( $_.UserPrincipalName -notlike "userWhoNeedsAccess*")} | select UserPrincipalName | % {Add-MailboxFolderPermission "$($_.UserPrincipalName):\Calendar" -User userWhoNeedsAccess@yourDomain.com -AccessRights FolderVisible,ReadItems}

This gives errors for folks who don't have email boxes or whom already grant permission to this user but otherwise works OK.

calendar, allow several other people to edit - see also calendar, allow someone else to view, public folders, add permission

If you have, say, 3 task masters who all want to pack some poor soul's calendar chock full of things to do:

$taskMasters = ("taskMaster1@ScroogeAndMarley.com","taskMaster2@ScroogeAndMarley.com","taskMaster3@ScroogeAndMarley.com")

$taskMasters | % {Add-MailboxFolderPermission -Identity atBeckAndCallOfManagers@ScroogeAndMarley.com:\Calendar -User $_ -AccessRights Editor}

If these people already have one permission - say AvailabilityOnly, you might get an error.

An existing permission entry was found for user: Task Master 1

So, use the Set-MailboxFolderPermission instead of the Add-MailboxFolderPermission:

$taskMasters | % {Set-MailboxFolderPermission -Identity atBeckAndCallOfManagers@ScroogeAndMarley.com:\Calendar -User $_ -AccessRights Editor}

calendar, display all calendars for a user

Get-MailboxFolderStatistics "someUser" | ? {($_.foldertype -eq "Calendar") -or ($_.folderpath -like "/Calendar*")} | ft Name, Identity, folderpath, foldertype

calendar items, remove

This comes in handy if someone leaves, you keep his mailbox around for a while, but you want to remove recurring meetings he set up. Our first attempt

Remove-CalendarEvents -Identity dontLet@TheDoorHitYouOnYourFannyOnYourWayOut.com -CancelOrganizedMeetings -QueryWindowInDays 360

Failed with

Error on proxy command 'Remove-CalendarEvents -Identity:'dontLet@TheDoorHitYouOnYourFannyOnYourWayOut.com' -CancelOrganizedMeetings:$True -QueryWindowInDays:'360' -Confirm:$False' to server CH0PR10MB5148.namprd10.prod.outlook.com: Server version 15.20.5186.0000, Proxy method PSWS: Cmdlet error with following error message:

(There was more to that error message above.) But our 2nd attempt

Remove-CalendarEvents -Identity dontLet@TheDoorHitYouOnYourFannyOnYourWayOut.com -CancelOrganizedMeetings -QueryStartDate 3-1-2020 -QueryWindowInDays 750

worked. Think what was needed to get around error was to specify a start date 2 years ago because suspect some of his meetings were long running recurring meetings. Wanted to go back and scrape them all out.

Then people were inundated with cancellation requests and wondered if they were legit. This only worked to kill meetings created last year and didn't kill all meetings. All that remained were meetings that others set for him; meetings he created were canceled. The purpose is to get the recipient to click on the "remove from calendar" button to get it off their calendar. So, apparently this command won't automatically remove these meetings from others' calendars.

calendar notifications - see also calendar processing

Will a user get notified in his email when a calendar invite comes along wanting to modify his calendar?

Get-CalendarNotification -identity someUser | select Identity, CalendarUpdateNotification | ft

change/set:

Set-CalendarNotification -Identity someUser@yourDomain.com -CalendarUpdateNotification $true

check status for everyone in an OU:

Get-ADUser -SearchBase "OU=yourOU,DC=yourDomain,DC=com" -Filter * -SearchScope Subtree -Properties name | % {Get-CalendarNotification -identity $_.userPrincipalName | select Identity, CalendarUpdateNotification} | ft

There are also calendar agenda notification (DailyAgendaNotification) and reminder notification (MeetingReminderNotification) settings

calendar permissions, remove

Remove-MailboxFolderPermission -Identity whoseCalendar@yourDomain.com:\Calendar -User userWhoWantsPerms -confirm:$false

calendar permissions, what permissions does one person have for each person in a distribution group?

Useful to verify permissions for one person who must coordinate calendar events for a group of several people, all of whom belong to one distribution group

$distributionGroup = Get-DistributionGroupMember -Identity "Executive Team"
$i = 0
foreach ($member in $distributionGroup){
    $i++
    $folderPerms = Get-MailboxFolderPermission -Identity "$($member.Name):\Calendar"
    foreach ($perm in $folderPerms) {
        if ($perm.user.ToString() -eq "Shirley Temple") {
            "$i of $($distributionGroup.count): $($member.Name) - $($perm.AccessRights)"
        }
    }
}

Note that if the person doesn't have permission on a particular member of the distribution group, that won't be obvious other than a sequence number skipped in the output as this is written.   That's why sequence numbers are include to give a clue if this happens.

The code above was written to give compact output.  But if you want to show all the distribution group members (even if the person in question has no access), then move the $($member.Name) to its own line right after the $i++ statement

calendar permissions, which calendars can one person read?

Get-Mailbox -ResultSize unlimited | % {Get-Mailboxfolderpermission (($_.PrimarySmtpAddress)+":\calendar") -User busyBody -ErrorAction SilentlyContinue} | Select-Object Identity, User, Accessrights

but what if you want to search several domains' worth, some of whose members use a different language, in which case we might need to look for MailboxFolderPermissions on folders other than strictly "Calendar"? Let's reconnoiter first:

Get-Mailbox -Filter {WindowsEmailAddress -like "*@belgium.be" -or WindowsEmailAddress -like "*@europe.eu"} | % {Get-MailboxFolderStatistics -Identity $_.PrimarySmtpAddress | ? {($_.foldertype -eq "Calendar")} | select Identity, Name, FolderPath}

It's quite possible this will reveal several possible names for a user's primary calendar such as "Calendar", "Calendrier", "Agenda"

calendar permissions, who can see one user's calendar and what rights does each user have?

Get-MailboxFolderPermission -Identity whoseCalWantToKnowPerms@yourDomain.com:\Calendar

Note: if this comes back with

The operation couldn't be performed because 'whoseCalWantToKnowPerms@yourDomain.com:\Calendar' couldn't be found.

the calendar folder might be called something else in another country. This command might give a clue what they're called:

Get-MailboxFolderStatistics "whoseCalWantToKnowPerms@yourDomain.com" | ? {($_.foldertype -eq "Calendar") -or ($_.folderpath -like "/Calendar*")} | ft Name, Identity, folderpath, foldertype

Once you know what the calendar folder is supposed to be called re-run your original query

calendar processing - see also calendar notifications

Will a user get notified in his email when a calendar invite comes along wanting to modify his calendar?

Get-CalendarProcessing -identity someUser | select Identity, AutomateProcessing | ft

change/set:

Set-CalendarProcessing -Identity someUser@yourDomain.com -AutomateProcessing None

Valid values are:

  • None: Calendar processing is disabled on the mailbox. Both the resource booking attendant and the Calendar Attendant are disabled on the mailbox.
  • AutoUpdate: Only the Calendar Attendant processes meeting requests and responses. Meeting requests are tentative in the calendar until they're approved by a delegate. Meeting organizers receive only decisions from delegates.
  • AutoAccept: Both the Calendar Attendant and resource booking attendant are enabled on the mailbox. This means that the Calendar Attendant updates the calendar, and then the resource booking assistant accepts the meeting based upon the policies. Eligible meeting organizers receive the decision directly without human intervention (free = accept; busy = decline).

check status for everyone in an OU:

Get-ADUser -SearchBase "OU=yourOU,DC=yourDomain,DC=com" -Filter * -SearchScope Subtree -Properties name | % {Get-CalendarProcessing -identity $_.userPrincipalName | select Identity, AutomateProcessing} | ft

There are also many other settings/parameters

calandars, list public calendars - see public folders, list

conference room, create - see room, create

conference rooms, filter out from list of mailboxes- see mailbox types, filter out types

Connect-AzureAD error

Connect-ExchangeOnline

fails with:

Could not load type 'System.Security.Cryptography.SHA256Cng' from assembly 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.

This post suggested

Import-Module AzureAD -UseWindowsPowerShell

But that failed:

port-Module: Failure from remote command: Import-Module -Name 'AzureAD': The specified module 'AzureAD' was not loaded because no valid module file was found in any module directory.

Even though running it earlier without the -UseWindowsPowerShell parameter worked just fine.

This post suggested finding

All Users Configuration

$PSHOME

Current User Configuration

Split-Path $PROFILE.CurrentUserCurrentHost

and configuring the powershell.config.json file. But I couldn't get this to work.

'Connect-ExchangeOnline' is not recognized

Install-Module ExchangeOnlineManagement

may result in error:

Connect-ExchangeOnline error (MFA)

If you encounter, “Error AADSTS50076: Due to a configuration change made by your administrator, or because you moved to a new location, you must use multi-factor authentication to access” error when running:

Connect-ExchangeOnline -Credential $cred

check any conditional access policies that enforce MFA. You may need to exclude this user.

Connect-MSOLservice is not recognized as a name of a cmdlet, function, script file, or executable program.

Install-Module AzureAD

If you are running this on PowerShell 7+/Core, you need to import the module in compatibility mode:

Import-Module AzureAD -UseWindowsPowerShell

But this gave:

Connect-MsolService: Could not load type 'System.Security.Cryptography.SHA256Cng' from assembly 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.

Running the above without -UseWindowsPowerShell didn't fail with message above but didn't fix later problem below, either.

Install-ModuleAzureADPreview
Install-ModuleMSOnline
Import-ModuleMSOnline

Running below on PS7 didn't fail, but didn't fix, either. Got error:

Connect-MsolService: Could not load type 'System.Security.Cryptography.SHA256Cng' from assembly 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.

From here, "If you want to connect to Microsoft 365 services from Windows PowerShell V7 or later, it is recommended that please use Azure Active Directory PowerShell for Graph module or Azure PowerShell. For more details, please refer to Connect with the Azure Active Directory PowerShell for Graph module" which starts out with installing & importing AzureAD

Install-Module -Name AzureAD
Import-Module AzureAD

After running the two commands above, Get-Mailbox worked. So, looks as if the answer for PowerShell version 7 is: don't bother ven trying to get this to work 'cause it won't. Oddly, merely installing and importing AzureAD didn't enable Connect-AzureAD. That is, running Connect-AzureAD after installing and importing AzureAD produced errors. See Connect-AzureAD error.

contact, add - New-MailContact.  Curiously, even though there's a Get-MsolContact command, there does not appear to be any corresponding New-MsolContact command

from CSV

$dir = [environment]::getfolderpath("mydocuments")
$docName = "$($dir)/MigrationnewContactsNeedContact.csv"
$newContacts = Import-Csv $docName
$i = 0
foreach ($SharedMailbox in $newContacts) {
    $i++
    $displayName = "$($SharedMailbox.'Display Name') (new tenant)"
    $firstName = $displayName.Split(" ")[0]
    $lastName = "$($displayName.Split(" ")[1]) $($displayName.Split(" ")[2])".Trim() # trim because sometimes display name only has one name
    $email = $SharedMailbox.'Mail Address'
    Write-Host "$i of $($newContacts.Count): $displayName - $email ($firstName $lastName)" -ForegroundColor Green
    New-MailContact -Name $displayName -DisplayName $displayName -ExternalEmailAddress $email -FirstName $firstName -LastName $lastName
}

replicate all contacts in an OU in local AD to cloud (useful if not syncing)

$contacts = Get-ADObject -filter {objectclass -eq "contact"} -SearchBase "OU=yourOU,DC=yourDomain,DC=com" -Properties name, givenName, middleName, sn, mail | `
    Sort-Object sn, givenName | select name, givenName, middleName, sn, mail
$contacts | % {New-MailContact -Name $_.Name -DisplayName $_.Name -ExternalEmailAddress $_.mail -FirstName $_.FirstName -LastName $_.LastName}

replicate all users in an OU in local AD to cloud

$users = Get-ADUser -filter * -SearchBase "OU=someOU,DC=yourDomain,DC=com" `

    -Properties company, department, displayName, givenName, mail, middleName, name, physicalDeliveryOfficeName, sn, telephoneNumber, targetAddress

$users | % {New-MailContact -DisplayName $_.DisplayName -Name $_.Name -FirstName $_.givenName `
    -ExternalEmailAddress $_.targetAddress.split(":")[1] -LastName $_.LastName}

There are many attributes you simply can't include when adding a contact. These include such important attributes as: company, department, office, phone, title. But we can add them after the fact using the Set-Contact command.

$users | % {Set-Contact $_.DisplayName -company $_.company -department $_.department -Office $_.physicalDeliveryOfficeName -phone $_.telephoneNumber -title $_.title}

contact, add to distribution group - see distribution group, add members

Add-DistributionGroupMember -Identity "Marx Brothers" -Member "Zeppo Marx"

contacts, delete and replace with guest users

This assumes you already have guest IDs in place (which do not yet act as contacts) alongside of contacts which you want to replace. You can’t add users to your tenant if you already have contacts there with the same email because their proxy addresses will conflict. So, we must delete all interfering contacts. To delete one interfering contact:

Get-MailContact -filter "DisplayName -eq 'Jack D Ripper'"

or maybe

Get-MailContact -filter "DisplayName -like 'Jack*'"

Sometimes all you know is that the email address you're trying to put in as the email address for a replacement guest ID is already being taken up by a contact.

Get-MsolContact -All | select DisplayName, EmailAddress, ObjectID | where {$_.EmailAddress -match "roger@ScarletLetter.onmicrosoft.com"} | fl

Note that, for the above to work properly, you need to include that "select" statement.

Sometimes if you try to fill in the WindowsEmailAddress of a guest ID, you'll get a cryptic:

The proxy address "SMTP:roger@ScarletLetter.onmicrosoft.com" is already being used by the proxy addresses or LegacyExchangeDN of "Contact_fdcf1eceb4". Please choose another proxy address.

for more info - including the EmailAddress that you'll need to remove below with the Remove-MsolContact command:

Get-MailContact -filter "Name -eq 'Contact_fdcf1eceb4'" | select DisplayName, Identity, Id, WindowsEmailAddress
Get-MsolContact -All | ? {$_.DisplayName -eq "Roger Chillingsworth"} | Remove-MsolContact -Force

or for a bunch of contacts all in the same domain:

Get-MsolContact -All| where {$_.EmailAddress -like "*ScarletLetter.com"} | Remove-MsolContact -Force

In this case I want a little more finesse:

Get-MsolContact -All | where {($_.CompanyName -eq "Some Group" ) -and ($_.EmailAddress -like "*ScarletLetter.com")} | Remove-MsolContact -Force

Because there were some contacts I needed to keep because they had no corresponding UK users / US guest users.  And they usually had outside email domains like Gmail or HotMail or the like which means those IDs would be excluded from being deleted by the statement above.

A couple of them you might have to mop up individually:

Get-MsolContact -All | where {($_.EmailAddress -eq "HesterPrynne@ScarletLetter.com")} | Remove-MsolContact -Force

Get-MsolContact -All | where {($_.EmailAddress -eq "Nathaniel.Hawthorne.@gmail.com")} | Remove-MsolContact -Force

Because there was no easily-identifiable property (such as email domain) that was consistent across all the guest IDs I wanted to modify that I could grab onto as part of a PowerShell to delete them all at once.  Keep in mind that I had to retain some contacts who never got IDs in the foreign tenant.

For guest IDs to show up in the GAL and actually function as a viable email, we must populate two heretofore null attributes:

  • HiddenFromAddressListsEnabled (to show up)
  • WindowsEmailAddress (to actually function, see Windows email address - add/set. Or just look at the next command.)

We do this through PowerShell commands.  As stated above, you must delete all conflicting contacts before attempting to populate the WindowsEmailAddress property.

Here's how you can set one user’s HiddenFromAddressListsEnabled and WindowsEmailAddress attributes:

Get-User -ResultSize unlimited | ? {$_.Name -eq "someUser_someDomain.com#EXT#"} | Set-MailUser -WindowsEmailAddress "someUser@someDomain.com " -HiddenFromAddressListsEnabled $false

But since we’re dealing with a lot of users, we don’t want to do these just one at a time.  So, this will update everyone’s WindowsEmailAddress.

Get-User -ResultSize unlimited -RecipientTypeDetails GuestMailUser | ? {$_.Department -eq 'Department of Redundancy Department' }| Set-MailUser -HiddenFromAddressListsEnabled $false

This will populate the WindowsEmailAddress and HiddenFromAddressListsEnabled:

$guests = Get-AzureADUser -Filter "userType eq 'Guest' and (Department eq 'Department of Redundancy Department')"
$guests.Count
$guests | % {Set-MailUser -Identity $_.UserPrincipalName -WindowsEmailAddress $_.OtherMails[0] -HiddenFromAddressListsEnabled $false}

or if Get-AzureADUser on some attribute such as userType and Department such as in the example above is not a convenient way to select, then perhaps Get-MailUser selecting some substring of UserPrincipalName might work better:

$SpidersFromMarsMailUsers = Get-MailUser -ResultSize unlimited | Where {($_.UserPrincipalName -like '*SpidersFromMars.com#EXT#@yourTenant.onmicrosoft.com')}
$SpidersFromMarsMailUsers | select UserPrincipalName | % {Set-MailUser -Identity "$($_.USerPrincipalName)" -WindowsEmailAddress "$($_.USerPrincipalName.Split("#")[0].Split("_")[0])@SpidersFromMars.onmicrosoft.com" -HiddenFromAddressListsEnabled $false}

Not exactly sure how the “OtherMails” got populated in the first place.  Automatically from when we invited the users & they accepted, I suppose.  It was the only reliable stash of the guest ID’s email I could find.  Do not try to populate it with UserPrincipalName.  That might work with normal users.  But not guest users.

If you don't want to rely on the OtherMails field:

$allUsers = Get-User -ResultSize unlimited
$yourDomainUsers = $allUsers | ? {$_.Name -like "*yourDomain.com*"}
$yourDomainUsersNoWindowsEmailAddress = $yourDomainUsers | Select-Object DisplayName, name, UserPrincipalName, HiddenFromAddressListsEnabled, WindowsEmailAddress | ? {$_.WindowsEmailAddress -eq \""}
$yourDomainUsersNoWindowsEmailAddress | select DisplayName, @{n="newWindowsEmailAddress";e={"$($_.Name.split("_")[0])@yourTenant.onmicrosoft.com"}}
foreach ($user in $yourDomainUsersNoWindowsEmailAddress) {
    $WindowsEmailAddress = "$($user.Name.split("_")[0])@yourTenant.onmicrosoft.com"
    $UserPrincipalName = $user.UserPrincipalName
    "UPN $UserPrincipalName will get new WindowsEmailAddress $WindowsEmailAddress"
    Set-MailUser -Identity $user.UserPrincipalName -WindowsEmailAddress $WindowsEmailAddress -HiddenFromAddressListsEnabled $false
}

To verify:

$guests | % {Get-MailUser -Identity $_.UserPrincipalName | select DisplayName, WindowsEmailAddress , HiddenFromAddressListsEnabled}

contact exists?

$EmailAddress = "phantom@OfTheOpera.org"
if (Get-MailContact -Identity $EmailAddress -ErrorAction SilentlyContinue) {"there IS a contact for $EmailAddress"} else {"there is NO contact for $EmailAddress"}

contact, find

If you only know an email of a mail contact which has an email address (as opposed to an Msol contact):

Get-MsolContact -ObjectId (Get-MailContact -Identity Smedley.Butler@CoupAttempt.com).ExternalDirectoryObjectId

Try one of these commands:

Get-MsolContact -SearchString "Smedley"
Get-MsolContact -SearchString "Smedley.Butler"
Get-MailContact -ANR "Smedley"

If you get the object ID

Get-MsolContact -SearchString "Smedley" | ft DisplayName, objectID

Below will give you the alias, which you can't find using fl directly

Get-MsolContact -ObjectId adc41dc7-4130-4215-adfb-2403bc9f844e

and from the alias you get above, you can use that for the -Identity

Get-MailContact -Identity Smedley.Butler

contact, find and remove a contact with the same name as a user - see contact, remove

contact info (with proxyAddress),

Get-MailContact -Identity someUser@someDomain.net | select DisplayName,alias,externalEmailAddress,emailAddresses

contacts, bad according to DirSync - find and remove

run this:

Get-MsolContact -All | where {($_.ValidationStatus -eq "Error")} | sort DisplayName | select DisplayName, EmailAddress, objectID

in order to get known problems.  To remove same:

Get-MsolContact -All | where {($_.ValidationStatus -eq "Error")} | Remove-MsolContact -Force

This doesn't always get everything.  So, try this to find more:

Get-MsolDirSyncProvisioningError -ErrorCategory PropertyConflict | where {$_.ObjectType -EQ "contact"} | ft

And then delete these:

Get-MsolDirSyncProvisioningError -ErrorCategory PropertyConflict | where {$_.ObjectType -EQ "contact"} | Remove-MsolContact -Force

contacts, display "proxyAddresses" and "targetAddress"

contacts on the cloud don't really have the same "proxyAddresses" or "targetAddress" as local AD contacts. Instead, they have "externalEmailAddress" and "emailAddresses" analogs:

Get-MailContact | select DisplayName,alias, externalEmailAddress, emailAddresses

contacts, list distribution groups for each - see distribution group, list all contacts along with which distribution group(s) they belong

contacts, proxyAddresses add

this example takes whatever the main address is and adds another email with a new suffix:

$contacts = Get-MailContact | Select-Object Identity, PrimarySmtpAddress, emailAddresses
$contacts | % {Set-MailContact -Identity $_.Identity -EmailAddresses @{Add = "smtp:" + $_.PrimarySmtpAddress.split("@")[0] + "@yourDomain.com"}}

contact, remove

Azure AD (cloud)

Get-MsolContact -SearchString "Some User" | Remove-MsolContact -Force

Local AD: find

$DepartingUserIdentity = "someUser";

Get-ADObject -LDAPFilter "objectClass=Contact" -Properties Name,mail,DistinguishedName  | Where-Object{$_.mail -like "$($DepartingUserIdentity)*"} | ft Name, mail, DistinguishedName

Remove

$EmployeeDetails = Get-ADUser $DepartingUserIdentity -properties *

Get-ADObject -Filter {(cn -eq $EmployeeDetails.Name) -and (objectClass -eq "Contact")} | Remove-ADObject -Confirm:$False

or hard-coded name instead of using a variable:

Get-ADObject -Filter {(cn -eq "Some User") -and (objectClass -eq "Contact")} | Remove-ADObject -Confirm:$False

custom attribute, set for AAD users who are not synced with local AD - see also extension attribute, set for AAD users who are synced with local AD

following code works for both synced (to local AD) and unsynced (pure cloud), users and guests because it figures out whether to set customAttribute or extensionAttribute

$UserPrincipalName = "Boba.Fett@mandalorian.com"
$thisUser = Get-MsolUser -UserPrincipalName $UserPrincipalName
$CustomAttribute1 = "Bounty hunter"
if ($null -eq $thisUser.LastDirSyncTime) { # good for cloud only
    Write-Host "CustomAttribute1 empty, change to '$CustomAttribute1' for $FullName (cloud $UserPrincipalName)" -ForegroundColor Cyan
    if ($thisUserType -eq "user") { # "native" users
        Set-Mailbox -Identity $UserPrincipalName -CustomAttribute1 $CustomAttribute1
    }
    else { # guest users
        Set-MailUser -Identity $UserPrincipalName -CustomAttribute1 $CustomAttribute1
    }
} else { # proceed differently for local AD synced
    $identity = $UserPrincipalName.Split("@")[0] # this usually works to get the local AD identity from the UserPrincipalName
    Write-Host "CustomAttribute1 empty, change to '$CustomAttribute1' for $FullName (local $identity)" -ForegroundColor Magenta
    Set-ADUser -Identity $identity -Replace @{extensionAttribute1 = $CustomAttribute1}
}

–D–

delegate a mailbox to another user - see also permissions - assign mailbox permissions/delegation of one user to another user

Add

Add-MailboxPermission 7Dwarves -User Sneezy -AccessRights FullAccess -AutoMapping:$true -Confirm:$False
Add-RecipientPermission 7Dwarves -AccessRights SendAs -Trustee Sneezy -Confirm:$False
Set-Mailbox 7Dwarves -GrantSendOnBehalfTo Sneezy

Add several users to one mailbox at once.

$emailNeedsDelegates = "7Dwarves@ArtisanMines.com"
$delegates = @("sneezy@ArtisanMines.com", "doc@ArtisanMines.com", "bashful@ArtisanMines.com", "grumpy@ArtisanMines.com")
$i=0
foreach ($delegate in $delegates) {
    $i++
    Write-Host "$i of $($delegates.Count): add $delegate as delegate to $emailNeedsDelegates" -ForegroundColor Green
    Add-MailboxPermission $emailNeedsDelegates -User $delegate -AccessRights FullAccess -AutoMapping:$true -Confirm:$False
    Add-RecipientPermission $emailNeedsDelegates -AccessRights SendAs -Trustee $delegate -Confirm:$False
}

conversely, you can add several delegated mailboxes to one user

$sharedMailboxes = @("7Dwarves@ArtisanMines.com", "SnowWhite@secretAdmirers.com", "getCourageUpToAsk@introverts.com")
$userNeedsAccess = "bashful@ArtisanMines.com"
$i = 0
foreach ($sharedMailbox in $sharedMailboxes) {
    $i++
    Write-Host "$i of $($sharedMailboxes.Count): add $userNeedsAccess as delegate to $sharedMailbox" -ForegroundColor Green
    Add-MailboxPermission $sharedMailbox -User $userNeedsAccess -AccessRights FullAccess -AutoMapping:$true -Confirm:$False
    Add-RecipientPermission $sharedMailbox -AccessRights SendAs -Trustee $userNeedsAccess -Confirm:$False
}

Verify

If you want to verify, you can run something like this:

Get-Mailbox 7Dwarves | Get-MailboxPermission | where {$_.user.tostring() -ne "NT AUTHORITY\SELF" -and $_.IsInherited -eq $false} | ft

Unfortunately, it doesn't return any automapping field. That column doesn't show up even if you add a | fl at the end. I haven't yet figured out how to get the automapping field to show up in any PowerShell command.

delegated mailbox does not show up as expected in Outlook see also automap a shared mailbox, remove for several users

You've delegated full access rights on a mailbox to another user's mailbox. And the delegated user can see the mailbox and emails for that user just fine in WebMail.   But the delegated user cannot see the other user's mailbox in Outlook.  Or if the shared mailbox itself is visible in the user's mailbox, there are no emails there in that mailbox. Answer: you have to take away full access and then add it back again with Automapping.  The following forces all of a user's delegates to show up in Outlook.

Two things to check

  • AutoMapping set on the delegate's mailbox and discussed below
  • MAPIEnabled set on the shared mailbox discussed elsewhere, follow link

Find all delegates

$Delegates = Get-Mailbox "mailboxToShareOut@yourDomain.com" | Get-MailboxPermission | where {$_.user.tostring() -ne "NT AUTHORITY\SELF" -and $_.IsInherited -eq $false}
$Delegates | %{Remove-MailboxPermission -Identity $_.Identity -user $_.User -AccessRights FullAccess -Confirm:$False}
$Delegates | %{Add-MailboxPermission -Identity $_.Identity -user $_.User -AccessRights FullAccess -AutoMapping:$True}

Perhaps, instead, you want to make sure just one of a user's delegates to show up in his Outlook. If, for example, you want to reset the marketing shared mailbox for Bob, marketing is the sourceUser below and Bob is the userToBeADelegate below, you could try to do it all in one fell swoop

Get-Mailbox "mailboxToShareOut@yourDomain.com" | Remove-MailboxPermission -user "userToBeADelegate@yourDomain.com" -AccessRights FullAccess -Confirm:$False | Add-MailboxPermission -AccessRights FullAccess -Automapping $true -User "userToBeADelegate@yourDomain.com"

But sometimes trying to do so much so fast in just one statement just doesn't seem to work well because it takes a bit of time between removing & adding back in again. So doing one at a time (remove & then add back in again using separate commands) might work better.

Remove

Remove-MailboxPermission 7Dwarves -User Sneezy -AccessRights FullAccess -InheritanceType All -Confirm:$False
Remove-RecipientPermission 7Dwarves -AccessRights SendAs -Trustee Sneezy -Confirm:$False

Set-Mailbox 7Dwarves -GrantSendOnBehalfTo @{remove="Sneezy"}

or to remove several at once

$emailNeedsDelegates = "7Dwarves@ArtisanMines.com"
$delegates = @("sneezy@ArtisanMines.com", "doc@ArtisanMines.com", "bashful@ArtisanMines.com", "grumpy@ArtisanMines.com")
$i=0
foreach ($delegate in $delegates) {
    $i++
    Write-Host "$i of $($delegates.Count): remove $delegate as delegate from $emailNeedsDelegates" -ForegroundColor Green
    Remove-MailboxPermission $emailNeedsDelegates -User $delegate -AccessRights FullAccess -InheritanceType All -Confirm:$False
    Remove-RecipientPermission $emailNeedsDelegates -AccessRights SendAs -Trustee $delegate -Confirm:$False
}

Add back

wait a bit after removing access above

Add-MailboxPermission 7Dwarves -User Sneezy -AccessRights FullAccess -AutoMapping:$true -Confirm:$False
Add-RecipientPermission 7Dwarves -AccessRights SendAs -Trustee Sneezy -Confirm:$False
Set-Mailbox 7Dwarves -GrantSendOnBehalfTo Sneezy

see also Add several users to one mailbox at once - again, wouldn't try to put the remove (above) & add back (below) inside the same loop but suggest separate loops separated by at least a few seconds.

Verify

If you want to verify, you can run something like this:

Get-Mailbox 7Dwarves | Get-MailboxPermission | where {$_.user.tostring() -ne "NT AUTHORITY\SELF" -and $_.IsInherited -eq $false} | ft

Unfortunately, it doesn't return any automapping field. That column doesn't show up even if you add a | fl at the end. I haven't yet figured out how to get the automapping field to show up in any PowerShell command.

For SendAs and SendOnBehalfOf:

Get-RecipientPermission 7Dwarves | ft
Get-Mailbox 7Dwarves | select GrantSendOnBehalfTo

delegates for a (normally shared) mailbox

Get-Mailbox someUser | Get-MailboxPermission | where {$_.user.tostring() -ne "NT AUTHORITY\SELF" -and $_.IsInherited -eq $false} | Select-Object user, AccessRights

remove one of the delegates

Remove-MailboxPermission -identity someUser -user someDelegate@yourDomain.com -AccessRights FullAccess -confirm: $false

delegated mailboxes to which a user has access

Get-Mailbox -Resultsize Unlimited | Get-MailboxPermission -user 'someUser@yourDomain.com'

delegated mailboxes to which a user has FullAccess

find

$DepartingUserIdentity = "someUser";

Get-Mailbox -Resultsize Unlimited | Get-MailboxPermission -user $DepartingUser

The one-liner above doesn't give you too much visibility if you have a ton of users and want to make sure you cover all users in all domains. If we break it up into more statements, we can user Write-Progress to see how far we've gotten.

$allMailboxes = Get-Mailbox -Resultsize Unlimited | sort DisplayName

if you're fairly certain the user would only have delegates within his own domain, filter on the domain instead of just getting -Resultsize Unlimited:

$allMailboxes = Get-Mailbox *LalaLand.uk | sort DisplayName
$i = 0
foreach ($mailbox in $allMailboxes) {
    $i++
    $percent = $i/$($allMailboxes.count)
    $percentTxt = $percent.ToString("P")
    Get-MailboxPermission -Identity $mailbox.UserPrincipalName -User SleepingBeauty@LalaLand.uk
    Write-Progress -Activity "$i of $($allMailboxes.count) $($mailbox.DisplayName) ($($mailbox.UserPrincipalName))" -PercentComplete $percent -Status "$percentTxt complete"
}

(or another way)

Get-Mailbox -Resultsize Unlimited | ? {$_.GrantSendOnBehalfTo-match $DepartingUserIdentity}

Or, with an individual, hard-coded email address:

Get-Mailbox -Resultsize Unlimited | Get-MailboxPermission -user 'someUser@yourDomain.com'

(or another way)

Get-Mailbox -Resultsize Unlimited | ? {$_.GrantSendOnBehalfTo -match "someUser"}

The most common permission we need to worry about is “FullAccess”.

Remove

Attempt to remove “FullAccess” in one fell swoop fails because we run out of threads

Get-Mailbox -Resultsize Unlimited | Get-MailboxPermission -user $DepartingUser | % {Remove-MailboxPermission -identity $_.Identity -user $_.User -AccessRights FullAccess -InheritanceType All -confirm: $false}

Error is

Remove-MailboxPermission : The session WinRM1, 24b1bbc8-5f00-4836-b7c0-097b589ed891, outlook.office365.com is not available to run commands.  The session availability is Busy.

Which means trying to do too much at once.

But split this up into 2 parts, seems to work better

$targetUsers = Get-Mailbox | Get-MailboxPermission -user $DepartingUser

$targetUsers | % {Remove-MailboxPermission -identity $_.Identity -user $_.User -AccessRights FullAccess -InheritanceType All -confirm: $false}

delegated mailboxes that a user has SendOnBehalfTo

find

$DepartingUserIdentity = "someUser";

Get-Mailbox -Resultsize Unlimited | ? {$_.GrantSendOnBehalfTo -match $DepartingUserIdentity}

remove

delegates, generate list

for all mailboxes

Get-Mailbox -RecipientTypeDetails -ResultSize Unlimited | Get-MailboxPermission | where {$_.user.tostring() -ne "NT AUTHORITY\SELF" -and $_.IsInherited -eq $false} | select Identity, User, UserPrincipalName

Above doesn't group delegates for any one mailbox; below focuses on shared mailboxes (or omit the -RecipientTypeDetails SharedMailbox if you sill want all mailboxes) and lists the original mailbox along with all the delegates for each, one row for each shared mailbox.

$mailboxes = Get-Mailbox -RecipientTypeDetails SharedMailbox -ResultSize Unlimited | select DisplayName, UserPrincipalName, Identity, Alias | sort displayname
$report = $mailboxes | % {
     $permission = Get-MailboxPermission -Identity $_.alias | ? {$_.IsInherited -eq $False -and $_.User -ne "NT AUTHORITY\SELF"};
     New-Object -TypeName PSObject -Property @{
     "Display Name" = $_.DisplayName
     "EmailAddress" = $_.UserPrincipalName
     "delegate" = $permission.User -join ", "
     "AccessRights" = $permission.AccessRights -join ", "}}
$report | ogv

Unfortunately, this above lists users and their permissions in separate columns. so leave off the last AccessRights column

We don't always want to check for all users' delegates. A department head may instead only care about certain departmental shared mailboxes - perhaps to make sure these shared departmental mailboxes are being monitored by at least someone.

$mailboxes = ("announcements@MegaCorp.com", "inquiries@MegaCorp.com", "consulting@MegaCorp.com")
$delegates = $mailboxes | % {Get-Mailbox $_ | Get-MailboxPermission | where {$_.user.tostring() -ne "NT AUTHORITY\SELF" -and $_.user.tostring() -ne "admin@MegaCorp.com" -and $_.IsInherited -eq $false} | select Identity, User}
$delegates

This assumes we have one administrator ("admin") who is a delegate on all or most of these mailboxes which we don't care to report.

Unfortunately, this doesn't include any mailboxes with no delegate which omission we want to know. This, although admittedly longer, also includes boxes with no delegate:

$result = @()
$mailboxes | ForEach-Object {
     $delegates = Get-Mailbox $_ |Get-MailboxPermission | where {$_.user.tostring() -ne "NT AUTHORITY\SELF" -and $_.user.tostring() -ne "admin@MegaCorp.com" -and $_.IsInherited -eq $false}
     if ($delegates.count -eq 0) {$result += New-Object PSObject -property @{mailbox = $_; delegate = "No one!"}}
     else {foreach ($delegate in $delegates) {$result += New-Object PSObject -property @{mailbox = $_; delegate = $delegate.User}}}
}
$result | ogv

delegate, remove - see also remove shared (delegated) mailboxes for a user

Remove-MailboxPermission -identity someUser -user someDelegate@yourDomain.com -AccessRights FullAccess -confirm: $false

what if you get:

The operation couldn't be performed because 'someUser' matches multiple entries.

Get IDs for the dupes:

Get-Mailbox someUser | select UserprincipalName, Guid, ExchangeGuid

then re-run the delete command using the Guid:

Remove-MailboxPermission -identity c5909e04-1407-423f-82c8-0f60de50cf0c -user someDelegate@yourDomain.com -AccessRights FullAccess -confirm: $false

You may ask: why not just rename one of the aliases to remove the alias?  First of all, if you sync with local AD, you'd have to make the change in local AD.  Second of all, there's probably a good reason why there are duplicate aliases.  Alias is the same as proxyAddress. And you might one user with someuser@yourDomain1.com and another someuser@yourDomain2.com. Like if this user moved to a different division but we kept the old mailbox hanging around as a shared mailbox for successor to monitor.

delete all emails in a folder - see here for a nicer script that generalizes this process and includes commands below

This approach will only purge up to 10 emails.   Here's a script that loops through loop however many iterations of 10 at a time you specify to get the job done.

Need to already have an Exchange security session. Check to see if such a session has already been established. If the following system variable is empty, then we don't have such a session and must establish such a session first before proceeding.

$EOSession

We must know the folder ID - see folder ID, find for every folder in a user's mailbox

$search = "folderid:5B38D6796C629B4A878E979FE690E93900000050F84E0000"

Come up with a name for the job we want to submit

$date = Get-Date -format "yyyy-MM-dd HH.mm.ss"

$person = "Some User"

$complianceSearchName = "$person $date"

Create the job.  Once you do this, you can see it in the Office 365 Security & Compliance section.  We haven’t run it yet.  But we can at least see that it’s there.

New-ComplianceSearch -Name $complianceSearchName -ExchangeLocation all -ContentMatchQuery "$search"

Now that we’ve created it, start it.

Start-ComplianceSearch $complianceSearchName

The script I mention above does a nice job of letting you know when this command is done. Without a loop that script includes, I can't figure any way of really knowing when it’s done. But once it’s done, you can run this:

Get-ComplianceSearch $complianceSearchName | Format-List -Property Items

And it should return something like:

Items : 833

Now that we’ve created & run the search, we use that to try to get rid of all the items in the folder:

New-ComplianceSearchAction -SearchName $complianceSearchName -Purge -PurgeType SoftDelete

Should return something like:

Confirm
Are you sure you want to perform this action?
This operation will make message items meeting the criteria of the compliance search "Some Name 2019-08-29 16.11.33" completely inaccessible to users. There is no automatic method to undo the  removal of these message items.

Name                                 SearchName                     Action RunBy       JobEndTime Status
----                                 ----------                     ------ -----       ---------- ------
Some User 2019-08-29 16.11.33_Purge  Some User 2019-08-29 16.11.33  Purge  Your Name            Starting

No matter how many times you run it, it returns pretty much the same result except eventually you’ll get a “JobEndTime” which will never change and times after your first run happen really fast.

Name                                 SearchName                     Action RunBy       JobEndTime           Status
----                                 ----------                     ------ -----       ----------           ------
Some User 2019-08-29 16.11.33_Purge  Some User 2019-08-29 16.11.33  Purge  Your Name   8/29/2019 9:13:57 PM Completed

This is because you only get to run this once and when you think you’re running it more times, you’re really not but the system just doesn’t say so.  And it only gets rid of 10 emails that first run. 

Another, saner way to get the results of your purge job is to run this:

Get-ComplianceSearchAction -Identity "$($complianceSearchName)_Purge"

Once you’re done, might as well delete it out of the queue so it doesn’t clog up what you see in the Office 365 Security & Compliance section.  After all, the only reason we created this search in the first placeholder was to give us search results to try to delete.

Remove-ComplianceSearch -Identity $complianceSearchName -Confirm:$false

delete emails

This will delete all emails for a particular user which are older than a certain date:

Search-Mailbox -Identity "Bob Smith" -SearchQuery "(Received < 11/1/2018)" -DeleteContent -Confirm:$false

or

Get-Mailbox -Identity "some body" | Search-Mailbox -SearchQuery "(Received < 11/1/2018)" -DeleteContent

A few things to note:

  • Not just anyone can run the Search-Mailbox; you have to have the right role added
  • The operators for the SearchQuery date range argument aren't the usual "-lt" or its ilk. Rather, it seems content with regular "<" and its ilk.
  • If you think you can leave off the -DeleteContent off the end to test, you'd be wrong.  It will fail with a worthless, misleading "target mailbox or .pst file path is required error"
  • If your mailbox is big (like, say, over half a million records), this could take a long time.  Like over 12 minutes.  The time it takes seems to be erratic.  Sometimes just a few minutes, other times close to an hour.
  • The way it's written above, it'll ask for confirmation to make sure you really want to delete these.  And even with an added -Confirm:$false, it will still ask for confirmation.

I've found that if I run this too many times in succession, I might get an error:

Deletion failed with error 'Move/Copy messages failed.' for the following item:

And then followed by the subject and some other gobbledegook for each of hundreds of emails in the date range. I think this has to do with the index getting corrupt.  If I wait a day or so, it seems I can resume.  I had this problem deleting 10,000 at a time for a mailbox which had over half a million records.  Even if I crunched down the time span to a much smaller interval:

Search-Mailbox -Identity "some body" -SearchQuery {Received:9/1/2018..9/2/2018} -DeleteContent -Confirm:$false

which, in this case, resulted in only 3 records, I still get the same error

deleted emails, find out who deleted them and when - see audit deleted emails

deleted emails, permanently delete

Search-Mailbox -Identity someUser -SearchQuery '#deleted items#' -DeleteContent

Should return something like:

WARNING: The Search-Mailbox cmdlet returns up to 10000 results per mailbox if a search query is specified. To return more than 10000 results, use the New-MailboxSearch cmdlet or the In-Place eDiscovery & Hold console in the Exchange Administration Center.
Confirm
Deleting content from mailboxes someUser
[Y] Yes [A] Yes to All [N] No [L] No to All [?] Help (default is "Yes"): a
 

RunspaceId       : 1e74c7bf-a89d-44d8-b9bc-3298b5f5ab93
Identity         : Some User
TargetMailbox    :
Success          : True
TargetFolder     :
ResultItemsCount : 68
ResultItemsSize  : 16.2 MB (16,987,063 bytes)

To get

deleted emails, recover

first way - easier if items only recently deleted

list items of potential interest

Get-RecoverableItems -Identity "haplessUser@yourDomain.com" -SourceFolder RecoverableItems -FilterItemType Ipm.Note -FilterStartTime "8/26/2019 01:00:00" -FilterEndTime "8/27/2019 22:00:00" | ogv

recover

Restore-RecoverableItems -Identity " haplessUser @yourDomain.com" -SourceFolder RecoverableItems -FilterStartTime "8/26/2019 01:00:00" -FilterEndTime "8/27/2019 22:00:00"

second way - this might work better if items have been deleted for a while now

My initial mistake of setting Discovery Search Mailbox as the target mailbox was the beginning of a long detour.

Search-Mailbox "haplessEndUser@theirDomain.com" -SearchQuery "subject:""$9 million dollar signed contract!""" -TargetMailbox "Discovery Search Mailbox" -TargetFolder "Signed Contract Recovery" -LogLevel Full

This worked.  I got 8 items:

RunspaceId       : d8758080-fe7c-4006-ae91-3a0d56aad4e0
Identity         : Hapless EndUser
TargetMailbox    : DiscoverySearchMailbox{D919BA05-46A6-415f-80AD-7E09334BB852}
Success          : True<
TargetFolder     : \Signed Contract Recovery\Hapless EndUser-3/22/2018 3:48:33 PM
ResultItemsCount : 8
ResultItemsSize  : 245.4 KB (251,260 bytes)

But I should have thought better before sending this to the “Discovery Search Mailbox".  Where the heck is that?  It turns out the system somehow recognized the word Discovery Search and made a mailbox with special, limited properties.  Those properties include locking any search results in it in such a way that once they go in, you can't move them anywhere else.  But I didn't know that right off.  So I tried moving these same 8 items somewhere else.  Note: you can’t just send it to the same mailbox as the source mailbox or you’ll get this:

WARNING: The source mailbox 'Hapless EndUser' will not be searched because it is the target mailbox. The source mailbox cannot be used as the target mailbox.

So, instead send to my email box:

Search-Mailbox "haplessEndUser@theirDomain.com" -SearchQuery "subject:""$9 million dollar signed contract!""" -TargetMailbox "longSufferingSysAdmin@myDomain.com" -TargetFolder "Signed Contract Recovery" -LogLevel Full

But that returns nothing:

RunspaceId       : d8758080-fe7c-4006-ae91-3a0d56aad4e0
Identity         : Hapless EndUser
TargetMailbox    : Longsuffering SysAdmin
Success          : True
TargetFolder     : \Signed Contract Recovery\Hapless EndUser-3/22/2018 4:05:25 PM
ResultItemsCount : 0
ResultItemsSize  : 0 B (0 bytes)

Well, that was annoying and surprising.  Let’s see if we can dig them out of that discovery mailbox and put them in my mailbox:

Search-Mailbox "Discovery Search Mailbox" -SearchQuery "subject:""$9 million dollar signed contract!""" -TargetMailbox "longSufferingSysAdmin@myDomain.com" -TargetFolder "Signed Contract Recovery" -LogLevel Full

Again, no results:

RunspaceId       : d8758080-fe7c-4006-ae91-3a0d56aad4e0
Identity         : DiscoverySearchMailbox{D919BA05-46A6-415f-80AD-7E09334BB852}
TargetMailbox    : Longsuffering SysAdmin
Success          : True
TargetFolder     : \Signed Contract Recovery\Discovery Search Mailbox-3/22/2018 4:06:57 PM
ResultItemsCount : 0
ResultItemsSize  : 0 B (0 bytes)

Same “no results” if I choose a more verbose name I find for this box:

Search-Mailbox "DiscoverySearchMailbox{D919BA05-46A6-415f-80AD-7E09334BB852}" -SearchQuery "subject:""$9 million dollar signed contract!""" -TargetMailbox "longSufferingSysAdmin@myDomain.com" -TargetFolder "Signed Contract Recovery" -LogLevel Full

OK, so they’re stuck in this weird Discovery box.  Can I make myself a delegate?

Get-Mailbox -Resultsize unlimited -Filter {RecipientTypeDetails -eq "DiscoveryMailbox"} | `
    Add-MailboxPermission -AccessRights FullAccess -Automapping $true -User "longSufferingSysAdmin@myDomain.com"

Apparently:

WARNING: The appropriate access control entry is already present on the object "CN=DiscoverySearchMailbox{D919BA05-46A6-415f-80AD-7E09334BB852},OU=yourDomain.onmicrosoft.com,OU=Microsoft Exchange Hosted Organizations,DC=NAMPR10A004,DC=PROD,DC=OUTLOOK,DC=COM" for account "NAMPR10A004\jmosc50247-845198855".

 

Identity             User                 AccessRights   IsInherited Deny
--------             ----                 ------------   ----------- ----
DiscoverySearchMa... NAMPR10A004\jmosc... {FullAccess}

Well, it didn’t error out.  But it seems to think I already have access.  This didn’t show up as a new mailbox in Outlook.  Nor coulc I find it through WebMail. But after half an hour or so, it finally did show up as a delegated box in my Outlook and I was able to copy the email to my main mailbox & send (couldn’t send from that mailbox because I hadn’t set myself up with “SendAs” privileges.)

So, the big lesson learned: just send it to my own dang email box to begin with.  Otherwise it gets trapped in that Discovery mailbox that’s created on the fly and the only way to get it out then is by setting myself up as a delegate.

deleted mailboxes, list - see also deleted users (soft deleted), list

Get-Mailbox -SoftDeletedMailbox | Select DisplayName,ExchangeGuid,PrimarySmtpAddress,ArchiveStatus,DistinguishedName | Out-GridView -Title "Select Mailbox and GUID" -PassThru

deleted mailbox, recover mailbox when synced user associated with that deleted mailbox is still present

First, get the ExchangeGUID of the deleted mailbox

get-mailbox -SoftDeletedMailbox -identity somedeleteduser | fl ExchangeGUID

Simlarly, get the ExchangeGUID of the target user. Make sure this target user has an email licenses of some kind and that we've logged in at least once - to create an empty mailbox to migrate into

get-mailbox   -identity userWhoLostHisMailbox | fl ExchangeGUID

Copy the stuff from the deleted mailbox to the target, using the ExchangeGUIDs you got above as appropriate:

new-MailboxRestoreRequest -SourceMailbox "8c86592c-5cb7-4bc5-8b06-7f6a57b84d2b" -TargetMailbox "4c587005-e303-4689-aed7-564e49b0734b" -AllowLegacyDNMismatch

deleted mailbox, recover a soft deleted / disconnected mailbox merged to another user on exchange online - So You Need To Recover A Soft Deleted / Disconnected Mailbox Merged To Another User On Exchange Onlines or Office 365 and Exchange Online Restore and Recover Processes for Soft-Deleted Mailboxes

DirSync errors, list

Get-MsolDirSyncProvisioningError -ErrorCategory PropertyConflict | ft

disabled accounts, filter out from list of mailboxes - see mailbox types, filter out types

distribution group, add members - this same syntax applies to email-enabled security groups

for regular users, perhaps assign everyone in a certain domain to a distribution group

$users = Get-MsolUser -All | ? {($_.UserPrincipalName -like "*someDomain.com") -and ($_.islicensed -eq $true)}
$users | % {Add-DistributionGroupMember -Identity "Some Distribution Group" -Member $_.UserPrincipalName}

for guest users, perhaps assign everyone in a certain department to a distribution group

$guests = Get-AzureADUser -Filter "userType eq 'Guest' and (Department eq 'Some Department')"
$guests | % {Add-DistributionGroupMember -Identity "Some Distribution Group" -Member $_.UserPrincipalName}

for contacts, again perhaps assign everyone in a certain domain to a distribution group

$contacts = Get-MsolContact -All | where {($null -ne $_.EmailAddress) -and ($_.EmailAddress.split("@")[1] -like "*yourDomain.com")}
$contacts | % {Add-DistributionGroupMember -Identity "Some Distribution Group" -Member $_.EmailAddress}

Caveat: you might see: multiple recipients matching the identity xxx@yourDomain.com

distribution group, bulk change WindowsEmailAddress of cloud-only (exclude those synced with local AD)

$distGroup = Get-DistributionGroup | ? {$_.isdirsynced -eq 0 -and ($_.WindowsEmailAddress.split("@")[1] -match "yourdomain.com")}

Optional: inspect first before proceding to the command that actually applying our changes:

$distGroup | ft name, proxyAddresses

Now proceed to actually do what we set out to do: set "PrimarySmtpAddress" for all users which had corresponding "PrimarySmtpAddress" correpsonding to our domain:

$distgGp | %{Set-DistributionGroup -identity $_.identity -WindowsEmailAddress ($_.WindowsEmailAddress.split("@")[0] +"@yourTenant.onmicrosoft.com")}

Note that we could have done all this in one command without the intermediate variable.   But it's nice to actually see the group we intend to change things before we actually apply changes (using the Set-DistributionGroup command) just to make sure.

distribution group, authorization to use - see also distribution group, outsiders can use

distribution group, create new

New-DistributionGroup -Name "Some Distribution Group Name" -PrimarySmtpAddress "distrGrpEmail@yourDomain.com" -Type "Distribution"

distribution group, find by email

Get-DistributionGroup -Identity someGroup@yourDomain.com

distribution group, force delete when synchronized from your on-premises organization

You may have had a distribution group that you used to synchronize from your on-premises organization but which now you don't want. You may have deleted it from your local Active Directory and even run an initial sync (full sync) but it still persists.  If you attempt:

Remove-DistributionGroup -Identity "Some Distr Group" -Confirm:$false

You get:

The action 'Remove-DistributionGroup', 'Confirm,Identity', can't be performed on the object 'Employees Germany' because the object is being synchronized from your on-premises organization. This action should be performed on the object in your on-premises organization

And there is no -Force parameter available for this command. Instead, run:

Get-MsolGroup -SearchString "Some Distr Group"

This should show the ObjectId.  Copy that and paste into:

Remove-MsolGroup -ObjectID 58217d82-fde2-4b25-8255-82e8f6dfdfb6

replacing with the appropriate ObjectId you got from the first command.

distribution group, guest ID incorporation into - see guest ID, delegate of shared mailbox

The short story is: you must make sure your guest IDs have their WindowsEmail attribute populated for them to work properly.

distribution group, list all

this is kind of an "old school" way of creating this object, by adding one property at a time

$report = @()
$distgroups = @(Get-DistributionGroup -ResultSize Unlimited)
foreach ($dg in $distgroups)
{
    $count = @(Get-DistributionGroupMember $dg.DistinguishedName).Count
    $reportObj = New-Object PSObject
    $reportObj | Add-Member NoteProperty -Name "Group Name" -Value $dg.Name
    $reportObj | Add-Member NoteProperty -Name "PrimarySmtpAddress" -Value $dg.PrimarySmtpAddress
    $reportObj | Add-Member NoteProperty -Name "Require Sender Authentication" -Value $dg.RequireSenderAuthenticationEnabled # can outsiders send?
    $reportObj | Add-Member NoteProperty -Name "Member Count" -Value $count # adding this takes longer
    Write-Host "$($dg.Name) has $($count) members"
    $report += $reportObj
}
$report | Export-CSV -Path "$([environment]::getfolderpath("mydocuments"))\DistrGrpCount$((Get-Date).ToString('MM-dd-yyyy_hh-mm-ss')).csv" -NoTypeInformation -Encoding UTF8

Or, more succinctly and adding a few more properties (RecipientType, GroupType, whether or not synced to local AD, email domain)

This is also a more "modern" way to create an object, without having to have separate "Add-Member" statements:

$report = @(Get-DistributionGroup -ResultSize Unlimited) | % {New-Object -TypeName PSObject -Property @{
     "Group Name" = $_.Name
     "Display Name" = $_.DisplayName
     "Primary Smtp Address" = $_.PrimarySmtpAddress
     "Recipient Type" = $_.RecipientType
     "Group Type" = $_.GroupType
     "Insiders only" = $_.RequireSenderAuthenticationEnabled # restrict from outsiders?
     "local AD Sync" = $_.IsDirSynced
     "domain" = $_.PrimarySmtpAddress.split("@")[1]
     count = @(Get-DistributionGroupMember $_.DistinguishedName).Count}} # adding this takes longer
$report | ogv

This more succinct command doesn't, however, preserve the order you choose for the fields. In order to specify the order of the columns, you'll have to select them in the order you want.

distribution group, list all contacts along with which distribution group(s) to which each belongs. Note that we specify $_.isDirSynced -eq $false. Cloud-only contacts are most vulnerable to an accidental deletion with no hope to recover because unlike users, contacts don't seem to end up in any soft-deleted state; they just disappear.

$collection = @()
$contacts = Get-MailContact -ResultSize unlimited
$contactsNotSynced = Get-MailContact | ? {$_.isDirSynced -eq $false} | Select-Object Identity, DistinguishedName, DisplayName, Alias, WindowsEmailAddress, externalEmailAddress, PrimarySmtpAddress, emailAddresses, HiddenFromAddressListsEnabled
ForEach($contact in $contactsNotSynced) {
    $DN=$contact.DistinguishedName
    $Filter = "Members -like '$DN'"
    $groups = Get-DistributionGroup -ResultSize unlimited -Filter $Filter
    $outObject = "" | Select "DisplayName","Alias","WindowsEmailAddress","PrimarySmtpAddress","emailAddresses","EmailDomain","Groups"
    $outObject."DisplayName" = $contact.DisplayName
    $outObject."Alias" = $contact.Alias
    $outObject."WindowsEmailAddress" = $contact.WindowsEmailAddress
    $outObject."PrimarySmtpAddress" = $contact.PrimarySmtpAddress
    $outObject."emailAddresses" = $contact.emailAddresses
    $outObject."EmailDomain" = $contact.WindowsEmailAddress.split("@")[1]
    $outObject."Groups" = ($groups -join "; ") # join the array into a string so it comes out properly in the CSV file
    $collection += $outObject
}
$collectionSortedByEmailDomain = $collection | sort EmailDomain, Groups
$collectionSortedByEmailDomain | Out-GridView
$collectionSortedByEmailDomain | Export-CSV -Path "$([environment]::getfolderpath("mydocuments"))\ContactsNotSynced$((Get-Date).ToString('MM-dd-yyyy_hh-mm-ss')).csv" -NoTypeInformation -Encoding UTF8

distribution group, list all distribution lists to which someone belongs

Easiest way

Get-DistributionGroup -ResultSize Unlimited | ? {(Get-DistributionGroupMember $_.Name | % {$_.PrimarySmtpAddress}) -contains "heidi@klum.com"}

Or in a little more roundabout way, get your user's distinguished name

Get-User Heidi@Klum.com | select -ExpandProperty DistinguishedName

Once you have your user's distinguished name, try one or some of these:

Get-Recipient -Filter "Members -eq 'CN=Heidi,OU=Klum.onmicrosoft.com,OU=Microsoft Exchange Hosted Organizations,DC=NAMPR10A004,DC=PROD,DC=OUTLOOK,DC=COM'"

will return all Distribution groups, Mail-enabled security groups and Office 365 groups

Get-Recipient -Filter "Members -eq 'CN=Heidi,OU=Klum.onmicrosoft.com,OU=Microsoft Exchange Hosted Organizations,DC=NAMPR10A004,DC=PROD,DC=OUTLOOK,DC=COM'" -RecipientTypeDetails GroupMailbox,MailUniversalDistributionGroup,MailUniversalSecurityGroup

If you want to return membership of Exchange Role Groups as well, use the Get-Group cmdlet:

Get-Group -Filter "Members -eq 'CN=Heidi,OU=Klum.onmicrosoft.com,OU=Microsoft Exchange Hosted Organizations,DC=NAMPR10A004,DC=PROD,DC=OUTLOOK,DC=COM'"

We can also do the same with the Azure AD cmdlets

Get-AzureADUser -SearchString Heidi@Klum.com | Get-AzureADUserMembership
Get-AzureADUser -SearchString Heidi@Klum.com | Get-AzureADUserMembership | % {Get-AzureADObjectByObjectId -ObjectId $_.ObjectId | select DisplayName,ObjectType,MailEnabled,SecurityEnabled,ObjectId} | ft

distribution group members, fill in missing WindowsEmailAddress - see also distribution group members, list with WindowsEmailAddress

If all the members in a distribution group are supposed to have valid WindowsEmailAddress, if you've already gone about listing distribution group members with WindowsEmailAddress, it's a short step from there to fill in any missing WindowsEmailAddress attributes and update the HiddenFromAddressListsEnabled at the same time.

$employeesMexico | % {
    $Mailbox = Get-MailUser -Identity $_.WindowsLiveID | select DisplayName, name, UserPrincipalName, HiddenFromAddressListsEnabled, WindowsEmailAddress
    if ($Mailbox.HiddenFromAddressListsEnabled -eq $True) {
        $newWindowsEmailAddress = $Mailbox.Name.ToString().split("_")[0]+"@ForeignTenant.onmicrosoft.com";
        Set-MailUser -Identity $_.WindowsLiveID -WindowsEmailAddress $newWindowsEmailAddress -HiddenFromAddressListsEnabled $False}}

It’s probably a good idea to first display your distribution group members along with any missing WindowsEmailAddress before running the code above to update them.

Once you've filled in these missing WindowsEmailAddresses, it might behoove someone on that foreign tenant to run something like listing proxy addresses for users in a domain - especially breaking out the "onmicrosoft.com" proxy addresses into a separate column and compare that with your list of WindowsEmailAddresses to make sure they match up. Because if you have a WindowsEmailAddresses in your tenant with no corresponding target proxy address in the foreign tenant, problems.

distribution group members, list

Get-DistributionGroupMember -Identity "Golden Horde" | select Name, DisplayName, UniversalPrincipalName, RecipientType, PrimarySmtpAddress, WindowsLiveID

distribution group members, list with WindowsEmailAddress - see also distribution group members, fill in missing WindowsEmailAddress

What if everyone in this distribution list is a guest ID and we want to know more about their HiddenFromAddressListsEnabled and WindowsEmailAddressattributes? Start by putting the results of the above command into a variable.

UniversalPrincipalName is usually empty/missing. But WindowsLiveID will usually suffice. So, we'll get that to use for the subsequent statement.

$employeesMexico = Get-DistributionGroupMember -Identity "Employees Mexico" | select Name, DisplayName, RecipientType, PrimarySmtpAddress, WindowsLiveID

Then get values for HiddenFromAddressListsEnabled and WindowsEmailAddressattributes, using WindowsLiveID for the identity we search on for each member. If all these happen to be from the same foreign tenant, we can also suggest a WindowsEmailAddress for those who are missing one. In this case, the way we decide who does or doesn't have a WindowsEmailAddress is by choosing on the closely related HiddenFromAddressListsEnabled attribute.

$WindowsEmailAddressStatus = $employeesMexico | % {
    $Mailbox = Get-MailUser -Identity $_.WindowsLiveID | select DisplayName, name, UserPrincipalName, HiddenFromAddressListsEnabled, WindowsEmailAddress
    New-Object -TypeName PSObject -Property @{
    "new WindowsEmailAddress" = if ($Mailbox.HiddenFromAddressListsEnabled -eq $True) {$Mailbox.Name.split("_")[0]+"@ForeignTenant.onmicrosoft.com"} else {"not needed"}
    "Display Name"= $Mailbox.DisplayName
    "WindowsEmailAddress" = $Mailbox.WindowsEmailAddress
    "HiddenFromAddressListsEnabled" = $Mailbox.HiddenFromAddressListsEnabled
    "UserPrincipalName"= $Mailbox.UserPrincipalName
    "name" = $Mailbox.name}}

see also distribution group members, fill in missing WindowsEmailAddress

distribution group members, how many?

$distributionGroup = Get-DistributionGroupMember -Identity "Minions"
$distributionGroup.Count

distribution group members, update all to the same department

Get-DistributionGroupMember -Identity "Legal" | % {Set-AzureADUser -ObjectId $_.WindowsLiveID -Department "Dewey Cheetum, and Howe"}

distribution group, outsiders can use - for the properties below, "true" means only "insiders" can use this distribution group and "false" means "outsiders" can also use.

cloud only

RequireSenderAuthenticationEnabled

local active directory

msExchRequireAuthToSendTo

distribution group, primarySMTPAddress change

$groups = Get-DistributionGroup-resultsize unlimited |? {$_.Name -like "Bad*"}
$groups| % {
    "$($_.name) of $($groups.count) - $($_.PrimarySmtpAddress.split("@")[0])@good.com"
    Set-DistributionGroup $_.Name -PrimarySmtpAddress "$($_.PrimarySmtpAddress.split("@")[0])@good.com"
}

distribution group, remove a user

find distribution groups to which he belongs

(Get-Recipient -Filter "Members -eq '$((Get-User $DepartingUser.UserPrincipalName).DistinguishedName)'").name

delete (This works OK if he only belongs to one distribution group. Have not tested for case when he belongs to many distribution groups. Would probably need to throw in a for each.)

Remove-DistributionGroupMember -Identity (Get-Recipient -Filter "Members -eq '$((Get-User $DepartingUser.UserPrincipalName).DistinguishedName)'").name -Member $DepartingUser.UserPrincipalName

distribution group, rename

There are about 3 different properties that constitute name: Identity, Name, DisplayName.  If you create a distribution group through the Exchange Admin Center, it'll append a number corresponding to the date to the Identity. I sometimes rename that.  The command below does all 3 at once. Name is the same as Identity and that's how I rename Identity

Set-DistributionGroup -Identity "DivisionNameStaff20191209171925" -Name "DivisionNameStaff" -Alias "DivisionNameStaff" -DisplayName "Division Name Staff"

distribution group, verify whether one person can see calendars for every member in - see calendar permissions, what permissions does one person have for each person in a distribution group?

domain, bypass spam settings for (whitelist) - see whitelisted domains for all transport rules

domain, list all emails for

see also: Windows Email Address, list guest users By domain, Windows Email Address, list guest users For a domain

Get-MsolUser -All | where {$_.UserPrincipalName -match "yourDomain.com"}

or

Get-Mailbox *yourDomain.com

list shared mailboxes for a domain with who has permissions on them

Get-Mailbox *yourDomain.com | Get-MailboxPermission| where {$_.user.tostring() -ne "NT AUTHORITY\SELF" -and $_.IsInherited-eq $false} | Select-Object Identity, User, AccessRights

domain, list all guest IDs for along with their WindowsEmailAddress, sorted by the domain of their WindowsEmailAddress - see Windows Email Address, list guest users By domain, Windows Email Address, list guest users For a domain

domains, sort email boxes by - see mailboxes, sort by domain, Windows Email Address, list guest users By domain, Windows Email Address, list guest users For a domain

domains, sort contacts by

Get-MsolContact -All | Select-Object @{n="Dom";e={$_.EmailAddress.split("@")[1]}}, displayName, EmailAddress | Sort-Object dom, displayName | ogv

–E–

eDiscovery - see litigation hold

email box full - see Find folder ID for all of a user's folders, sort decending by size

email addresses, remove

Set-MailUser -Identity "someUser@yourDomain.com" -emailAddresses @{remove = "smtp:someAlias@yourTenant.onmicrosoft.com"}

email-enabled security groups - see distribution group, add members

email traffic

Best place I've found to get this is directly from the message trace log. I've had pretty good luck with Get-DetailedMessageStats.ps1. It neatly summarizes traffic by email box per day with separate columns for inbound & outbound counts & sizes. The only weird things were:

  1. Even though I specified credentials as an argument, it still insisted on collecting them from me
  2. Although it claims to attempt to grab all the data available in the log (which I would think would be around 30 days), It only got 3 days' worth

There's other traffic besides sent/received emails - at least as measured by our gateway. Such traffic that comes to mind is Outlook fetching email for non-cached clients.  Haven't figured out how to capture that from the server yet.

email user info

Get-MsolUser -UserPrincipalName test@yourdomain.com | fl

emails, count of from one user for a day

[Int ] $intRec = 0
Get-TransportService | `
  Get-MessageTrackingLog -ResultSize Unlimited -Start "4/09/2015" -End "4/10/2015" `
  -Recipients "someuser@yourdomain.com" -EventID DELIVER | `
  ForEach { $intRec++ }
Write-Host "E-mails received:", $intRec

emails, find all emails with some subject in some user's email box - see subject of email, find

Exchange role assignments for a user - see role assignments, to what roles is a user assigned?

exist, does a contact exist - see contact exists?

exist, does an email box exist - see mailbox exists?

exist, does a mailbox exist - see mailbox exists?

extension attribute, set for AAD users who are synced with local AD - see also custom attribute, set for AAD users who are not synced with local AD

–F–

folder, delete all emails in - see delete all emails in a folder

folder ID, find for every folder in a user's mailbox

The raw folder ID isn't very useful as it comes out of Get-MailboxFolderStatistics.  It might looke like this:

LgAAAAA6VjXDW2+DT6KoDHT98/baAQBbg435bGKbSod+mJ/pYOk5AAAAUPhOAAAB

For one thing, it isn't in hex format. Instead, to search for it and perfrom actions on it, you must convert it from this form to another which is more useful for searches.

Get folder ID for just one folder for a user:

$UserFolderStats = Get-MailboxFolderStatistics "someUser"
$UserFolderStats | ? {$_.FolderPath -eq "/Sync Issues/Conflicts"} | select FolderPath, FolderandSubFolderSize, ItemsInFolderAndSubfolders, NewestItemReceivedDate, OldestItemReceivedDate | ft
$folderId = ($UserFolderStats | ?{$_.FolderPath -eq "/Sync Issues/Conflicts"}).FolderID
$encoding = [System.Text.Encoding]::GetEncoding("us-ascii")
$nibbler = $encoding.GetBytes("0123456789ABCDEF");
$folderIdBytes = [Convert]::FromBase64String($folderId);
$indexIdBytes = New-Object byte[] 48;
$indexIdIdx =0;
$folderIdBytes | select -skip 23 -First 24 | % {$indexIdBytes[$indexIdIdx++]=$nibbler[$_ -shr 4];$indexIdBytes[$indexIdIdx++]=$nibbler[$_ -band 0xF]}
$folderIDusable = $encoding.GetString($indexIdBytes)

After which $folderIDusable might look more like this:

5B836D679C692B4A778E989FE960E93900000050F84E0000

Which is more useful for searching.

Find folder ID for all of a user's folders, sort decending by size

The following gets the folder ID for all of a user's folders. Well, all the folders that have anything in them, anyway. This encapsulates several of the statements above into just one field in the display in the code below:

$user = "someUser"
$encoding = [ System.Text.Encoding]::GetEncoding("us-ascii")
$nibbler = $encoding.GetBytes("0123456789ABCDEF");
$userFolders = Get-MailboxFolderStatistics $user -IncludeOldestAndNewestItems | ? {$_.ItemsInFolderAndSubfolders -gt 0} | Select-Object @{name="Location";Expression={$_.Identity -replace "$user\\",""}},FolderandSubFolderSize,ItemsinFolderandSubfolders,@{name="FolderID"; Expression = {$folderIdBytes = [Convert]::FromBase64String($_.folderId);$indexIdBytes = New-Object byte[] 48;$indexIdIdx=0;$folderIdBytes | select -skip 23 -First 24 | % {$indexIdBytes[$indexIdIdx++]=$nibbler[$_ -shr 4];$indexIdBytes[$indexIdIdx++]=$nibbler[$_ -band 0xF]}; $folderIDusable =$encoding.GetString($indexIdBytes); $folderIDusable}}

This somewhat fancier version of the query also sorts descending by size:

$userFolders = Get-MailboxFolderStatistics $user -IncludeOldestAndNewestItems | ? {$_.ItemsInFolderAndSubfolders -gt 0} | Select-Object @{name="Location";Expression={$_.Identity -replace "$user\\",""}},FolderandSubFolderSize,ItemsinFolderandSubfolders,@{name="Size"; Expression = {$tmp = [regex]::match($_.FolderandSubFolderSize, "\((.*)\)").Groups[1]; [uint64]$foldersize = $tmp -replace ',','' -replace ' bytes',''; $foldersize }},NewestItemReceivedDate,OldestItemReceivedDate,@{name="FolderID"; Expression = {$folderIdBytes = [Convert]::FromBase64String($_.folderId);$indexIdBytes = New-Object byte[] 48;$indexIdIdx=0;$folderIdBytes | select -skip 23 -First 24 | % {$indexIdBytes[$indexIdIdx++]=$nibbler[$_ -shr 4];$indexIdBytes[$indexIdIdx++]=$nibbler[$_ -band 0xF]}; $folderIDusable = $encoding.GetString($indexIdBytes); $folderIDusable}} | Sort-Object -Property Size -Descending
$userFolders | ogv

folders, list

We want to list a user's folders in order of biggest to smallest. We should be able to do it like this:

Get-MailboxFolderStatistics someUser | ? {$_.ItemsInFolderAndSubfolders -gt 0} | sort FolderAndSubfoldersize -Descending | Select Name,FolderandSubFolderSize,ItemsinFolderandSubfolders

But if you're in a remote session (as you likely would be if you're trying to run this on a Microsoft tenant), this will instead sort the FolderAndSubfoldersize field as if it were a text field instead of a number. You can't get an un-serialized object from a remote session. The data stream between the sessions is SOAP. This is HTTP/HTTPS, so it has to be serialized to a text stream. If you were in a local session, it would probably work.  Anyway, you'll get the FolderandSubFolderSize data in a format that looks something like this:

940.7 KB (963,235 bytes)

We need to extract the stuff in parentheses, get rid of the commas, and remove the  bytes.  The code snippet below achieves this by creating a new custom Size field which is numeric instead of character and therefore can be sorted properly:

@{name="Size"; Expression = {$tmp = [regex]::match($_.FolderandSubFolderSize, "\((.*)\)").Groups[1]; [uint64]$foldersize = $tmp -replace ',','' -replace ' bytes',''; $foldersize }}

The code above is actually 3 separate lines separated by semicolons (;) run in succession, each line feeding into the next.

1. The first section extracts stuff in between the parentheses and puts that result in the $tmp variable.

$tmp = [regex]::match($_.FolderandSubFolderSize, "\((.*)\)").Groups[1]

2. Then get rid of the commas and the ' bytes' from the $tmp variable and convert it to a number using uint64 and put that in the intermediate $foldersize variable.

[uint64]$foldersize = $tmp -replace ',','' -replace ' bytes',''

3. And then finally simply spit out the value of the $foldersize variable.

$foldersize

Here's a version that uses the code above to achieve our main goal of sorting by size (our newly created Size field), biggest first (Sort-Object -Property Size -Descending):

$folders = Get-MailboxFolderStatistics someUser -IncludeOldestAndNewestItems | ? {$_.ItemsInFolderAndSubfolders -gt 0 } | Select-Object @{name="Location";Expression={$_.Identity -replace 'someUser\\',''}},FolderandSubFolderSize,ItemsinFolderandSubfolders, @{name="Size"; Expression = {$tmp = [regex]::match($_.FolderandSubFolderSize, "\((.*)\)").Groups[1]; [uint64]$foldersize = $tmp -replace ',','' -replace ' bytes',''; $foldersize }}, NewestItemReceivedDate, OldestItemReceivedDate

This code above does a bunch of other stuff:

  • eliminates folders with no items from consideration ( ? {$_.ItemsInFolderAndSubfolders -gt 0 })
  • gets rid of the useless leading someUser\ string from the identities ( @{name=Location;Expression={$_.Identity -replace 'someUser\\',''}}) - you need two backslashes, the first to escape it from being interpreted as an escape character.
  • the date of the newest and oldest items

Then we can also:

  • convert to HTML
  • export to an HTM file

$folders | Sort-Object -Property Size -Descending | ConvertTo-HTML -Property Location,FolderandSubFolderSize,ItemsinFolderandSubfolders, NewestItemReceivedDate, OldestItemReceivedDate | Out-File -FilePath "$([environment]::GetFolderPath("mydocuments"))\someUserFolders$((Get-Date).ToString('MM-dd-yyyy_hh-mm-ss')).htm"

What if we want to delete items from a certain folder? To do that, we'll need the FolderID, which isn't immediately available. The code below also adds a column for FolderID, which is needed to use various ComplianceSearch commands on just one folder (such as deleting all emails in a folder):

$user = "someUser"
$encoding = [ System.Text.Encoding]::GetEncoding("us-ascii")
$nibbler = $encoding.GetBytes("0123456789ABCDEF");
$userFolders = Get-MailboxFolderStatistics $user -IncludeOldestAndNewestItems | ? {$_.ItemsInFolderAndSubfolders -gt 0} | Select-Object @{name="Location";Expression={$_.Identity -replace "$user\\",""}},FolderandSubFolderSize,ItemsinFolderandSubfolders,@{name="Size"; Expression = {$tmp = [regex]::match($_.FolderandSubFolderSize, "\((.*)\)").Groups[1]; [uint64]$foldersize = $tmp -replace ',','' -replace ' bytes',''; $foldersize }},NewestItemReceivedDate,OldestItemReceivedDate,@{name="FolderID"; Expression = { $folderIdBytes = [Convert]::FromBase64String($_.folderId);$indexIdBytes = New-Object byte[] 48;$indexIdIdx=0;$folderIdBytes | select -skip 23 -First 24 | % {$indexIdBytes[$indexIdIdx++]=$nibbler[$_ -shr 4];$indexIdBytes[$indexIdIdx++]=$nibbler[$_ -band 0xF]}; $folderIDusable = $encoding.GetString($indexIdBytes); $folderIDusable}} | Sort-Object -Property Size -Descending
$userFolders | ogv

-shr, by the way, shifts bits to the right and -band is a Bitwise AND

forwarded email boxes

gets forwarded instigated by users (ForwardingSmtpAddress) as well as by sysadmins (EAC: ForwardingAddress) but filters out shared mailboxes (-RecipientTypeDetails UserMailbox)

Get-Mailbox -Filter {ForwardingSmtpAddress -ne $null -or ForwardingAddress -ne $null} -RecipientTypeDetails UserMailbox | select UserPrincipalName, ForwardingAddress, ForwardingSmtpAddress, DeliverToMailboxAndForward | ogv

forwarding, different ways

There are two levels of forwarding

  • ForwardingAddress - set by an administrator and the end user has no control over it, visible through admin EAC GUI, can only forward to emails which have an email-enabled object
  • ForwardingSmtpAddress - can be set by the user in Outlook Web Access, not visible admin EAC GUI, can forward to emails which do not have an email-enabled object. Like, say, to an email outside your organization. Except that MS doesn't really allow forwarding to outside email addresses anymore without jumping through some more hoops like setting up Remote domains in Exchange Online

Display both flavors for one user:

Get-Mailbox -Identity Dr.Jekyl@Hyde.uk | select ForwardingAddress, ForwardingSmtpAddress

See Difference Between ForwardingAddress and ForwardingSMTPAddress Attributes for a more complete explanation of the difference between the two.

Find both types of forwarding for all users:

Get-Mailbox -ResultSize Unlimited | ? {($_.ForwardingAddress -ne $Null) -or ($_.ForwardingsmtpAddress -ne $Null)} | Select Name, Alias, ForwardingAddress, ForwardingsmtpAddress, DeliverToMailboxAndForward

forward an entire department to an outside domain

Let's say you want to migrate a department, all of whose members currently have email suffixes belonging to an outside domain.  You've already created users for them with email suffixes belonging to an accepted domain in your tenant. But you're not quite ready to pull the trigger to accept the domain and switch over the MX records.  You want any mail routed to the newly created inside domain suffixed email to forward to their current outside domain suffixed email.

Populate a variable of users who belong to the department

$outsideDomain = Get-AzureADUser -Filter "Department eq 'outsideDomain'"

We can't forward a users' email directly to a domain that's not currenlty accepted to our tenant.  So, create similarly named contacts.  Tack on their department name to these contacts' names so they don't duplicate our users.

$outsideDomain | %{New-MailContact -Name "$($_.DisplayName) outsideDomain" -ExternalEmailAddress "$($_.MailNickName)@outsideDomain.com"}

Now that the mirrored external contacts have been created, we can now forward each user in this department to his own external contact.

$outsideDomain | %{Set-Mailbox -Identity $_.MailNickName -ForwardingAddress "$($_.UserPrincipalName.split("@")[0])@outsideDomain.com" -DeliverToMailboxAndForward $false}

Did we make sure our users can't be seen in the GAL? We don't want our existing users, which aren't quite ready for prime time, to show up alongside contacts we just created.

$outsideDomain | %{Set-Mailbox -Identity $_.MailNickName -HiddenFromAddressListsEnabled $true}

ForwardingAddress - administrator sets, can only forward to emails which have an email-enabled object - see also forwarding, different ways

find for one user

Get-Mailbox -Identity Darth.Vader@Deathstar.com | select ForwardingAddress

clear for one user

Set-Mailbox Darth.Vader -ForwardingAddress $NULL

find all instances

Get-Mailbox -ResultSize Unlimited | ? {$_.ForwardingAddress -ne $Null}

ForwardingSmtpAddress - user sets, not visible through EAC, can forward to emails which do not have an email-enabled object - see also forwarding, different ways

find for one user

Get-Mailbox -Identity Darth.Vader@Deathstar.com | select ForwardingSmtpAddress

clear for one user

Set-Mailbox Darth.Vader -ForwardingSmtpAddress $NULL

find all instances

Get-Mailbox -ResultSize Unlimited | ? {$_.ForwardingsmtpAddress -ne $Null}

FullAccess delegated, to which users does a particular user have this access? - see delegated mailboxes that a user has FullAccess

–G–

GAL - see Global Address List (GAL) (or Offline Address Book / OAB), suppress entries

allow guest IDs to show up in the GAL - see Guest ID, show in GAL

Get-Mailbox no longer works – the new MSGraph version is Get-MgUser but first must Connect-MgGraph all by itself with no arguments.

Get-MgUser -ConsistencyLevel eventual -Search "DisplayName:HealthMailbox"

Get-Mailbox try/catch failure - see Get-Mailbox try/catch failure

Get-MailboxStatistics

individual

Get-MailboxStatistics -Identity "someUser@yourTenant.com"

By default, this command also gives the LastLogonTime but it does not give what we care most about: how big the dang box actually is. So make sure that shows up in the output, too.

Get-MailboxStatistics -Identity "someUser@yourTenant.com" | ft DisplayName, ItemCount, TotalItemSize

for a domain

Get-Mailbox *yourDomain.com | Select -Expand UserPrincipalName | Get-MailboxStatistics | Select DisplayName, @{name="TotalItemSize (MB)"; expression={[math]::Round(($_.TotalItemSize.ToString().Split("(")[1].Split(" ")[0].Replace(",","")/1MB),2)}}, ItemCount | Export-Csv -Path "$([environment]::GetFolderPath("mydocuments"))\fileBaseName$((Get-Date).ToString('MM-dd-yyyy_hh-mm-ss')).csv" -NoTypeInformation -Encoding UTF8

If you don't include the Select -Expand UserPrincipalName above, you might get an error related to duplicate names:

The specified mailbox Some User isn't unique.

another, somewhat more cumbersome way to get around the error above:

Get-MailboxStatistics -Identity "$((Get-Mailbox -identity someUser@yourDomain.com).ExchangeGuid)" | Format-Table DisplayName, TotalItemSize, ItemCount -Autosize

for the whole tenant

$allMailboxes = Get-Mailbox | Select -Expand UserPrincipalName | Get-MailboxStatistics | Select DisplayName, @{name="TotalItemSize (MB)"; expression={[math]::Round(($_.TotalItemSize.ToString().Split("(")[1].Split(" ")[0].Replace(",","")/1MB),2)}}, ItemCount | export-csv "MailboxSizes.csv" -NoTypeInformation -Encoding UTF8

followed by this to see the biggest ones at the top:

$allMailboxes | select DisplayName, "TotalItemSize (MB)", ItemCount | sort "TotalItemSize (MB)" -Descending

for a few people

$users = @("small@fry.com","big@cheese.com","neverCleansOut@DeletedItems.com")
$checkMailboxSize = $users | % {
    $stats = Get-MailboxStatistics -Identity $_
    New-Object -TypeName PSObject -Property @{
        ID = $_
        DisplayName = $stats.DisplayName
        ItemCount = $stats.ItemCount
        TotalItemSize = $stats.TotalItemSize
        LastLogonTime = $stats.LastLogonTime}}
$checkMailboxSize | Export-Csv -Path "$([environment]::GetFolderPath("mydocuments"))\checkMailboxSize$((Get-Date).ToString('MM-dd-yyyy_hh-mm-ss')).csv"-NoTypeInformation -Encoding UTF8

Get-TransportRule

Get-TransportRule | sort Guid | select Identity, Name, Guid

Global Address List (GAL) (or Offline Address Book / OAB), suppress entries - the key is either:

  • the value of their local AD attribute: “msExchHideFromAddressLists
  • the value of their O365 (Azure AD) attribute: “HiddenFromAddressListsEnabled

Cloud only (not local AD)

This first section assumes that you want to change this attribute for cloud-only IDs that are not synced with local AD.

To list mailboxes showing the status of this attribute

Get-Mailbox -ResultSize Unlimited | Sort-Object HiddenFromAddressListsEnabled,displayName | ft identity,displayName,HiddenFromAddressListsEnabled

list individuals whose status is false or null

Get-Mailbox -ResultSize Unlimited | ? {($_.HiddenFromAddressListsEnabled -eq $true) -or ($_.HiddenFromAddressListsEnabled -eq $null)} | ft identity,displayName,HiddenFromAddressListsEnabled

how to change this attribute for an individual

If a user has an email license, the following two commands will work to find …

Get-Mailbox -Identity someuser@yourTenant.onmicrosoft.com | ft identity,displayName,HiddenFromAddressListsEnabled

…and remove them from showing up in the GAL or OAB

Set-Mailbox -Identity someuser@yourTenant.onmicrosoft.com -HiddenFromAddressListsEnabled $true

 

Bulk method #1: Set-MailUser

But what if these users don't have an email license but have that annoying HiddenFromAddressListsEnabled set to $false (the default)?   They'll still show up in the GAL and you can't get at them using the Get-Mailbox command as we do above!  This comes up if we had a user that was synced with local AD and an email license, deleted him, and then restored him and take away his email license. We do this, for instance, if we move him from one tenant to another but decide to let him hang around in some capacity with no email license but perhaps a SharePoint license. Assume that we only care about real emails and not emails ending with *.onmicrosoft.com.  This command all by itself finds them:

Get-MailUser -ResultSize unlimited | ? {($_.UserPrincipalName -notlike '*onmicrosoft.com') -and ($_.HiddenFromAddressListsEnabled -eq $False)}

And then this following command goes one step further to gets rid of the offending HiddenFromAddressListsEnabled by setting it to true.

Get-MailUser -ResultSize unlimited | ? {($_.UserPrincipalName -notlike '*onmicrosoft.com') -and ($_.HiddenFromAddressListsEnabled -eq $False)} | ForEach-Object {Set-MailUser $_.userprincipalname -HiddenFromAddressListsEnabled $true}

or if you also want to target a certain display name prefix like "departed":

$departedNotHidden = Get-Mailbox -filter {HiddenFromAddressListsEnabled -eq $False} -ResultSize unlimited | ? {$_.UserPrincipalName -notlike '*onmicrosoft.com' -and $_.DisplayName -like "departed*"}
$departedNotHidden.Count
$departedNotHidden | select DisplayName, primarySMTPAddress | ogv
$i=0
$departedNotHidden | % {$i++; "$i of $($departedNotHidden.Count)"; Set-Mailbox -identity $_.identity -HiddenFromAddressListsEnabled $true}

this will also list progress if you have a bunch to process

 

Bulk method #2: Set-Mailbox

Sometimes, even though these users don't have a license, using the Set-Mailbox command instead of Set-MailUser works anyway:

$onmicrosoftUsersNotHidden = Get-Mailbox *onmicrosoft.com -filter {HiddenFromAddressListsEnabled -eq $False}

Note that, unlike other commands, for Get-Mailbox it seems that using Where in a pipe after the initial command won't filter properly. Instead, you must apply the filter immediately after the Get-Mailbox with a simple wildcard - "*onmicrosoft.com" in this case

Optional: make sure we have the right users before actually applying our changes:

$onmicrosoftUsersNotHidden | ft userPrincipalName,displayName,HiddenFromAddressListsEnabled

Now proceed to actually do what we set out to do: hide these users from showing up in the GAL

$onmicrosoftUsersNotHidden | % {Set-Mailbox -identity $_.identity -HiddenFromAddressListsEnabled $true}


what if we only have a bunch of UserPrincipalNames - perhaps of departed users who might once have had a mailbox but now may or may not?

$departed = $users | ? {$_.DisplayName -like "departed*"}
$departed.Count # 105
 
$i = 0
foreach ($user in $departed) {
  $i++
  $exist = [bool](Get-mailbox $user.UserPrincipalName -ErrorAction SilentlyContinue)
  if ($exist) {
    if ((Get-Mailbox -Identity $user.UserPrincipalName).HiddenFromAddressListsEnabled) {
      Write-Color -T "$i of $($departed.count): $($user.DisplayName) / $($user.UserPrincipalName) ", "is hidden" -C Blue, Green -B Black, Black
    }
    else {
      Write-Color -T "$i of $($departed.count): $($user.DisplayName) / $($user.UserPrincipalName) ", "is not hidden" -C Blue, Red -B Black, DarkYellow
      Set-Mailbox -Identity $user.UserPrincipalName -HiddenFromAddressListsEnabled $true
    }
  }
  else {
    Write-Color -T "$i of $($departed.count): $($user.DisplayName) / $($user.UserPrincipalName) ", "has no mailbox" -C Cyan, Yellow -B Black, Black
  }
}

I've never been able to get try/catch to work properly with Get-Mailbox failure. So, I resort to the somewhat clunky approach of separately checking whether the mailbox exists in the first place by populating the Get-Mailbox variable above. And I'm not the only one.


local AD users (not cloud-only)

$DepartingUserIdentity = "someUser";

Set-ADUser -identity $DepartingUserIdentity -Add @{msExchHideFromAddressLists = $True}

The command above with the -Add parameter will work if the original value for HiddenFromAddressListsEnabled is "null"; otherwise, use -Replace.

If you have a bunch of users with a common string, like "departed" in this example below, for whom you want to find which of these show in the GAL and to stop all of those from showing, you can set for all of them:

Get-ADUser -Filter * -Properties msExchHideFromAddressLists | ? {$_.Name -like"departed*" -and ($_.msExchHideFromAddressLists -eq $False -or $_.msExchHideFromAddressLists -eq $null)} | % {
     if ($_.msExchHideFromAddressLists -eq $null) {Set-ADUser -identity $_.ObjectGUID -add @{msExchHideFromAddressLists = $True}}
     else {(Set-ADUser -identity $_.ObjectGUID -Replace @{msExchHideFromAddressLists = $True})}
}

If the value is "null" or "true", it'll show up in the GAL either way. So in order to remove all such instances of either type, we have to look for both and replace or add accordingly. We use "replace" instead of "add" above if the value is not null.

change for guests - see Guest ID, show in GAL

guest ID, add to distribution group - see distribution group, add members

guest IDs, add recently added to distribution group

Start by finding all guest IDs

$guests = Get-AzureADUser -All $true -Filter "userType eq 'Guest'"

This assumes you just added a bunch of guest IDs and haven't added any since. Choose a date and time just before you added your guest IDs.

$recentGuests = $guests | ? {$_.RefreshTokensValidFromDateTime -gt "11/24/21 4 pm"}
$recentGuests | % {Add-DistributionGroupMember -Identity "Employees Masters of the Universe" -Member $_.UserPrincipalName}

guest ID, delegate of shared mailbox

Let’s say we want 2 folks from our outside vendor added to the debug email shared box.  Turns out, there’s a way to add them to that shared mailbox.  But because they’re not inside the organization, there’s no way they’ll ever be able to see any such emails.  That is, as far as I know, you must be a licensed user inside the tenant in order to be able to see any emails to which you are a delegate and guest users are, by definition, outside the tenant.  So, going down that path is an exercise in futility!

The only way I can get guest users to be able to see emails sent to the shared mailbox is to forward everything sent to that shared box to a distribution list.  I had to do the following things:

Add the WindowsEmail attribute to the guest ID.  By default, guest IDs you add via the Azure AD interface don’t have the WindowsEmail attribute filled in.  To list all our guest IDs and whether or not they have their WindowsEmail attribute filled in, run this:

Get-User -ResultSize unlimited -RecipientTypeDetails GuestMailUser | Get-MailUser | Select-Object DisplayName, name, UserPrincipalName, HiddenFromAddressListsEnabled, WindowsEmailAddress | Sort-Object DisplayName | ogv

This also shows whether or not they will show up in the GAL (HiddenFromAddressListsEnabled = True), but that’s not especially important if all you want to do is make sure they can receive an email.  Then, to fill in the WindowsEmailAddress attribute:

Get-User -ResultSize unlimited | ? {$_.Name -eq "bobSmith_someCompany.com#EXT#"} | Set-MailUser -WindowsEmailAddress "bobSmith@someCompany.com" -HiddenFromAddressListsEnabled $false

Again, that part about setting HiddenFromAddressListsEnabled = True is optional.

Now that you’ve set WindowsEmail attributes for these users, create a new cloud-only distribution group that includes those guest IDs. Note: I don't think anything from here on out really needs to be done via PowerShell; you should be able to do all of this through the regular O365 GUI you use to create groups.

New-DistributionGroup -Name "someCompany Debug" -Members bobSmith@some-Company.com,samSnead@some-company.com,someUser@yourDomain.com

Notice this command

  • creates the distribution group and
  • adds a couple outside users
  • adds inside user

all in one fell swoop.  Later I added another outside user

Add-DistributionGroupMember -Identity "someCompany Debug" -Member "someUser@someForeignTenant.onmicrosoft.com"

As stated above, you can also add such outside distribution group members through the GUI (not the Exchange GUI but the regular group management GUI in O365).

Once you're done with all this, then simply forward everything in this shared mailbox to the new distribution group you created.  I've only ever done this through the GUI.

guest ID, invite from CSV

$teamsurl = "https://MastersOfTheUniverse.sharepoint.com/"
$messageInfo = New-Object Microsoft.Open.MSGraph.Model.InvitedUserMessageInfo
$messageInfo.customizedMessageBody = ("Please accept the following invitation to be able to share Outlook calendars").ToString()
$dir = [environment]::getfolderpath("mydocuments")
$originalFileName = "$($dir)/GuestUsers.csv"
$csv = Import-Csv $originalFileName
$i = 0
Foreach ($invitee in $csv){
    $i++
    Write-Host "$i of $($csv.Count): $($invitee.Name) - $($invitee.email)" -ForegroundColor "green"
    New-AzureADMSInvitation -InvitedUserEmailAddress $invitee.email -InvitedUserDisplayName $invitee.Name -InviteRedirectUrl $teamsurl -InvitedUserMessageInfo $messageInfo -SendInvitationMessage $true
}

I usually follow this with guest IDs, add recently added to distribution group

guest ID invitations, resend

$guests = Get-AzureADUser -All $True -Filter "userType eq 'Guest'" | Select-Object DisplayName, department, UserPrincipalName, mail, UserState, @{n="Dom";e={$_.UserPrincipalName.Split("#")[0].Split("_")[1]}}, @{n="created";e={$_.ExtensionProperty.createdDateTime}}

Presumably these are recent, perhaps all on one day. I've never been able to directly filter on date created. At least, not using Get-AzureADUser's .ExtensionProperty.createdDateTime property. So, instead I resort to searching on a string instead

$RecentGuests = $guests | ? {$_.created -like "11/3*"}
$RecentAccepted = $RecentGuests | ? {$_.UserState -eq "Accepted"}
$RecentNotAccepted = $RecentGuests | ? {$_.UserState -eq "PendingAcceptance"}
$RecentAccepted.Count # 20
$RecentNotAccepted.count # 31
$teamsurl = "https://yourtenant.sharepoint.com/sites/AuditorMeetings/Shared Documents/General"
$messageInfo = New-Object Microsoft.Open.MSGraph.Model.InvitedUserMessageInfo
$messageInfo.customizedMessageBody = ("Good Afternoon  Auditors, please accept the following invitation to be a part of the Auditor Teams group").ToString()
foreach ($invitee in $RecentNotAccepted) {New-AzureADMSInvitation -InvitedUserEmailAddress $invitee.Mail -InvitedUserDisplayName $invitee.DisplayName -InviteRedirectUrl $teamsurl -InvitedUserMessageInfo $messageInfo -SendInvitationMessage $true}

guest IDs, list - see also guest IDs, list all for domain (regardless of WindowsEmailAddress, WindowsEmailAddress, add/set, List WindowsEmailAddress by Domain, List WindowsEmailAddress for Domain

Three ways:

  • Get-User
  • Get-AzureADUser
  • Get-MailUser

Get-User

can include

  • HiddenFromAddressListsEnabled
  • WindowsEmailAddress

can not include

  • Department
  • whether user has accepted invitation (UserState)
  • Custom Attributes

This also gets the email domain, which is handy to be able to sort on

Get-User -RecipientTypeDetails GuestMailUser | Get-MailUser | Select-Object DisplayName, name, UserPrincipalName, HiddenFromAddressListsEnabled, WindowsEmailAddress, @{n="Dom";e={$_.UserPrincipalName.Split("#")[0].Split("_")[1]}}

Get-AzureADUser

can include

  • Department
  • whether user has accepted invitation (UserState)

can not include

  • HiddenFromAddressListsEnabled
  • WindowsEmailAddress
  • Custom Attributes

Use

Get-AzureADUser -All $True -Filter "userType eq 'Guest'" | Select-Object DisplayName, department, UserPrincipalName | Sort-Object Department, DisplayName | ogv

or

Get-AzureADUser -All:$true -Filter "userType eq 'Guest'"

For one user

Get-AzureADUser -ObjectId "Tyranosaurus.Rex_fossils.com#EXT#@dinosaurs.onmicrosoft.com" | Select DisplayName, UserPrincipalName, RefreshTokensValidFromDateTime, UserState

What if we don't know the identity?

Get-AzureADUser -SearchString "elvis"

This gets more useful fields such as email domain, whether user has accepted invitation (UserState) and when the ID was created

$guestUsersAD = Get-AzureADUser -All $True -Filter "userType eq 'Guest'" | Select-Object DisplayName, department, UserPrincipalName, mail, UserState, @{n="Dom";e={$_.UserPrincipalName.Split("#")[0].Split("_")[1]}}, @{n="created";e={$_.ExtensionProperty.createdDateTime}} | Sort-Object Department, DisplayName

Get-MailUser

can include

  • HiddenFromAddressListsEnabled
  • WindowsEmailAddress
  • Custom Attributes

can not include

  • Department
  • whether user has accepted invitation (UserState)

Get-MailUser -ResultSize Unlimited -Filter ("RecipientTypeDetails -eq 'GuestMailUser'")

guest IDs, list all for domain - specifically, the domain implied by their UserPrincipalName, not the domain specified by their WindowsEmailAddress

You can not combine the -Filter "userType eq 'Guest'" and -SearchString "someDomain" arguments in the same command because fail with Parameter set cannot be resolved using the specified named parameters:

Get-AzureADUser -All:$true -Filter "userType eq 'Guest'" -SearchString "protiviti"

So, separate out into two commands:

$guests = Get-AzureADUser -All:$true -Filter "userType eq 'Guest'"
$guests | ? {$_.UserPrincipalName -like '*someDomain*'} | select DisplayName, UserPrincipalName

guest IDs, replace contacts with - see contacts, delete and replace with guest users

guest ID, show in GAL

set

Get-User -RecipientTypeDetails GuestMailUser | ? {$_.Name -eq "someExternalUser_someDomain.com#EXT#"} | Set-MailUser -HiddenFromAddressListsEnabled $false

the guest user will now show up in the GAL, but you still won't be able to mail to him. If you look for his email in the GAL, it will be blank?  Why?  The whole process of getting an external guest user set up involves sending him an email and having him use his email credentials to log in. His email sure ought to be in there somewhere.  And, indeed, it is.  But not in the right place yet. That's because, even though he has emails plugged into all sorts of properties, you need to add his email to one very particular property: WindowsEmailAddress

Get-User -RecipientTypeDetails GuestMailUser | ? {$_.Name -eq "someExternalUser_someDomain#EXT#"} | Set-MailUser -WindowsEmailAddress "someExternalUser@someDomain.com"

So, you'll want to do both:

Get-User -RecipientTypeDetails GuestMailUser | ? {$_.Name -eq "someExternalUser_someDomain#EXT#"} | Set-MailUser -WindowsEmailAddress "someExternalUser@someDomain.com" -HiddenFromAddressListsEnabled $false

verify

Get-MailUser "someExternalUser_someDomain.com#EXT#" | FL Name, WindowsEmailAddress, HiddenFromAddressListsEnabled

GUID for a mailbox

Get-Mailbox -identity someuser | select DisplayName, GUID, ExchangeGUID

–H–

hide email from showing up in GAL - see Global Address List (GAL), suppress entries

hidden emails in a folder, how many?

this will get hidden items along with visible items and all items for the inbox and any subfolders in the inbox which have hidden folders

Get-EXOMailboxFolderStatistics -Identity JackDawkins@ArtfulDodger.uk -FolderScope Inbox | ? {$_.HiddenItemsInFolder -gt 0}| select FolderPath, VisibleItemsInFolder, HiddenItemsInFolder, ItemsInFolder

or just for the hidden items in the inbox root folder

(Get-EXOMailboxFolderStatistics -Identity JackDawkins@ArtfulDodger.uk -FolderScope Inbox)[0].HiddenItemsInFolder

HiddenFromAddressListsEnabled - this cloud attribute is equivalent to msExchHideFromAddressLists in local AD

What it's set to:

Get-Mailbox vincent@price.com | Select-Object displayName, HiddenFromAddressListsEnabled

For guest IDs, a little different

Get-User -ResultSize unlimited | ? {$_.Name -eq "vincent_price.onmicrosoft.com#EXT#"} | Get-MailUser | Select-Object DisplayName, name, UserPrincipalName, HiddenFromAddressListsEnabled, WindowsEmailAddress

To change it:

Set-Mailbox vincent@price.com -HiddenFromAddressListsEnabled $true

or:

Get-Mailbox vincent@price.com | Set-Mailbox -HiddenFromAddressListsEnabled $true

change for guests - see Guest ID, show in GAL

Or, if you want a sneak preview for guests:

Get-User -RecipientTypeDetails GuestMailUser | ? {$_.Name -eq "vincent_price.onmicrosoft.com#EXT#"} | Set-MailUser -WindowsEmailAddress "someExternalUser@yourTenant.onmicrosoft.com" -HiddenFromAddressListsEnabled $false

–I–

–J–

JSON, deal with - see audit log, find rule creations for an example

junk email, trusted recipients/domains

find

Get-MailboxJunkEmailConfiguration -Identity dirt.poor | fl Trusted*

The command above doesn't format it very well because that variable is an array that may contain way more than what can be displayed and it'll just end with ellipses and trail off. This might work.

(Get-MailboxJunkEmailConfiguration -Identity dirt.poor@DogPatch.com).TrustedRecipientsAndDomains

But usually I find I must first assign to a variable…

$TrustedInquiries = Get-MailboxJunkEmailConfiguration -Identity dirt.poor@DogPatch.com

… and then display:

$TrustedInquiries.TrustedRecipientsAndDomains | sort

if there are a whole pile already and you want to narrow it down a bit (and you don't want to bother populating a variable - again, this sometimes returns nothing even though there are valid results):

(Get-MailboxJunkEmailConfiguration -Identity dirt.poor@DogPatch.com).TrustedRecipientsAndDomains | ? {$_ -like "y*"}

or, if you've populated a variable:

$TrustedInquiries.TrustedRecipientsAndDomains | ? {$_ -like "y*"}

add

Set-MailboxJunkEmailConfiguration dirt.poor -TrustedRecipientsAndDomains @{Add="youWon@NigerianPrince.com"}

didn't work for me. Sometimes using the name instead of the email seems to work better

Set-MailboxJunkEmailConfiguration "Dirt Poor" -TrustedRecipientsAndDomains @{Add="youWon@NigerianPrince.com"}

remove

Set-MailboxJunkEmailConfiguration dirt.poor -TrustedRecipientsAndDomains @{Remove="youWon@NigerianPrince.com"}

unlike add, remove sometimes seems to work OK

Overall, both the add and remove commands just seem to be flaky and not work consistently.

–K–

kiosk mailbox, filter out from list of mailboxes - see mailbox types, filter out types

–L–

language zone bulk change

set all mailboxes in our Office 365 Tenant to German language

Get-mailbox -ResultSize unlimited | Set-MailboxRegionalConfiguration -Language 1031 -TimeZone "W. Europe Standard Time" -LocalizeDefaultFolderName

as well as set time zone to W. Europe and change all of the default folders

last mailbox login time - see Get-MailboxStatistics

Get-MailboxStatistics -Identity "rip@vanWinkle.com" | ft DisplayName, LastLogonTime

or

(Get-MailboxStatistics -Identity rip@vanWinkle.com).LastLogonTime

licenses on shared mailboxes - see shared mailboxes with licenses

list mailboxes, see mailboxes, list

list all WindowsEmailAddress for a particular domain, sorted by the domain of their WindowsEmailAddress - see Windows Email Address, list all guest users For domain, Windows Email Address, list all guest users By domain

litigation hold (eDiscovery)

Set-Mailbox mailbox@yourtenant.com -LitigationHoldEnabled $true -LitigationHoldDuration 365

login time, most recent for mailbox - Get-MailboxStatistics

Get-MailboxStatistics -Identity "someUser@yourDomain.com" | ft DisplayName, LastLogonTime

–M–

mailbox, create

$UPN = "noreply@yourDomain.de"
$LO = "DE"
New-Mailbox -Alias noreply -Name noreply -Firstname noreply -LastName noreply -DisplayName "noreply" -MicrosoftOnlineServicesID $UPN -Password ( ConvertTo-SecureString -String 'topSecret' -AsPlainText -Force) -ResetPasswordOnNextLogon $true

Need to assign a location before we can assign a license. But we have to wait a bit after creation before we can set a location. So wait a few seconds.

start-sleep -s 20
set-msoluser -userprincipalname $UPN -UsageLocation $LO

Now assign a license. But we have to wait a bit after setting location before we can assign a license. So wait a few seconds.

start-sleep -s 20
Set-MsolUserLicense -UserPrincipalName $UPN -AddLicenses "yourTenant:EXCHANGESTANDARD_DE"

mailbox full - see Find folder ID for all of a user's folders, sort decending by size

mailbox exists?

$mailboxID = "007@M16.com"
if (Get-Mailbox $mailboxID -ErrorAction SilentlyContinue) {"$mailboxID exists"} else {"$mailboxID does NOT exist"}

or if you have a bunch in an array:

$lookup = @("lone@ranger.com", "richard@nixon.com", "spider@man.com")
$lookup | % {if (Get-Mailbox $_ -ErrorAction SilentlyContinue) {Write-Host "$_ exists" -ForegroundColor Green} else {Write-Host "$_ does NOT exist" -ForegroundColor Red}}

mailbox types, filter out types

filter out:

  • Shared mailboxes
  • Rooms (like conference rooms)
  • Disabled accounts
  • Kiosk accounts
  • Addresses that we deliberately omit from the GAL

Get-Mailbox -ErrorAction SilentlyContinue -identity $_.UserPrincipalName -Filter {(-not(RecipientTypeDetailsValue -eq 'SharedMailbox')) -and (-not(RecipientTypeDetailsValue -eq 'RoomMailbox')) -and (-not(RecipientTypeDetailsValue -eq 'EquipmentMailbox')) -and (ExchangeUserAccountControl -ne 'AccountDisabled') -and (HiddenFromAddressListsEnabled -eq $false) -and (MailboxPlan -notlike "ExchangeOnlineDeskless*")} | Sort-Object MailboxPlan, Identity | Select-Object DisplayName, WindowsEmailAddress, MailboxPlan | Export-Csv "EmailList.csv" -NoTypeInformation -Encoding UTF8

I could not filter out Kiosks.  When I try to filter, you always get 0 records.  But you can sort.  So, sort, include that value in the display & lop off bottom records.

mailbox size - see Get-MailboxStatistics

mailboxes, list

This also sorts by domain

Get-Mailbox | Select-Object @{n="Dom";e={$_.UserPrincipalName.split("@")[1]}}, displayName, userprincipalname | Sort-Object dom, displayName

mailboxes, sort by domain

Get-Mailbox | Select-Object @{n="Dom";e={$_.UserPrincipalName.split("@")[1]}}, displayName, UserPrincipalName | Sort-Object dom, displayName

mailboxes which are deleted, list - see deleted mailboxes, list

mail user, create

This is useful if you created an external email as a member

New-MailUser -Name "Some User" -ExternalEmailAddress someUser@externalDomain.com -MicrosoftOnlineServicesID someUser@yourTenant.onmicrosoft.com -Password (ConvertTo-SecureString -String 'P@ssw0rd1' -AsPlainText -Force)

mail user email address, remove - see email addresses, remove

mail user proxy address, remove - see email addresses, remove

MAPIEnabled must be enabled on shared mailbox for delegated users to see this mailbox in their Outlook

which haven't been enabled (and which have the string "Pumpkin*" in the DisplayName)

Get-EXOCASMailbox -filter 'MAPIEnabled -eq $false' | ? {$_.DisplayName -like "Pumpkin*"} | ft Identity, PrimarySmtpAddress, DisplayName, MAPIEnabled

To set those whose display name matches the string

Get-EXOCASMailbox -filter 'MAPIEnabled -eq $false' | ? {$_.DisplayName -like "Pumpkin*" } | % {Set-CASMailbox -Identity $_.Identity -MAPIEnabled $True}

management role assignments for a user - see role assignments, to what roles is a user assigned?

message trace - see trace message

members, add

to a distribution group

to an Azure security group - see security group, add members

to an email-enabled security groups - see distribution group, add members

most recent mailbox login time - see Get-MailboxStatistics

Get-MailboxStatistics -Identity "someUser@yourDomain.com" | ft DisplayName, LastLogonTime

msExchHideFromAddressLists - this local AD attribute is equivalent to HiddenFromAddressListsEnabled in the cloud

multi-factor authentication errors when trying Connect-ExchangeOnline - see Connect-ExchangeOnline error (MFA)

multi-factor authentication (MFA)

If you encounter, “Error AADSTS50076: Due to a configuration change made by your administrator, or because you moved to a new location, you must use multi-factor authentication to access” error when running:

Connect-ExchangeOnline -Credential $cred

check any conditional access policies that enforce MFA. You may need to exclude this user.

–N–

notification (will a user get notified in his email when a calendar invite comes along wanting to modify his calendar?) - see calendar notifications

–O–

Off-line Address Book (OAB), suppress entries from - see Global Address List (GAL), suppress entries

out of office message, see status

Get-Mailbox someUser@yourDomain.com | Get-MailboxAutoReplyConfiguration | select AutoReplyState, ExternalMessage, InternalMessage | fl

out of office message, specify

Initialize some variables

$message = "Hi. Thank you for your email. Ferris Bueller has gone AWOL.
Please update your records with our principal's email address: principal@HighSchool.com "
$EmployeeDetails = Get-ADUser Ferris.Bueller -properties *

Run the command

Set-MailboxAutoReplyConfiguration -Identity $EmployeeDetails.Mail -AutoReplyState enabled -ExternalAudience all -InternalMessage $message -ExternalMessage $message

Verify results

Get-MailboxAutoReplyConfiguration -Identity $EmployeeDetails.Mail

By default, the time period appears to set the StartTime to 9 am of the day you run this command and EndTime to 9 am the next day. To use either of these parameters, the AutoReplyState parameter must be set to Scheduled

out of office message, turn off

Set-MailboxAutoReplyConfiguration -Identity Ferris.Bueller@HighSchool.com -AutoReplyState disabled

I sometimes like to go a step further and also clear out messages

Set-MailboxAutoReplyConfiguration -Identity Ferris.Bueller@HighSchool.com -AutoReplyState disabled -InternalMessage "" -ExternalMessage ""

Outlook doesn't display shared mailbox. Or if the mailbox itself does show the shared mailbox, there are no emails there in the mailbox.  See delegates don't show up as expected in Outlook

–p–

permissions, assign mailbox permissions/delegation of one user to another user - see also delegate a mailbox to another user

The command below will give the user (perhaps a sysadmin) access to all mailboxes. The “Automapping $false” means that, even though the user will have permissions/be a delegate, the other peoples' mailboxes will not automatically show up in his Outlook

Get-Mailbox -ResultSize Unlimited | Add-MailboxPermission -AccessRights FullAccess -Automapping $false -User someuser@yourdomain.com

To give just one delegated user access to one source user (and also make sure that the other person's mailbox will automatically show up in his Outlook):

Get-Mailbox "sourceUser@yourDomain.com" | Add-MailboxPermission -AccessRights FullAccess -Automapping $true -User "targetUser@yourDomain.com"

or simpler:

Add-MailboxPermission -Identity sourceUser -AccessRights FullAccess -Automapping $true -User "targetUser@yourDomain.com"

Unlike the full access delegation above, you can't add SendAs permission with UserPrincipalName.   Instead, you have to apply the SendAs permission using Identity.

$DepartingUserIdentity = "sourceUser";
$DelegatedUserIdentity ="delegatedUser";
Add-RecipientPermission $DepartingUserIdentity -AccessRights SendAs -Trustee $DelegatedUserIdentity -Confirm:$False

PowerShell, disable remote - see RemotePowerShellEnabled, disable

preparing a mailbox for this user

After assigning the licenses to the user in office 365, the user is stuck with the message, "We are preparing a mailbox for this user".  Verify:

Get-MsolUser -UserPrincipalName someUser@yourDomain.com | ft DisplayName, UserPrincipalName, OverallProvisioningStatus

or

(Get-MsolUser -UserPrincipalName someUser@yourDomain.com).Licenses[0].ServiceStatus

If either of the commands above return "OverallProvisioningStatus" of "PendingInput", the "PendingInput" status means that nothing has been provisioned (yet) for this customer. If this doesn't clear up soon, you may have a problem.  According to here:

  1. Remove the existing licenses to the Office 365 User in office 365 portal or power shell and Wait for 24 hours to De-provision the resources in the target system.
  2. Re-Assign the Licenses to the user in Office 365 portal or Power Shell.
  3. After assigning the licenses to the user, the issue will be resolved.

primary SMTP, count of all users' for a tenant

first, get list of all domains for a tenant

Get-MsolDomain | sort name | select Name

next, see which of those domains are being used for how many users' primary SMTP

$mailUsers = Get-Mailbox -Resultsize Unlimited | select DisplayName, PrimarySMTPAddress, @{n="Dom";e={$_.PrimarySMTPAddress.split("@")[1]}}
$mailUsers | group dom | sort Name | select Name, Count

from here, you can visually compare to look for gaps (domains which no users use as their primary SMTP)

proxyAddresses, add or delete

user

$OldToDelete = "SMTP:" + $identity + "@" + $TenantDomain
$NewToAdd= "smtp:" + $identity + "@" + $TenantDomain
Set-Mailbox -Identity $identity -EmailAddresses @{Add = $NewToAdd; remove = $OldToDelete}

can not change proxyAddresses using Set-MsolUser

add proxyAddresses for contacts - see contacts, proxyAddresses add

proxyAddresses, find for an individual

local AD

(Get-ADUser SomeUser -Properties proxyAddresses).proxyAddresses

AAD

(Get-MsolUser -UserPrincipalName SomeUser@yourDomain.com).proxyAddresses

proxyAddresses, find match

Get-MsolUser -All | where-Object {$_.ProxyAddresses -match "someaddress" } | fl

or the following gives a little more assistance in spotting the suspected match. Since were only finding matches and not specifying the whole string, this helps in cases of partial match.

$m = "martin.luther@yourDomain.com"
$proxyAddressesMsolUsers = Get-MsolUser -all | where-Object {$_.ProxyAddresses -match "$m"}
if ($proxyAddressesMsolUsers) {
    foreach ($proxyAddressesMsolUser in $proxyAddressesMsolUsers) {
        Write-Host " found $($proxyAddressesMsolUser.ProxyAddresses.count) proxy addresses for $($proxyAddressesMsolUser.DisplayName) - at least one of which matches $m" -ForegroundColor Yellow
        $i=0
        foreach ($proxyAddress in $proxyAddressesMsolUser.ProxyAddresses) {
            $i++
            if ($proxyAddress -match "$m") {
                Write-Host " --> $i - $proxyAddress (matches $m)" -ForegroundColor Cyan
            }
            else {
                Write-Host "     $i - $proxyAddress (does not match $m)" -ForegroundColor White
            }
        }
    }
}

proxyAddresses for contacts - although local AD contacts have "proxyAddresses", on Office 365 this property translates to "emailAddresses" - see contacts, display proxyAddresses and targetAddress

proxyAddresses, list for a contact - see contact info (with proxyAddress),

proxyAddresses, list for a user

Get-MsolUser -UserPrincipalName someUser@yourDomain.com | Select-Object DisplayName, UserPrincipalName, proxyAddresses

or

Get-Mailbox someUser | fl EmailAddresses

also show the primary proxyAddress

Get-MSOLUser -UserPrincipalName someUser@yourDomain.com | Select userprincipalname, @{e={$_.ProxyAddresses -cmatch '^SMTP\:.*'};name='Primaryaddress'},Proxyaddresses

proxy addresses, list for users in a domain

Get-MsolUser -All | where {($_.userprincipalname -match "yourDomain.com")} | %{Get-Mailbox $_.UserPrincipalName | fl EmailAddresses}

or better

Get-MsolUser -All | where {$_.UserPrincipalName -match "yourDomain.com"} | Select UserPrincipalName, @{e={$_.ProxyAddresses -cmatch '^SMTP\:.*'};name='Primaryaddress'}, @{e={$_.ProxyAddresses -match 'onmicrosoft.com'};name='onMicroSoft'}, Proxyaddresses

the command immediately above breaks out not only the primary SmtpAddress address, but also all the “.onmicrosoft.com” proxy addresses into a separate column, which can be handy to compare on another (foreign) tenant's list of WindowsEmailAddresses for a distribution list which might closely map to IDs in this this domain. The point being: if that foreign tenants' WindowsEmailAddresses point here, there'd better be a corresponding “.onmicrosoft.com” proxy address here!

public folder, add permission

Add-PublicFolderClientPermission -Identity "\Calendars\someCalendar" -User someUser -AccessRights PublishingEditor

public folders, list permission

Get-PublicFolderClientPermission "\Calendars\someCalendar" -User someUser -AccessRights PublishingEditor

public folders, list

Get-PublicFolder -ResultSize Unlimited -Recurse

If you have any public caledars, they'll show up with a "\Calendar" parent path

–Q–

–R–

remotePowerShellEnabled, disable

Disable for one user:

Set-User -Identity someUser@yourDomain.com -RemotePowerShellEnabled $false

or

Set-User -Identity "Peter Lorre" -RemotePowerShellEnabled $false

You can use this to quickly get an idea of who all are enabled

Get-User -ResultSize unlimited -Filter {RemotePowerShellEnabled -eq $true} | Sort Name | ft -Auto Name,DisplayName,RemotePowerShellEnabled

However, below is better if you want counts and the number of displayed users to agree with each other. If instead you assign the results of the command above to a variable and then try to get count from a variable assigned to the raw Get-User statement above with the results piped to the Sort and ft portions the way it is above, that variable's count might not agree with how many display in its "ft" display. I found the count was 4 more than how many were displayed.

$usersWhoHavePowerShell = Get-User -ResultSize unlimited -Filter {RemotePowerShellEnabled -eq $true}
$usersWhoHavePowerShell.Count
$usersWhoHavePowerShell | ft

for a whole OU

Often I want to do this for a whole OU.  Although OU (Organizational Unit) is ostensibly available as an option in the Get-User and Set-User commands, it's pretty worthless: all the OUs are the same and they're tied to the tenant.  So I have to find OUs using the Get-ADUser command and then pipe the results - one at a time - to the Get-User (cloud) command.  I haven't figured out a way to only find those which are true or false.  If I try to specify that, it bombs for those individuals for which it's not true.

finds the status for all in an OU

Get-ADUser -SearchBase "OU=yourOU,DC=yourDomain,DC=com" -Filter * -SearchScope Subtree -Properties name | % {Get-User $_.Name} | Format-Table -Auto Name,DisplayName,RemotePowerShellEnabled

disables all users in an OU

Get-ADUser -SearchBase "OU=yourOU,DC=yourDomain,DC=com" -Filter * -SearchScope Subtree -Properties name | % {Get-User $_.Name | Set-User -RemotePowerShellEnabled $false}

This will bomb if you have any duplicate names

disable for all guest IDs

Get-User -RecipientTypeDetails GuestMailUser -ResultSize unlimited -Filter {RemotePowerShellEnabled -eq $true} | % {Get-User $_.Name | Set-User -RemotePowerShellEnabled $false}

remove shared (delegated) mailboxes for a user - sometimes necessary if the combined size of a user's shared mailboxes exceeds the max that Outlook can handle - see also delegate, remove

First, find out how many delegated mailboxes a user has

$UsersSharedMailboxes = Get-Mailbox | Get-MailboxPermission -user 'someUser@yourDomain.com'

see how big each of these is

$UsersSharedMailboxesStats = $UsersSharedMailboxes | % {Get-MailboxStatistics $_.Identity| Select DisplayName, @{name="TotalItemSize (MB)"; expression={[math]::Round(($_.TotalItemSize.ToString().Split("(")[1].Split(" ")[0].Replace(",","")/1MB),2)}}, ItemCount}

how big is the user's mailbox?

$UsersIndividualMailbox = Get-MailboxStatistics -Identity "someUser@yourDomain.com" | Select DisplayName, @{name="TotalItemSize (MB)"; expression={[math]::Round(($_.TotalItemSize.ToString().Split("(")[1].Split(" ")[0].Replace(",","")/1MB),2)}}, ItemCount

combine both to figure how big users + delegates

$combined = $UsersSharedMailboxesStats

$combined += $UsersIndividualMailbox

now that you've combined them, display them and see if the TotalItemSize (MB) adds up to more than 50G

$combined | ogv

choose a few to remove from Outlook

$delegates = @("delegatedUser1@yourDomain.com", "delegatedUser2@yourDomain.com", "delegatedUser3@yourDomain.com")

$delegates | %{Remove-MailboxPermission -Identity $_ -User yourUser@yourDomain.com -AccessRights FullAccess -Confirm:$false}

If you want to add them back again (only this time without them showing up in local Outlook)

$delegates | % {Add-MailboxPermission -Identity $_ -user yourUser@yourDomain.com -AccessRights FullAccess -AutoMapping:$false}

resources, set delegates - see permissions, assign mailbox permissions/delegation of one user to another user

role assignments, what roles does a user have? - see also role groups, add user to, role groups, remove user from

it may be instructive first to see all role assignments

$ManagementRoleAssignments = Get-ManagementRoleAssignment -GetEffectiveUsers

$ManagementRoleAssignments | ogv

now we can focus on a couple users

$GrizzledVeteran = $ManagementRoleAssignments | ? {$_.EffectiveUserName -eq "Obi-Wan Kenobi"}

$Acolyte = $ManagementRoleAssignments | ? {$_.EffectiveUserName -eq "Luke Skywalker"}

compare role assignments between the two

Compare-Object $GrizzledVeteran $Acolyte

role assignments, to which role groups has a role been assigned?

For example, which role groups have the "Mailbox Search" role assignment

Get-RoleGroup | ? {$_.RoleAssignments -match "Mailbox Search"}

role groups, add user to

Add-RoleGroupMember -Identity "Discovery Management" -Member "Luke Skywalker"

role groups, remove user from

Remove-RoleGroupMember -Identity "Discovery Management" -Member "Luke Skywalker"

role groups, what role assignments - see role assignments, to which role groups has a role been assigned?

room, create

Create on local AD as a regular user & sync.
Temporarily assign a license, log in as the new user using WebMail to force creation of a mailbox.
Make sure the mailbox exists:

Get-Mailbox -Identity MeetingRoom4@yourDomain.com

Convert to a room:

Set-Mailbox -identity MeetingRoom4@yourDomain.com -Type Room

Don't forget to take away the license when you're done.

rooms, set delegates - see permissions, assign mailbox permissions/delegation of one user to another user

rule, create

out of office reply

If you send a lot of emails out to where you get a bunch of them with "out of office" replies, this will route them to a separate "out of office" folder

New-InboxRule -Name AutomaticReplyToOutOfOffice -Mailbox sales -MessageTypeMatches AutomaticReply -MoveToFolder "Sales:\Inbox\Out of office"

differentiate between aliases

If we have one "payables" mailbox with several different aliases, one of which is spiderman.com, route that alias to "PayablesSpiderMan" folder

New-InboxRule -Name "payables@spiderman.com - Move& to SpiderMan" -Mailbox payables -HeaderContainsWords "payables@spiderman.com" -MoveToFolder ":\Inbox\spiderman" -StopProcessingRules $false

rule, delete

Remove-InboxRule -Mailbox moleIn@ourCompany.com -Identity "Forward top secret stuff to new company I'm going to work for!"

rule to route among aliases - see aliases, route incoming emails among various aliases using rule

rules, list for a single user

Get-InboxRule -mailbox someUser | select name, enabled, description | fl

In order to find out when rules were created, need to search the audit log. This example finds all new email rule creations on a certain day

$auditEventsForUser = Search-UnifiedAuditLog -StartDate '2020-12-14' -EndDate '2020-12-15' -UserIds [someUser] -RecordType ExchangeAdmin -Operations New-InboxRule
$ConvertedOutput = $auditEventsForUser | Select-Object -ExpandProperty AuditData | ConvertFrom-Json

$ConvertedOutput | Select-Object CreationTime,Operation,Workload,ClientIP,Parameters | ft -Wrap -a

rules, list for all users

Get-Mailbox -ResultSize Unlimited | % {Get-InboxRule -Mailbox $_.Alias | Select @{Expression={$_.SendTextMessageNotificationTo};Label="SendTextMessageNotificationTo"},@{Label = "MailboxOwner";Expression = {($_.MailboxOwnerId -Split "/")[3]}}, Name, Priority, Enabled, From , SentTo, CopyToFolder, DeleteMessage, ForwardTo, MarkAsRead, MoveToFolder, RedirectTo} | Export-Csv -Path "$([environment]::GetFolderPath("mydocuments"))\ForwardingRules$((Get-Date).ToString('MM-dd-yyyy_hh-mm-ss')).csv" -NoTypeInformation -Encoding UTF8

This does have problems with putative duplicates

–S–

security group, add members - see also distribution group, add members

This how I copied members from one group to another

Get-AzureADGroupMember -ObjectId c90ed612-d5fa-4613-b66d-d59da5cfe36b | % {Add-AzureADGroupMember -ObjectId "b359c371-3a54-4581-9329-968c45f3b1ac" -RefObjectId $_.ObjectId}

But how to get those object IDs?

Get-AzureADGroup -Filter "DisplayName eq 'Source Group'" | select DisplayName,ObjectID
Get-AzureADGroup -Filter "DisplayName eq 'Target Group'" | select DisplayName,ObjectID

SendAs permission delegation

report of all SendAs permissions (from here)

Get-RecipientPermission | ? {$_.Trustee -ne "NT AUTHORITY\SELF" -and $_.Trustee -ne "NULL SID"} | select Identity,@{n="RecipientType";e={((Get-Recipient$_.Identity -ErrorAction silentlycontinue).RecipientTypeDetails + (Get-Recipient$_.Identity -RecipientTypeDetails GroupMailbox -ErrorAction silentlycontinue).RecipientTypeDetails)}},Trustee, Access* | ft -a

check for one person

Get-Mailbox -identity mailboxToAllowPerm | Get-RecipientPermission -AccessRights SendAs -Trustee whoNeedsPerm

delete SendAs permission for every mailbox to which a user has such permission

$DepartingUserIdentity ="bob"

$departedRecipientPerm =Get-Mailbox | Get-RecipientPermission -AccessRights SendAs -Trustee $DepartingUserIdentity
$departedRecipientPerm | % {Remove-RecipientPermission $_.Identity -AccessRights SendAs -Trustee $_.User -Confirm:$False}

grant SendAs permission on a shared mailbox to a delegate's mailbox

Add-RecipientPermission someSharedMailbox@yourDomain.com -AccessRights SendAs -Trustee someUser@yourDomain.com -Confirm:$False

SendOnBehalfTo, add this permission for a user on a shared mailbox - I have seen where attempting to add through the GUI appears as if it gives the proper permissions but really doesn't - see also SendAs permission delegation

view

Get-Mailbox -identity someSharedMailbox@yourDomain.com | Select-Object GrantSendOnBehalfTo

add Simplest way:

Set-Mailbox someSharedMailbox -GrantSendOnBehalfTo whoYouWantToHaveAccess

or

Set-Mailbox -Identity someSharedMailbox -GrantSendOnBehalfTo whoYouWantToHaveAccess

another way

Set-Mailbox someSharedMailbox@yourDomain.com -GrantSendOnBehalfTo @{add="whoYouWantToHaveAccess@yourDomain.com"}

or

Set-Mailbox 'someSharedMailbox' -GrantSendOnBehalfTo @{add="whoYouWantToHaveAccess@yourDomain.com"}

or

Set-Mailbox 'someSharedMailbox' -GrantSendOnBehalfTo @{add="whoYouWantToHaveAccess"}

remove

Set-Mailbox someSharedMailbox -GrantSendOnBehalfTo @{remove="whoYouWantToHaveAccess"}

SendOnBehalfTo, find all mailboxes to which a user has been delegated - see delegated mailboxes that a user has SendOnBehalfTo

session, create

In order for some commands to be recognized, sometimes you need a session over and above what you normally need. For instance, any commands which include the word “compliance”.

If you are using multifactor authentication, the commands tend to be simpler

  • not requiring -ConnectionUri
  • not requiring two separate commands (the 2nd to import the session after it's created)

The following command, although it will get you into a “normal” Exchange session, won't suffice to be able to run “compliance” commands.

without multifactor authentication:

$cred = Get-Credential
$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/powershell-liveid/ -Credential $cred -Authentication Basic -AllowRedirection

Import-PSSession $Session -DisableNameChecking

with multifactor authentication:

Connect-ExchangeOnline -Credential $cred
Connect-MSOLservice -Credential $cred

You'll sometimes also need this (without multifactor authentication):

$EOSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://ps.outlook.com/powershell-liveid/ -Credential $cred -Authentication Basic -AllowRedirection

Import-PSSession $EOSession -AllowClobber -DisableNameChecking

For a “compliance” session (needed to run “compliance” commands such as those needed to delete all emails in a folder):

without multifactor authentication:

$SccSession = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://ps.compliance.protection.outlook.com/powershell-liveid -Credential $cred -Authentication Basic -AllowRedirection

Import-PSSession $SccSession -AllowClobber -DisableNameChecking

with multifactor authentication, simpler:

Connect-IPPSSession -Credential $cred

If you try to provide a ConnectionUri with multifactor authentication, you'll get access denied

You don't need the $EOSession session above to establish this session and to run compliance commands:

Check by running:

Get-PSSession

And you should see all 3 sessions

shared mailbox, convert individual mailbox to shared mailbox

Set-Mailbox "someUser@yourDomain.com" -Type shared

shared mailbox doesn't show up in Outlook - see delegates don't show up as expected in Outlook

shared mailbox, filter out from list of mailboxes- see mailbox types, filter out types

shared mailbox, guest IDs - see guest ID, delegate of shared mailbox

shared mailboxes, list all

Get-Mailbox -RecipientTypeDetails SharedMailbox -ResultSize:Unlimited | Select Identity,Alias,PrimarySmtpAddress,WindowsEmailAddress,UserPrincipalName,DisplayName | sort displayname

shared mailboxes, list all shared mailboxes upon which a user has permissions

full access

Get-Mailbox | Get-MailboxPermission -user "someUser@yourDomain.com"

SendOnBehalfOf

Get-Mailbox | ? {$_.GrantSendOnBehalfTo -match "someUser@yourDomain.com"}

shared mailbox, list delegates

for one person

Get-Mailbox -Identity someuser | Get-MailboxPermission | where {($_.IsInherited -eq $False) -and -not ($_.User -like "NT AUTHORITY\SELF")} | ft identity,user,accessrights

all mailboxes

$SharedMailboxes = Get-Mailbox –RecipientTypeDetails 'SharedMailbox' | Get-MailboxPermission | where {$_.user.tostring() -ne "NT AUTHORITY\SELF" -and $_.IsInherited -eq $false}
$SharedMailboxes | ogv

shared mailboxes, list who's delegated to each for a domain

First, stash the mailboxes into a variable. If you only want to list the shared mailboxes and don't care about delegates, you can dispense using the variable as an intermediate step and can stop here. But in the next step we'll list the delegates using the contents of this variable.

$mailboxes = Get-Mailbox -RecipientTypeDetails SharedMailbox -ResultSize:Unlimited | `
    where {$_.PrimarySmtpAddress -match "yourdomain.com"} | `
    Select Identity,Alias,DisplayName,user,AccessRights | sort displayname

Now list the delegates for each of these shared mailboxes.

$mailboxes | sort displayname | foreach {Get-MailboxPermission -Identity $_.alias | `
    where {($_.IsInherited -eq $False) -and -not ($_.User -like NT AUTHORITY\SELF) } | `
    ft identity,user,accessrights} > somefile.txt

shared mailboxes with licenses

licenses have to do with users (Get-MsolUser) whereas whether or not a mailbox is shared or not has to do with mailboxes (Get-Mailbox). If I try to pipe Get-MsolUser directly into Get-Mailbox, fail. So I find it easiest to collect info about all licenses users (Get-MsolUser) into one variable, info on all shared mailboxes (Get-Mailbox) and then compare the two.

$Users = Get-MsolUser -All | Where-Object {$_.IsLicensed -eq $true} | Select-Object -ExpandProperty UserPrincipalName
$Mailboxes = Get-Mailbox -RecipientTypeDetails SharedMailbox | Select-Object UserPrincipalName,DisplayName,Name
$Results = foreach ($User in $Users) {$Mailboxes | Where-Object UserPrincipalName -eq $User}

or simply

Get-Mailbox -ResultSize Unlimited -RecipientTypeDetails SharedMailbox | Get-MsolUser | ? {$_.isLicensed -eq $true}

shared mailbox, remove automapping for several users - see automap a shared mailbox, remove for several users

size of mailbox - see Get-MailboxStatistics

spam, bypass - see Get-TransportRule

spam message trace

Show spam for the past week

Get-MessageTrace -Start (Get-Date).AddDays(-7) -End (Get-Date) -Status "FilteredAsSpam" | `
    Export-Csv -Path "$([environment]::GetFolderPath("mydocuments"))\FilteredAsSpam$((Get-Date).ToString('MM-dd-yyyy_hh-mm-ss')).csv" -NoTypeInformation -Encoding UTF8

Note that we put in the FilteredAsSpam as part of the base search rather than piping unfiltered results to a where clause.  You can only get up to 1000 records at a time. Depending on how much spam is being filtered, if you don't add the filter right up front, you may get very few records for the week - only the most recent ones.  Even with the filter up front, you may not get a whole week's worth

For a particular user's history

$spamTraces = Get-MessageTrace -Start "10/25/2018 5 PM" -End "11/01/2018 4:00 PM" -Sender "someUser@yourDomain.com" | Where {$_.Status -eq "FilteredAsSpam"}

and then expand for more detail

$spamTraces | Get-MessageTraceDetail | Select-Object MessageID, Date, Event, Action, Detail, Data | ogv

spam settings for mailbox - see junk email, trusted recipients/domains

spam settings to whitelist outside domain - see whitelisted domains for all transport rules

special characters, export users into CSV

Get-MsolUser -all | select displayName, userprincipalname | Export-Csv 'Users.csv' -NoTypeInformation -Encoding UTF8

subject of email, find

Let's say you want to see all the emails with a subject of "Some Important Subject" in Bob Smith's email box.  You must specify a "TargetMailbox" and a "TargetFolder". So we'll specify Sam Snead's ("snead") mailbox and "BobSmith" as a directory in Sam Snead's in box:

Search-Mailbox "Bob Smith" -SearchQuery 'Subject:"Some Important Subject"' -TargetMailbox snead -TargetFolder BobSmith

As soon as the command above finishes, Sam Snead will see a new "BobSmith" as a directory in Sam Snead's in box (in Outlook).  Under that he'll see a directory named something like "Bob Smith-11/5/2018 8:54:34 PM" and under that "Primary Mailbox" and then under that a list of the various folders which might contain the emails.

Actually, you don't really have to specify a "TargetMailbox" and a "TargetFolder" if all you want to do is delete these.   If that's all you want, then see delete emails where you'll see that you can simply specify -DeleteContent there instead.

See here for discussion and more examples.

switch users

sometimes we want to move a user to a whole new division but leave their mailbox behind for their supervisor or successor to inherit.  We want the user's local AD to stay intact so all their bookmarks, cookies, passwords, etc. stay intact.  So we'll create a whole new ID in their old division, move the user's old mailbox over to the new ID, convert the new ID to a shared mailbox, and delegate that to their successor

So create the new user.  Use different userPrincipalName, targetAddress. Leave proxyAddresses blank for now.

Stash the old ImmutableIDs

$oldUserImmutableID = (Get-MsolUser -UserPrincipalName oldUser@yourDomain.com).ImmutableID

$newUserImmutableID = (Get-MsolUser -UserPrincipalName newUser@yourDomain.com).ImmutableID

Change UPN, email, main proxyAddress for old user.

The next step is very important: make sure that your old user doesn't have its targetAddress or proxyAddresses duplicated somewhere else.

$userString = "oldUserNameFragment"
Get-MsolUser | where {($_.userprincipalname -match "$userString") -or ($_.ProxyAddresses -match "$userString") } | `
    Select-Object DisplayName, UserprincipalName, ProxyAddresses
Get-ADUser -Filter "ProxyAddresses -like '*$userString*'" -Properties DisplayName, UserprincipalName, ProxyAddresses, TargetAddress | `
    Select-Object DisplayName, UserprincipalName, ProxyAddresses, TargetAddress | ogv
Get-ADUser -Filter "TargetAddress -like '*$userString*'" -Properties DisplayName, UserprincipalName, ProxyAddresses, TargetAddress | `
    Select-Object DisplayName, UserprincipalName, ProxyAddresses, TargetAddress | ogv

Carefully examin for dupes.  If you're satisfied, sync.   If you missed a dupe, the next steps will work but you won't be able to create a new mailbox for the old user.

Assign old email proxy to new user.  Sync.   Move both new and old users out of OUs that are synced to an OU that isn't. Sync again.  This will soft-delete both users so we can muck around with their ImmutableIDs.  Verify that both are soft deleted.

Get-MsolUser -All -ReturnDeletedUsers | Sort-Object UserPrincipalName | ft UserPrincipalName, DisplayName, ObjectId, ImmutableID

Restore users as floaters

Verify

Clear immutableIds of floaters

Switch ImmutableIDs

Verify reassigned ImmutableIDs

Restore both soft-deleted users by moving their IDs back to synced OUs in local AD and syncing.   This is where, if you weren't careful with making sure the targetAddresses and proxyAddresses were unique, problems.

Sync Errors - see DirSync errors, list

–T–

targetAddress for contacts - although local AD contacts have "targetAddress", on Office 365 this property translates to "externalEmailAddress" - see contacts, display proxyAddresses and targetAddress

time of last mailbox login - see Get-MailboxStatistics

time zone bulk change

set all mailboxes in our Office 365 Tenant to W. Europe Standard Time

Get-mailbox -ResultSize unlimited | Set-MailboxRegionalConfiguration -Language 1031 -TimeZone "W. Europe Standard Time" -LocalizeDefaultFolderName

as well as set language to German and change all of the default folders

trace message - (for emails, not calendars; for calendars, see calendar activity, examine log instead) - see also spam message trace

Log times are in UTC - which isn't necessarily the same time zone that your emails are in.  If you don't want to mess with UTC offset in the command and just want to find by UTC:

Get-MessageTrace -Start "9/20/21 8 pm" -End "9/20/21 9 pm" -Sender "haplessUser@yourDomain.com" | Select-Object Received, SenderAddress, RecipientAddress, Subject, Status, ToIP, FromIP, Size, MessageID, MessageTraceID | Where {$_.Status -eq "Failed"} | Out-GridView

But it's useful to search for messages using the same time as your local time zone.  So start by finding the offset:

$rawUTCOffset = (([TimeZoneInfo]::Local).BaseUtcOffset).TotalHours
$adjustedUTCOffset = if ((Get-Date).IsDayLightSavingTime()) {$rawUTCOffset+1} else {$rawUTCOffset}

Once you know offset, you can more accurately find stuff by searching for time spans that make sense to you in the time zone where you are:

Get-MessageTrace -Start ([datetime]"10/11/2022 11:30 PM").AddHours($adjustedUTCOffset*(-1)) -End ([datetime]"10/11/2022 11:50 PM").AddHours($adjustedUTCOffset*(-1)) -Sender "some.user@yourDomain.com" | Where {$_.Subject -like "Email subject*"} | Select-Object @{n="UTReceived";e={$_.Received}}, @{n="YourLocalTime";e={$_.Received.AddHours($adjustedUTCOffset)}}, @{n="SomeOtherTimeMaybeEurope"; e={$_.Received.AddHours(1)}}, SenderAddress, RecipientAddress, Subject, Status, size | ogv

For last 6 hours:

Get-MessageTrace -Start (get-date).AddHours(-6) -End (get-date) -Sender "Fred.Flintstone@bedrock.com" | Select-Object @{n="UTReceived";e={$_.Received}}, @{n="MyTime";e={$_.Received.AddHours($adjustedUTCOffset)}}, @{n="EuropeTime";e={$_.Received.AddHours(1)}}, SenderAddress, RecipientAddress, Subject, Status, size | ogv

trace message including a wild card

Get-MessageTrace -Sender "someUser@yourDomain.com" -Start (Get-Date).AddDays(-7 ) -End (Get-Date) | ? {$_.RecipientAddress –like "someOtherUser*"} | ogv

you can put in wildcard (*) immediately before or after the @ for the recipient or sender.  But I haven't had luck putting something like @yourDomain.* - system complains Invalid RecipientAddress value

filtering on subject:

Get-MessageTrace -Start "9/7/21 1:50 pm" -End "9/7/21 11 pm" | Where {$_.Subject -like "*xyz*"} | Out-GridView

trace message size defaults to no more than 1000 records

By default, Get-MessageTrace only returns up to 1000 records. Add -PageSize 5000 parameter to get up to 5000.

trace message that have errors

You can also get more detail on errors. You can start with the general info (usually with a very narrow time window which I determine by looking at previous Get-MessageTrace statements without specifying Failed):

Get-MessageTrace -Start "10/03/21 10:25:30 am" -End "10/03/21 10:25:35 am" | Where {$_.Status -eq "Failed"} | `
    Select-Object Received, SenderAddress, RecipientAddress, Subject, Status, ToIP, FromIP, Size, MessageID, MessageTraceID |`
    Export-Csv -Path "$([environment]::GetFolderPath("mydocuments"))\failedDeliveries$((Get-Date).ToString('MM-dd-yyyy_hh-mm-ss')).csv" -NoTypeInformation -Encoding UTF8

and then pipe that same statement into another Get-MessageTraceDetailstatement to get the details associated with the failed emails:

Get-MessageTrace -Start "10/03/21 10:25:30 am" -End "10/03/21 10:25:35 am" | Where {$_.Status -eq "Failed"} `
    | Get-MessageTraceDetail | Select-Object MessageID, Date, Event, Action, Detail, Data | `
    Export-Csv -Path "$([environment]::GetFolderPath("mydocuments"))\failedDeliveryDetails$((Get-Date).ToString('MM-dd-yyyy_hh-mm-ss')).csv" -NoTypeInformation -Encoding UTF8

If you run both these, to make sense of how they relate to each other, you'll have to open both files side-by-side.  But there's a better way where we can see elements of the message we attempted to send along with the error it encountered.  This time we do specify fail:

$rawUTCOffset = (([TimeZoneInfo]::Local).BaseUtcOffset).TotalHours
$adjustedUTCOffset = if ((Get-Date).IsDayLightSavingTime()) {$rawUTCOffset+1} else {$rawUTCOffset}
$failedTraces = Get-MessageTrace -Start ([datetime]"10/11/2021 11:30 PM").AddHours($adjustedUTCOffset*(-1)) -End ([datetime]"10/11/2021 11:50 PM").AddHours($adjustedUTCOffset*(-1)) `
    -Sender "haplessUser@yourDomain.com" | Where {$_.Subject -like "some subject*"} | Where {$_.Status -eq "Failed"}
$failedTraces| Foreach-Object{
    $trace = $
    $stats = $trace | Get-MessageTraceDetail -event FAIL
    New-Object -TypeName PSObject -Property @{
        MessageUTCTime = $trace.Received
        MessageLocalTime = $trace.Received.AddHours($adjustedUTCOffset)
        MessageEuropeTime = $trace.Received.AddHours(1)
        Sender = $trace.SenderAddress
        Recipients = $trace.RecipientAddress
        Subject = $trace.Subject
        MessageSize = $trace.Size
        StatusMessage = $stats.Detail
    }} | ogv

For some reason, the order of the columns is completely different from the order above.

For emails older than a week, need to run Start-HistoricalSearch instead.

traffic (email) - see email traffic

transport rules

list all transport rules

Get-TransportRule

list domain whitelisting rules (rules designed to bypass spam filter for certain domains)

Get-TransportRule "Bypass Spam: friend domains" | ? {$_.SetSCL -eq "-1" -and $null -ne $_.SenderDomainIS -and $_.State -eq "Enabled" -and $_.Mode -eq "Enforce"}

list domains in a rule configured as whitelist

(Get-TransportRule "Bypass Spam: Friend domains").SenderDomainis | fl

list whitelisted sender domains for all rules (bypass spam filter) sorted primarily by domain (to highlight domains redundantly whitelisted twice)

$bypassSpamRules = Get-TransportRule | ? {$_.SetSCL -eq "-1" -and $null -ne $_.SenderDomainIS -and $_.State -eq "Enabled" -and $_.Mode -eq "Enforce"}
$result = @()
foreach ($rule in $bypassSpamRules) {
    foreach ($domain in $rule.senderDomainIs) {
        $result += New-Object -TypeName PSObject -Property @{
            RuleID = $rule.Identity
            domain = $domain
            Priority = $rule.Priority}}}
$result = $result | sort domain, Priority
$result | ogv

the list generated above is handy if you have several different whitelisting rules and want to make sure you don't have a domain entered in more than just one rule (find dupes)

–U–

unified group, bulk change email addresses

unified groups include

  • email-enabled security groups in local Active Directory
  • SharePoint groups online only
  • Planner groups

Let's say we want to find all the groups belonging to the "yourdomain" domain and purge all emailAddresses for that same domain.  Find all the groups that fit this profile and put it in a variable:

$UnifiedGroup = Get-UnifiedGroup | where-Object {$_.emailAddresses -like "*yourdomain.com"}

Optional: inspect first before proceding to the command that actually applying our changes:

$UnifiedGroup | ft name, emailAddresses

Now proceed to actually do what we set out to do: remove all "emailAddresses" corresponding to our domain:

$UnifiedGroup | % {Set-UnifiedGroup -identity $_.identity -emailAddresses @{remove = "smtp:" + $_.PrimarySmtpAddress.split("@")[0] + "@yourdomain.com"}}

Note that, unlike many other objects, unified groups use "emailAddresses" much the same way as other objects (such as users) use "proxyAddresses".   Also note that we could have done all this in one command without the intermediate variable.   But it's nice to actually see the group we intend to change things before we actually apply changes (using the Set-UnfiedGroup command) just to make sure.

unified group, bulk change primary SmtpAddress

$UnifiedGp = Get-UnifiedGroup | ? {$_.isdirsynced -eq 0 -and ($_.PrimarySmtpAddress.split("@")[1] -match "yourdomain.com")}

Optional: inspect first before proceding to the command that actually applying our changes:

$UnifiedGp | ft name, emailAddresses

Now proceed to actually do what we set out to do: set "PrimarySmtpAddress" for all users which had corresponding "PrimarySmtpAddress" correpsonding to our domain:

$UnifiedGp | % {Set-UnifiedGroup -identity $_.identity -primarysmtpaddress ($_.PrimarySmtpAddress.split("@")[0] + "@yourTenant.onmicrosoft.com")}

Note that we could have done all this in one command without the intermediate variable.   But it's nice to actually see the group we intend to change things before we actually apply changes (using the Set-UnfiedGroup command) just to make sure.

user, what roles does a user have? - see role assignments, what roles does a user have?

–V–

visibility for a user in GAL - see Global Address List (GAL) (or Offline Address Book / OAB), suppress entries

–W–

We are preparing a mailbox for this user - see preparing a mailbox for this user

whitelist domains - see whitelisted domains for all transport rules

Windows email address, add/set - see also

First of all (and after we're done), might want to verify what this already is

Get-MailUser -Identity "elvis.presley_isStillAlive.com#EXT#@isStillAlive.onmicrosoft.com" | select DisplayName, WindowsEmailAddress, HiddenFromAddressListsEnabled, EmailAddresses

This is usually important when we want a guest ID to show up in the Global Address List (GAL). I usually include the EmailAddresses parameter just in case it hase extraneous junk from some otherwise user in there that might conflict. To set:

Get-MailUser -Identity "elvis.presley_isStillAlive.com#EXT#@isStillAlive.onmicrosoft.com" | Set-MailUser -WindowsEmailAddress "elvis.presley@isStillAlive.onmicrosoft.com" -HiddenFromAddressListsEnabled $false

simpler:

Set-MailUser -Identity "elvis.presley_isStillAlive.com#EXT#@isStillAlive.onmicrosoft.com" -WindowsEmailAddress "elvis.presley@isStillAlive.onmicrosoft.com" -HiddenFromAddressListsEnabled $false

this creates a credible WindowsEmailAddress from the existing guest ID:

$user = "elvis.presley_isStillAlive.com#EXT#@isStillAlive.onmicrosoft.com"
Set-MailUser -Identity $user -WindowsEmailAddress "$($user.Split("#")[0].Split("_")[0])@isStillAlive.onmicrosoft.com" -HiddenFromAddressListsEnabled $false

that is, it takes the first part (before "_") of the the first part (before "#"). This may be overkill since you could probably almost always just take the first part (before "_") and be done with it.

Or, if you don't know the exact ID but have a pretty good idea of a unique name that's part of it:

Get-MailUser -ResultSize unlimited | ? {$_.Identity -like "elvis.presley*" } | Set-MailUser -WindowsEmailAddress "elvis.presley@isStillAlive.onmicrosoft.com" -HiddenFromAddressListsEnabled $false

I set HiddenFromAddressListsEnabled to false to make sure it's visible in the GAL.

Once you've set, verify

Get-MailUser -Identity "elvis.presley_isStillAlive.com#EXT#@isStillAlive.onmicrosoft.com" | select DisplayName, name, UserPrincipalName, HiddenFromAddressListsEnabled, WindowsEmailAddress

What if we don't know the identity?

Get-AzureADUser -SearchString "elvis"

To populate a bunch of existing guest IDs' Windows email address, see populate a bunch of existing guest IDs' windows email address. See also distribution group, bulk change WindowsEmailAddress of cloud-only (exclude those synced with local AD)

Windows email address, list all by domain

This only shows external users whose email is not null or empty

Get-MailUser -ResultSize unlimited | ? {$_.Identity -like "*#EXT#*" -and $null -ne $_.WindowsEmailAddress -and "" -ne $_.WindowsEmailAddress} | select DisplayName, Identity, WindowsEmailAddress, @{n="DomWinEmail";e={$_.WindowsEmailAddress.split("@")[1]}}, ExternalEmailAddress, @{n="DomExtEmail";e={$_.ExternalEmailAddress.split("@")[1]}} | sort DomWinEmail | ogv

This also shows WindowsEmailAddress and ExternalEmailAddress, which seem to move in lockstep and then sorts by the domain of WindowsEmailAddress

Windows email address, list all for a domain

This finds all guest users from a particular domain in a foreign tenant and sorts by the domain of the WindowsEmailAddress. If the goal is to have all WindowsEmailAddress be present and have the same domain, this highlights any discrepencies

Get-MailUser -ResultSize unlimited | Where {($_.UserPrincipalName -like '*foreignTenantDomain.com#EXT#@foreignTenant.onmicrosoft.com')} | select DisplayName, UserPrincipalName, WindowsEmailAddress, @{n="Hide";e={$_.HiddenFromAddressListsEnabled}}, @{n="Dom";e={$_.WindowsEmailAddress.split("@")[1]}} | sort dom, DisplayName | ft -AutoSize

–X–

–Y–

–Z–