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
- [email protected] - the main alias which emails should go to the Inbox
- [email protected] - a secondary alias which emails should go to a “BatMan box”
then
New-InboxRule -Name "[email protected] - Move to BatMan direcctory" -Mailbox payables -HeaderContainsWords "[email protected]" -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
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:
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 : <[email protected]>
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
:
[email protected]
InternetMessageId :
<[email protected]>
MessageTime :
2020-07-01T10:09:49
NetworkMessageId : 0355bf10-1001-477e-4f40-08d81da6e39c
Recipients : {[email protected],
[email protected]}
Subject
: Fwd: Rappel de paiement_BurbleFrop
which is a lot closer to what we're looking for.
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
- delegated mailbox does not show up as expected in Outlook
deals with the
Automapping
set on the delegate's mailbox - MAPIEnabled set on the shared mailbox
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
=
@("[email protected]",
"[email protected]",
"[email protected]")
$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
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
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
=
@("[email protected]","[email protected]","[email protected]")
$sharedCalendars
=
@("[email protected]","[email protected]")
$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
= ("[email protected]","[email protected]","[email protected]")
$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
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:'[email protected]' -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 '[email protected]:\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 "[email protected]" | ? {($_.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-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 "[email protected]"} | 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:[email protected]" 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 "[email protected]")} | Remove-MsolContact -Force
Get-MsolContact -All | where {($_.EmailAddress -eq "[email protected]")} | 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 "[email protected] " -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}
$EmailAddress
=
"[email protected]"
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
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"}}
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
=
"[email protected]"
$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}
}
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
=
"[email protected]"
$delegates
=
@("[email protected]",
"[email protected]",
"[email protected]",
"[email protected]")
$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
=
@("[email protected]",
"[email protected]",
"[email protected]")
$userNeedsAccess
=
"[email protected]"
$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
"[email protected]"
|
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 "[email protected]" | Remove-MailboxPermission -user "[email protected]" -AccessRights FullAccess -Confirm:$False | Add-MailboxPermission -AccessRights FullAccess -Automapping $true -User "[email protected]"
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
=
"[email protected]"
$delegates
=
@("[email protected]",
"[email protected]",
"[email protected]",
"[email protected]")
$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-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 '[email protected]'
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 '[email protected]'
(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
= ("[email protected]",
"[email protected]",
"[email protected]")
$delegates
=
$mailboxes
|
%
{Get-Mailbox
$_
|
Get-MailboxPermission
|
where
{$_.user.tostring() -ne
"NT AUTHORITY\SELF"
-and
$_.user.tostring() -ne
"[email protected]"
-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 [email protected] and another [email protected]. 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
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
first way - easier if items only recently deleted
list items of potential interest
Get-RecoverableItems -Identity "[email protected]" -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 "[email protected]" -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 "[email protected]" -SearchQuery "subject:""$9 million dollar signed contract!""" -TargetMailbox "[email protected]" -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 "[email protected]" -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 "[email protected]" -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 "[email protected]"
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
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
[email protected]
$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 "[email protected]" -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 "[email protected]"}
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 [email protected] |
Get-AzureADUser
-SearchString [email protected] |
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 WindowsEmailAddress
attributes? 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 WindowsEmailAddress
attributes, 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"}
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
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
eDiscovery - see litigation hold
email box full - see Find folder ID for all of a user's folders, sort decending by size
Set-MailUser -Identity "[email protected]" -emailAddresses @{remove = "smtp:[email protected]"}
email-enabled security groups - see distribution group, add members
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:
- Even though I specified credentials as an argument, it still insisted on collecting them from me
- 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 [email protected] | 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 "[email protected]"
-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
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
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
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
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 [email protected] | 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
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
individual
Get-MailboxStatistics -Identity "[email protected]"
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 "[email protected]" | 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
=
@("[email protected]","[email protected]","[email protected]")
$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 | 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 "[email protected]" -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 "[email protected]"
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.
$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
$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
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 "[email protected]"
So, you'll want to do both:
Get-User -RecipientTypeDetails GuestMailUser | ? {$_.Name -eq "someExternalUser_someDomain#EXT#"} | Set-MailUser -WindowsEmailAddress "[email protected]" -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
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 "[email protected]" -HiddenFromAddressListsEnabled $false
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="[email protected]"}
didn't work for me. Sometimes using the name instead of the email seems to work better
Set-MailboxJunkEmailConfiguration "Dirt Poor" -TrustedRecipientsAndDomains @{Add="[email protected]"}
remove
Set-MailboxJunkEmailConfiguration dirt.poor -TrustedRecipientsAndDomains @{Remove="[email protected]"}
unlike add
, remove
sometimes seems to work OK
Overall, both the add
and remove
commands
just seem to be flaky and not work consistently.
kiosk mailbox, filter out from list of mailboxes - see mailbox types, filter out types
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 "[email protected]" | 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 "[email protected]" | ft DisplayName, LastLogonTime
$UPN
=
"[email protected]"
$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
$mailboxID
=
"[email protected]"
if
(Get-Mailbox
$mailboxID
-ErrorAction SilentlyContinue)
{"$mailboxID
exists"}
else
{"$mailboxID
does NOT exist"}
or if you have a bunch in an array:
$lookup
=
@("[email protected]",
"[email protected]",
"[email protected]")
$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
This also sorts by domain
Get-Mailbox | Select-Object @{n="Dom";e={$_.UserPrincipalName.split("@")[1]}}, displayName, userprincipalname | Sort-Object dom, displayName
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
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 "[email protected]" | 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.
notification (will a user get notified in his email when a calendar invite comes along wanting to modify his calendar?) - see calendar notifications
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:
[email protected] "
$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
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 "[email protected]" | Add-MailboxPermission -AccessRights FullAccess -Automapping $true -User "[email protected]"
or simpler:
Add-MailboxPermission -Identity sourceUser -AccessRights FullAccess -Automapping $true -User "[email protected]"
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:
- 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.
- Re-Assign the Licenses to the user in Office 365 portal or Power Shell.
- 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
=
"[email protected]"
$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 WindowsEmailAddress
es for a distribution list
which might closely map to IDs in this this domain.
The point being: if that foreign tenants' WindowsEmailAddress
es point here,
there'd better be a corresponding “.onmicrosoft.com” proxy address here!
Add-PublicFolderClientPermission -Identity "\Calendars\someCalendar" -User someUser -AccessRights PublishingEditor
public folders, list permission
Get-PublicFolderClientPermission "\Calendars\someCalendar" -User someUser -AccessRights PublishingEditor
Get-PublicFolder -ResultSize Unlimited -Recurse
If you have any public caledars, they'll show up with a
"\Calendar
" parent path
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 '[email protected]'
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 "[email protected]" | 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 = @("[email protected]", "[email protected]", "[email protected]")
$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"}
Add-RoleGroupMember -Identity "Discovery Management" -Member "Luke Skywalker"
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?
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
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"
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 "[email protected] - Move& to SpiderMan" -Mailbox payables -HeaderContainsWords "[email protected]" -MoveToFolder ":\Inbox\spiderman" -StopProcessingRules $false
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
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
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
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
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="[email protected]"}
or
Set-Mailbox 'someSharedMailbox' -GrantSendOnBehalfTo @{add="[email protected]"}
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
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 Import-PSSession
$Session
-DisableNameChecking with multifactor authentication: Connect-ExchangeOnline
-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 “ 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 You don't need the
$Session
=
New-PSSession
-ConfigurationName Microsoft.Exchange -ConnectionUri
https://outlook.office365.com/powershell-liveid/ -Credential
$cred
-Authentication Basic -AllowRedirection
Connect-MSOLservice
-Credential
$cred
compliance
” session
(needed to run “compliance
” commands
such as those needed to delete all emails in a folder):ConnectionUri
with
multifactor authentication, you'll get access denied
$EOSession
session above to
establish this session and to run compliance
commands:
Get-PSSession
And you should see all 3 sessions
shared mailbox, convert individual mailbox to shared mailbox
Set-Mailbox "[email protected]" -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
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 "[email protected]"
SendOnBehalfOf
Get-Mailbox | ? {$_.GrantSendOnBehalfTo -match "[email protected]"}
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
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 "[email protected]" | 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
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.
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
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
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 "[email protected]" | 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 "[email protected]" | 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
"[email protected]"
|
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
"[email protected]"
-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, 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
-Start
"10/03/21 10:25:30 am"
-End
"10/03/21 10:25:35 am"
|
Where
{$_.Status
-eq
"Failed"} | ` and then pipe that same statement into another
Get-MessageTrace
-Start
"10/03/21 10:25:30 am"
-End
"10/03/21 10:25:35 am"
|
Where
{$_.Status
-eq
"Failed"} ` 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 $rawUTCOffset
= (([TimeZoneInfo]::Local).BaseUtcOffset).TotalHours For some reason, the order of the columns is completely
different from the order above. For emails older than a week, need to run
traffic (email) - see email traffic 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"} 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) unified group, bulk change email addresses unified groups include 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 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 user, what roles does a user have? -
see role assignments, what roles does a user have? visibility for a user in GAL - see
Global Address List (GAL) (or Offline Address Book / OAB), suppress entries 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 Get-MailUser
-Identity
"elvis.presley_isStillAlive.com#EXT#@isStillAlive.onmicrosoft.com"
|
Set-MailUser
-WindowsEmailAddress
"[email protected]"
-HiddenFromAddressListsEnabled
$false simpler: Set-MailUser -Identity
"elvis.presley_isStillAlive.com#EXT#@isStillAlive.onmicrosoft.com" -WindowsEmailAddress
"[email protected]"
-HiddenFromAddressListsEnabled
$false this creates a credible $user
=
"elvis.presley_isStillAlive.com#EXT#@isStillAlive.onmicrosoft.comGet-MessageTrace
only returns up to 1000 records.
Add -PageSize 5000
parameter to get up to 5000.Get-MessageTrace
statements
without specifying 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 UTF8Get-MessageTraceDetail
statement to get the details
associated with the failed emails:
|
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 UTF8fail
:
$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
"[email protected]"
|
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
}} | ogvStart-HistoricalSearch
instead.
$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
Set-UnfiedGroup
command) just to make sure.Set-UnfiedGroup
command) just to make sure.
EmailAddresses
parameter just in case it hase extraneous junk from some otherwise
user in there that might conflict. To set:WindowsEmailAddress
from the existing guest ID:
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 "[email protected]" -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