1
+ function Sync-Gal {
2
+ <#
3
+ . DESCRIPTION
4
+ Does a one-way sync between two Exchange Online Gal's.
5
+
6
+ . PARAMETER PrimaryTenantCreds
7
+ Credentials to login to the primary tenant.
8
+
9
+ . PARAMETER SecondaryTenantCreds
10
+ Credentials to login to the secondary tenant.
11
+
12
+ . PARAMETER AsJob
13
+ Enables the updates to be run as PS Jobs.
14
+
15
+ . PARAMETER Jobs
16
+ The number of PS Jobs to create to run the updates. Limited to 3 because of an O365 default max concurrent connection.
17
+
18
+ . PARAMETER ContactLimit
19
+ Number of contacts to sync. Default is Unlimited. Useful for doing quick tests.
20
+
21
+ . EXAMPLE
22
+ ./Sync-Gal.ps1 -PrimaryTenantCreds (Get-Credential) -SecondaryTenantCreds (Get-Credential) -AsJob -Jobs 3
23
+
24
+ . EXAMPLE
25
+ ./Sync-Gal.ps1
26
+
27
+ . NOTES
28
+ Created by Nick Rodriguez
29
+ Syncs the Gal of Exchange Online across two Office 365 tenants
30
+ Note that this will break the creation of external user objects until MS addresses a known issue:
31
+ https://products.office.com/en-us/business/office-365-roadmap?filters=&featureid=72273
32
+ #>
33
+ [CmdletBinding (
34
+ SupportsShouldProcess = $true ,
35
+ DefaultParameterSetName = ' Synchronous'
36
+ )]
37
+ Param (
38
+ [Parameter (Mandatory = $true )]
39
+ [PSCredential ]
40
+ $PrimaryTenantCreds ,
41
+
42
+ [Parameter (Mandatory = $true )]
43
+ [PSCredential ]
44
+ $SecondaryTenantCreds ,
45
+
46
+ [Parameter (
47
+ Mandatory = $false ,
48
+ ParameterSetName = ' Asynchronous'
49
+ )]
50
+ [Switch ]
51
+ $AsJob ,
52
+
53
+ [Parameter (
54
+ Mandatory = $false ,
55
+ ParameterSetName = ' Asynchronous'
56
+ )]
57
+ [ValidateRange (1 , 3 )]
58
+ [Int ]
59
+ $Jobs = 2 ,
60
+
61
+ [Parameter (Mandatory = $false )]
62
+ [ValidateRange (0 , [Int ]::MaxValue)]
63
+ [Int ]
64
+ $ContactLimit = 0
65
+ )
66
+
67
+ begin {
68
+ # Log everything
69
+ $LogDirectory = (New-Item - ItemType Directory " C:\powershell-scripts\Exchange Online\Logs" - Force).FullName
70
+ $Date = (Get-Date ).ToString(' yyyyMMdd-HHmm' )
71
+ Start-Transcript - Path " $LogDirectory \$ ( $MyInvocation.MyCommand.Name ) -$Date .log"
72
+ }
73
+
74
+ process {
75
+ # Create Exchange Online PowerShell session for primary tenant
76
+ $PrimaryTenantEOSession = New-EOSession - Credential $PrimaryTenantCreds
77
+
78
+ # Enter session on primary tenant
79
+ Import-PSSession $PrimaryTenantEOSession
80
+
81
+ # Get all Gal recipients using the primary filter
82
+ $GalFilter = (Get-GlobalAddressList ).RecipientFilter
83
+ $ResultSizeLimit = if ($ContactLimit -eq 0 ) { ' Unlimited' } else { $ContactLimit }
84
+ $Gal = Get-Recipient - ResultSize $ResultSizeLimit - Filter $GalFilter
85
+
86
+ # Export Gal to Csv file
87
+ $Gal | Export-Csv - Path " $LogDirectory \Gal.csv" - NoTypeInformation - Force
88
+
89
+ # Remove session on primary tenant
90
+ Remove-PSSession - Session $PrimaryTenantEOSession
91
+
92
+ # Create/Update contact for each Gal entry
93
+ # If Jobs param specified, break up the list into smaller lists that can be started as jobs
94
+ if ($AsJob ) {
95
+ $ContactLists = @ {}
96
+ $Count = 0
97
+
98
+ # Separate the contacts into smaller lists
99
+ $Gal | ForEach-Object {
100
+ $ContactLists [$Count % $Jobs ] += @ ($_ )
101
+ $Count ++
102
+ }
103
+
104
+ # Create a job for each sublist of contacts
105
+ foreach ($ContactList in $ContactLists.Values ) {
106
+ Start-Job - ArgumentList $SecondaryTenantCreds , $ContactList - ScriptBlock {
107
+ # Create Exchange Online PS session
108
+ $SecondaryTenantEOSession = New-EOSession - Credential $args [0 ]
109
+
110
+ # Enter session on secondary tenant
111
+ Import-PSSession $SecondaryTenantEOSession
112
+
113
+ Update-GalContact - Gal $args [1 ]
114
+
115
+ # Remove session on secondary tenant
116
+ Remove-PSSession - Session $SecondaryTenantEOSession
117
+ } - InitializationScript { Import-Module ' C:\powershell-scripts\Exchange Online\GalSync.psm1' }
118
+ }
119
+
120
+ # Wait for all jobs to finish then receive and remove the jobs
121
+ Get-Job | Wait-Job | Receive-Job
122
+ Get-Job | Remove-Job
123
+ } else {
124
+ # Create Exchange Online PS session
125
+ $SecondaryTenantEOSession = New-EOSession - Credential $SecondaryTenantCreds
126
+
127
+ # Enter session on secondary tenant
128
+ Import-PSSession $SecondaryTenantEOSession
129
+
130
+ Update-GalContact - Gal $Gal
131
+
132
+ # Remove session on secondary tenant
133
+ Remove-PSSession - Session $SecondaryTenantEOSession
134
+ }
135
+ }
136
+
137
+ end {
138
+ # Remove any lingering PSSessions and stop the logging
139
+ Get-PSSession | Remove-PSSession
140
+ Stop-Transcript
141
+ }
142
+ }
143
+
144
+ function New-EOSession {
145
+ <#
146
+ . DESCRIPTION
147
+ Create a new Exchange Online PowerShell session
148
+
149
+ . PARAMETER Credential
150
+ The credentials to use for the session.
151
+
152
+ . EXAMPLE
153
+ New-EOSession -Credential $creds
154
+
155
+ . NOTES
156
+ Created by Nick Rodriguez
157
+ #>
158
+ [CmdletBinding (SupportsShouldProcess = $true )]
159
+ Param (
160
+ [Parameter (Mandatory = $true )]
161
+ [PSCredential ]
162
+ $Credential
163
+ )
164
+
165
+ New-PSSession - ConfigurationName Microsoft.Exchange - Authentication Basic - AllowRedirection `
166
+ - ConnectionUri https:// outlook.office365.com / powershell- liveid/ - Credential $Credential
167
+ }
168
+
169
+ function Update-GalContact {
170
+ <#
171
+ . DESCRIPTION
172
+ Takes an array of recipients and updates the Gal.
173
+
174
+ . PARAMETER Gal
175
+ The recipient or list of reipients to update.
176
+
177
+ . EXAMPLE
178
+ Update-GalContact -Gal $ExternalGal
179
+
180
+ . NOTES
181
+ Created by Nick Rodriguez
182
+ #>
183
+ [CmdletBinding (SupportsShouldProcess = $true )]
184
+ Param (
185
+ [PSObject []]$Gal
186
+ )
187
+
188
+ foreach ($Recipient in $Gal ) {
189
+ # Create a new contact if one doesn't exist
190
+ if (Get-MailContact - Identity $Recipient.Name ) {
191
+ Write-Host " Contact $ ( $Recipient.Name ) already exists."
192
+ } else {
193
+ try {
194
+ New-MailContact `
195
+ - ExternalEmailAddress $Recipient.PrimarySmtpAddress `
196
+ - Name $Recipient.Name `
197
+ - FirstName $Recipient.FirstName `
198
+ - LastName $Recipient.LastName `
199
+ - DisplayName $Recipient.DisplayName `
200
+ - Alias $Recipient.Alias
201
+ Write-Host " New contact created for $ ( $Recipient.Name ) "
202
+ } catch {
203
+ Write-Host " Error creating new contact for $ ( $Recipient.Name ) : $_ "
204
+ }
205
+ }
206
+
207
+ try {
208
+ # Update mail contact properties
209
+ Set-MailContact `
210
+ - Identity $Recipient.Name `
211
+ - ExternalEmailAddress $Recipient.PrimarySmtpAddress `
212
+ - Name $Recipient.Name `
213
+ - DisplayName $Recipient.DisplayName `
214
+ - Alias $Recipient.Alias `
215
+ - CustomAttribute1 $Recipient.CustomAttribute1 `
216
+ - CustomAttribute2 $Recipient.CustomAttribute2 `
217
+ - CustomAttribute3 $Recipient.CustomAttribute3 `
218
+ - CustomAttribute4 $Recipient.CustomAttribute4 `
219
+ - CustomAttribute5 $Recipient.CustomAttribute5 `
220
+ - CustomAttribute6 $Recipient.CustomAttribute6 `
221
+ - CustomAttribute7 $Recipient.CustomAttribute7 `
222
+ - CustomAttribute8 $Recipient.CustomAttribute8 `
223
+ - CustomAttribute9 $Recipient.CustomAttribute9 `
224
+ - CustomAttribute10 $Recipient.CustomAttribute10 `
225
+ - CustomAttribute11 $Recipient.CustomAttribute11 `
226
+ - CustomAttribute12 $Recipient.CustomAttribute12 `
227
+ - CustomAttribute13 $Recipient.CustomAttribute13 `
228
+ - CustomAttribute14 $Recipient.CustomAttribute14 `
229
+ - CustomAttribute15 $Recipient.CustomAttribute15 `
230
+ - ExtensionCustomAttribute1 $Recipient.ExtensionCustomAttribute1 `
231
+ - ExtensionCustomAttribute2 $Recipient.ExtensionCustomAttribute2 `
232
+ - ExtensionCustomAttribute3 $Recipient.ExtensionCustomAttribute3 `
233
+ - ExtensionCustomAttribute4 $Recipient.ExtensionCustomAttribute4 `
234
+ - ExtensionCustomAttribute5 $Recipient.ExtensionCustomAttribute5 `
235
+ | Out-Null
236
+
237
+ # Update Windows Email Address only if it's populated
238
+ if ($Recipient.WindowsLiveID -ne ' ' ) {
239
+ Set-MailContact - Identity $Recipient.Name - WindowsEmailAddress $Recipient.WindowsLiveID | Out-Null
240
+ }
241
+ } catch {
242
+ Write-Host " Error updating mail contact info for $ ( $Recipient.Name ) : $_ "
243
+ }
244
+
245
+ try {
246
+ # Update contact properties
247
+ Set-Contact `
248
+ - Identity $Recipient.Name `
249
+ - FirstName $Recipient.FirstName `
250
+ - LastName $Recipient.LastName `
251
+ - Department $Recipient.Department `
252
+ - Company $Recipient.Company `
253
+ - Phone $Recipient.Phone `
254
+ - HomePhone $Recipient.HomePhone `
255
+ - OtherHomePhone $Recipient.OtherHomePhone `
256
+ - MobilePhone $Recipient.MobilePhone `
257
+ - OtherTelephone $Recipient.OtherTelephone `
258
+ - Pager $Recipient.Pager `
259
+ - Fax $Recipient.Fax `
260
+ - OtherFax $Recipient.OtherFax `
261
+ - Office $Recipient.Office `
262
+ - CountryOrRegion $Recipient.UsageLocation `
263
+ - StreetAddress $Recipient.StreetAddress `
264
+ - City $Recipient.City `
265
+ - StateOrProvince $Recipient.StateOrProvince `
266
+ - PostalCode $Recipient.PostalCode `
267
+ - PostOfficeBox $Recipient.PostOfficeBox `
268
+ - Title $Recipient.Title `
269
+ - Manager $Recipient.Manager `
270
+ - AssistantName $Recipient.AssistantName `
271
+ - Notes $Recipient.Notes `
272
+ | Out-Null
273
+ } catch {
274
+ Write-Host " Error updating contact info for $ ( $Recipient.Name ) : $_ "
275
+ }
276
+ }
277
+ }
0 commit comments