-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathSend-M365Digest-OAuth.ps1
More file actions
373 lines (324 loc) · 14.4 KB
/
Copy pathSend-M365Digest-OAuth.ps1
File metadata and controls
373 lines (324 loc) · 14.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
#Requires -Version 5.1
<#
.SYNOPSIS
M365 Digest Email Campaign - OAuth2 Authentication (Azure AD App)
.DESCRIPTION
Example script demonstrating bulk email sending with:
- OAuth2 Client Credentials Flow (Azure AD App Registration)
- CSV data import
- Template-based HTML emails
- Inline images (logo + 3 product icons)
- PDF attachments
- Batched sending with rate limiting
.NOTES
Requires:
- M365DigestEmailModule.psm1
- Azure AD App Registration with Mail.Send permission
- Admin consent granted for the application
Setup Steps:
1. Register app in Azure AD: Azure Portal > App registrations > New
2. Add API permission: Microsoft Graph > Application > Mail.Send
3. Grant admin consent
4. Create client secret and note the value
5. Update configuration below with TenantId, ClientId, ClientSecret
.AUTHOR
Jan Huebener
.VERSION
1.0.0
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[ValidateSet('Test', 'Production')]
[string]$ConfigMode = 'Production'
)
# ============================================================================
# CONFIGURATION
# ============================================================================
# Import module
$modulePath = Join-Path $PSScriptRoot "M365DigestEmailModule.psm1"
Import-Module $modulePath -Force -Verbose
# Paths - UPDATE THESE FOR YOUR ENVIRONMENT
$csvPath = "C:\Temp\master_users_all_merged_with_wave4.csv"
$htmlTemplate = Join-Path $PSScriptRoot "M365_Digest_Template.htm"
$checkpointFile = "C:\Temp\smtp_send_checkpoint_m365digest_oauth.txt"
# Inline Images (CID must match template references)
$inlineImages = @(
@{
ContentId = 'datagroup_logo'
FilePath = 'C:\temp\datagroup_logo.png'
},
@{
ContentId = 'm365_icon'
FilePath = Join-Path $PSScriptRoot 'm365_icon.png'
},
@{
ContentId = 'exchange_icon'
FilePath = Join-Path $PSScriptRoot 'exchange_icon.png'
},
@{
ContentId = 'sharepoint_icon'
FilePath = Join-Path $PSScriptRoot 'sharepoint_icon.png'
}
)
# Attachments - UPDATE OR REMOVE AS NEEDED
$attachments = @(
# "C:\temp\Anleitung_Erstanmeldung_Authentifizierung_mit_Smartphone.pdf",
# "C:\temp\Anleitung_Erstanmeldung_Authentifizierung_mit_Telefon.pdf"
)
# SMTP Configuration
$smtpConfig = @{
Server = "smtp.office365.com"
Port = 587
EnableSsl = $true
From = "noreply@yourdomain.com" # UPDATE: Sender address
Bcc = "admin@yourdomain.com" # UPDATE: BCC for monitoring
Subject = "Microsoft 365 Monthly Digest - What's new?"
}
# ============================================================================
# OAUTH CONFIGURATION - UPDATE THESE VALUES
# ============================================================================
#
# To get these values:
# 1. Go to Azure Portal > Azure Active Directory > App registrations
# 2. Select your app (or create new one)
# 3. Overview page has: Application (client) ID and Directory (tenant) ID
# 4. Certificates & secrets > New client secret > Copy the VALUE (not ID)
#
# SECURITY WARNING:
# Do NOT commit real secrets to source control!
# Use secure storage methods in production:
# - Azure Key Vault
# - Windows Credential Manager
# - Environment variables
# - Encrypted config files
$oauthConfig = @{
TenantId = "your-tenant-id-here" # Directory (tenant) ID
ClientId = "your-client-id-here" # Application (client) ID
ClientSecret = "your-client-secret-here" # Client secret VALUE
Username = "noreply@yourdomain.com" # Must match From address
}
# ============================================================================
# SECURE CREDENTIAL LOADING (PRODUCTION RECOMMENDED)
# ============================================================================
# Uncomment ONE of these sections for production use:
# Option 1: Load from environment variables
# $oauthConfig = @{
# TenantId = $env:M365_TENANT_ID
# ClientId = $env:M365_CLIENT_ID
# ClientSecret = $env:M365_CLIENT_SECRET
# Username = $env:M365_SENDER_EMAIL
# }
# Option 2: Load from Azure Key Vault (requires Az.KeyVault module)
# Import-Module Az.KeyVault
# $vaultName = "your-keyvault-name"
# $oauthConfig = @{
# TenantId = (Get-AzKeyVaultSecret -VaultName $vaultName -Name "M365-TenantId" -AsPlainText)
# ClientId = (Get-AzKeyVaultSecret -VaultName $vaultName -Name "M365-ClientId" -AsPlainText)
# ClientSecret = (Get-AzKeyVaultSecret -VaultName $vaultName -Name "M365-ClientSecret" -AsPlainText)
# Username = (Get-AzKeyVaultSecret -VaultName $vaultName -Name "M365-SenderEmail" -AsPlainText)
# }
# Option 3: Load from encrypted file (Windows DPAPI - user-specific)
# $configPath = "C:\Secure\oauth_config.xml"
# if (Test-Path $configPath) {
# $encrypted = Import-Clixml $configPath
# $oauthConfig = @{
# TenantId = $encrypted.TenantId
# ClientId = $encrypted.ClientId
# ClientSecret = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(
# [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($encrypted.ClientSecret)
# )
# Username = $encrypted.Username
# }
# }
# ============================================================================
# Batch Configuration
# ============================================================================
$batchConfig = @{
BatchSize = 20
WindowMinutes = 3.0
MaxRetries = 3
}
# Test Mode Configuration - reduced batch for testing
if ($ConfigMode -eq 'Test') {
Write-Host "`n[TEST MODE] Using reduced batch settings" -ForegroundColor Yellow
$batchConfig.BatchSize = 2
$batchConfig.WindowMinutes = 0.1
}
# ============================================================================
# VALIDATION
# ============================================================================
function Test-OAuthConfiguration {
param([hashtable]$Config)
$valid = $true
if ($Config.TenantId -match 'your-tenant-id|^$') {
Write-Host " [ERROR] TenantId not configured" -ForegroundColor Red
$valid = $false
}
if ($Config.ClientId -match 'your-client-id|^$') {
Write-Host " [ERROR] ClientId not configured" -ForegroundColor Red
$valid = $false
}
if ($Config.ClientSecret -match 'your-client-secret|^$') {
Write-Host " [ERROR] ClientSecret not configured" -ForegroundColor Red
$valid = $false
}
if ($Config.Username -match 'yourdomain.com|^$') {
Write-Host " [ERROR] Username (sender email) not configured" -ForegroundColor Red
$valid = $false
}
return $valid
}
# ============================================================================
# MAIN EXECUTION
# ============================================================================
try {
Write-Host "`n" -NoNewline
Write-Host "================================================" -ForegroundColor Cyan
Write-Host " M365 Monthly Digest Email Campaign (OAuth2) " -ForegroundColor Cyan
Write-Host "================================================" -ForegroundColor Cyan
Write-Host " Mode: $ConfigMode" -ForegroundColor $(if ($ConfigMode -eq 'Test') { 'Yellow' } else { 'Green' })
Write-Host "================================================`n" -ForegroundColor Cyan
# Validate OAuth configuration
Write-Host "[1/6] Validating OAuth configuration..." -ForegroundColor Cyan
if (-not (Test-OAuthConfiguration -Config $oauthConfig)) {
throw "OAuth configuration is incomplete. Please update the script with your Azure AD app credentials."
}
Write-Host " [OK] OAuth configuration valid" -ForegroundColor Green
# Validate file paths
Write-Host "`n[2/6] Validating file paths..." -ForegroundColor Cyan
if (-not (Test-Path $csvPath)) {
throw "CSV file not found: $csvPath"
}
Write-Host " [OK] CSV file: $csvPath" -ForegroundColor Green
if (-not (Test-Path $htmlTemplate)) {
throw "HTML template not found: $htmlTemplate"
}
Write-Host " [OK] HTML template: $htmlTemplate" -ForegroundColor Green
# Validate inline images
Write-Host "`n[3/6] Validating inline images..." -ForegroundColor Cyan
$missingImages = 0
foreach ($img in $inlineImages) {
if (Test-Path $img.FilePath) {
$size = [math]::Round((Get-Item $img.FilePath).Length / 1KB, 1)
Write-Host " [OK] $($img.ContentId): $($img.FilePath) (${size}KB)" -ForegroundColor Green
}
else {
Write-Host " [WARN] Missing: $($img.FilePath)" -ForegroundColor Yellow
$missingImages++
}
}
if ($missingImages -gt 0) {
Write-Host " [!] $missingImages image(s) missing - emails may display incorrectly" -ForegroundColor Yellow
}
# Validate attachments
Write-Host "`n[4/6] Validating attachments..." -ForegroundColor Cyan
if ($attachments.Count -eq 0) {
Write-Host " [INFO] No attachments configured" -ForegroundColor Gray
}
else {
foreach ($attachment in $attachments) {
if (Test-Path $attachment) {
$size = [math]::Round((Get-Item $attachment).Length / 1KB, 1)
Write-Host " [OK] $attachment (${size}KB)" -ForegroundColor Green
}
else {
Write-Host " [WARN] Missing: $attachment" -ForegroundColor Yellow
}
}
}
# Acquire OAuth token
Write-Host "`n[5/6] Acquiring OAuth2 token..." -ForegroundColor Cyan
Write-Host " Tenant: $($oauthConfig.TenantId)" -ForegroundColor Gray
Write-Host " Client: $($oauthConfig.ClientId)" -ForegroundColor Gray
$credential = Get-EmailAuthenticationCredential `
-AuthMethod 'OAuth' `
-Username $oauthConfig.Username `
-TenantId $oauthConfig.TenantId `
-ClientId $oauthConfig.ClientId `
-ClientSecret $oauthConfig.ClientSecret
$smtpConfig.Credential = $credential
$smtpConfig.From = $oauthConfig.Username
Write-Host " [OK] OAuth token acquired successfully" -ForegroundColor Green
# Load recipients
Write-Host "`n[6/6] Loading recipient data..." -ForegroundColor Cyan
$csvData = Import-Csv -LiteralPath $csvPath -Delimiter ';' -Encoding UTF8
# Build recipient objects with template replacements
$recipients = @()
foreach ($row in $csvData) {
$email = ($row.email).Trim()
if ([string]::IsNullOrWhiteSpace($email)) { continue }
# Build replacement hashtable for this recipient
# UPDATE THESE to match your actual content
$replacements = @{
'CARD1_TITLE' = "New Teams Features"
'CARD1_CONTENT' = "Microsoft Teams introduces new collaboration features including enhanced meeting recordings and AI-powered meeting summaries."
'CARD1_LINK' = "https://admin.microsoft.com/?ref=MessageCenter/:/messages/MC1069560"
'CARD2_TITLE' = "Exchange Online Updates"
'CARD2_CONTENT' = "Enhanced security features now available for Exchange Online mailboxes, including improved phishing protection."
'CARD2_LINK' = "https://admin.microsoft.com/?ref=MessageCenter/:/messages/MC1134178"
'CARD3_TITLE' = "SharePoint Improvements"
'CARD3_CONTENT' = "New document management capabilities in SharePoint Online with AI-powered search and classification."
'CARD3_LINK' = "https://admin.microsoft.com/?ref=MessageCenter/:/messages/MC1069560"
'UNSUBSCRIBE_LINK' = "https://www.yourdomain.com/unsubscribe?email=$email"
}
# Personalize if DisplayName is available
if ($row.PSObject.Properties.Name -contains 'DisplayName_email' -and $row.DisplayName_email) {
$name = $row.DisplayName_email
$replacements['CARD1_CONTENT'] = "Hello $name, " + $replacements['CARD1_CONTENT']
}
$recipients += [PSCustomObject]@{
Email = $email
Replacements = $replacements
}
}
Write-Host " [OK] Loaded $($recipients.Count) recipients" -ForegroundColor Green
# Display summary before sending
Write-Host "`n================================================" -ForegroundColor Cyan
Write-Host " CAMPAIGN SUMMARY" -ForegroundColor Cyan
Write-Host "================================================" -ForegroundColor Cyan
Write-Host " Recipients: $($recipients.Count)"
Write-Host " Batch Size: $($batchConfig.BatchSize)"
Write-Host " Window Interval: $($batchConfig.WindowMinutes) minutes"
Write-Host " Max Retries: $($batchConfig.MaxRetries)"
Write-Host " Sender: $($smtpConfig.From)"
Write-Host " Subject: $($smtpConfig.Subject)"
Write-Host " Auth: OAuth2 (Client Credentials)"
Write-Host "================================================`n" -ForegroundColor Cyan
if ($ConfigMode -eq 'Production') {
Write-Host "[!] PRODUCTION MODE - Sending to ALL recipients" -ForegroundColor Yellow
Write-Host " Press Ctrl+C within 5 seconds to abort..." -ForegroundColor Yellow
Start-Sleep -Seconds 5
}
# Template Configuration
$templateConfig = @{
TemplatePath = $htmlTemplate
Encoding = 'UTF8'
InlineImages = $inlineImages
Attachments = $attachments
}
# Send bulk emails
$bulkParams = @{
Recipients = $recipients
TemplateConfig = $templateConfig
SmtpConfig = $smtpConfig
BatchSize = $batchConfig.BatchSize
WindowMinutes = $batchConfig.WindowMinutes
MaxRetries = $batchConfig.MaxRetries
CheckpointPath = $checkpointFile
}
Send-BulkHtmlEmail @bulkParams
Write-Host "`n================================================" -ForegroundColor Green
Write-Host " CAMPAIGN COMPLETED SUCCESSFULLY" -ForegroundColor Green
Write-Host "================================================`n" -ForegroundColor Green
}
catch {
Write-Host "`n================================================" -ForegroundColor Red
Write-Host " CAMPAIGN FAILED" -ForegroundColor Red
Write-Host "================================================" -ForegroundColor Red
Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Red
Write-Host "`n Stack Trace:" -ForegroundColor Gray
Write-Host $_.ScriptStackTrace -ForegroundColor Gray
Write-Host "================================================`n" -ForegroundColor Red
exit 1
}