β‘ Import module: Import-Module .\M365DigestEmailModule.psm1
β‘ Update HTML template with your content
β‘ Prepare inline images (PNG/JPG) matching CID references
β‘ Create CSV file with recipient data
β‘ Configure authentication (Basic or OAuth)
β‘ Test configuration: .\Test-M365DigestConfig.ps1
β‘ Run test campaign: .\Send-M365Digest-BasicAuth.ps1 -ConfigMode Test
β‘ Run production: .\Send-M365Digest-BasicAuth.ps1 -ConfigMode Production
# Full validation
.\Test-M365DigestConfig.ps1
# Skip auth test
.\Test-M365DigestConfig.ps1 -SkipAuthTest# Test mode (2 emails, fast)
.\Send-M365Digest-BasicAuth.ps1 -ConfigMode Test
# Production mode
.\Send-M365Digest-BasicAuth.ps1 -ConfigMode Production# Test mode
.\Send-M365Digest-OAuth.ps1 -ConfigMode Test
# Production mode
.\Send-M365Digest-OAuth.ps1 -ConfigMode ProductionImport-Module .\M365DigestEmailModule.psm1 -Force -Verbose<!-- Card placeholders -->
CARD1_TITLE
CARD1_CONTENT
CARD1_LINK
CARD2_TITLE
CARD2_CONTENT
CARD2_LINK
CARD3_TITLE
CARD3_CONTENT
CARD3_LINK
<!-- Other placeholders -->
UNSUBSCRIBE_LINK
DISPLAYNAME$replacements = @{
'CARD1_TITLE' = 'Your Title Here'
'CARD1_CONTENT' = 'Your content here'
'CARD1_LINK' = 'https://example.com'
# ... repeat for CARD2, CARD3
}$inlineImages = @(
@{
ContentId = 'logo1' # Unique CID
FilePath = 'C:\temp\logo.png' # Actual file path
},
@{
ContentId = 'm365_icon'
FilePath = 'C:\temp\m365.png'
}
)<img src="cid:logo1" alt="Logo" width="130">
<img src="cid:m365_icon" alt="M365" width="100">email;DisplayName_email;password;secret_link
john@example.com;John Doe;Pass123;https://link1
jane@example.com;Jane Smith;Pass456;https://link2$csvData = Import-Csv -Path $csvPath -Delimiter ';' -Encoding UTF8
foreach ($row in $csvData) {
$email = $row.email
$name = $row.DisplayName_email
# Process row...
}$batchConfig = @{
BatchSize = 20 # 20 emails per window
WindowMinutes = 3.0 # 3 minute windows
MaxRetries = 3 # Retry 3 times
}$batchConfig = @{
BatchSize = 2 # 2 emails per window
WindowMinutes = 0.1 # 6 second windows
MaxRetries = 1 # Retry once
}$batchConfig = @{
BatchSize = 10 # 10 emails per window
WindowMinutes = 5.0 # 5 minute windows
MaxRetries = 5 # Retry 5 times
}$credential = Get-EmailAuthenticationCredential `
-AuthMethod 'Basic' `
-Username 'user@example.com' `
-Password 'YourPassword'$credential = Get-EmailAuthenticationCredential `
-AuthMethod 'OAuth' `
-Username 'user@example.com' `
-TenantId 'your-tenant-id' `
-ClientId 'your-client-id' `
-ClientSecret 'your-secret'If sending is interrupted:
- Don't panic - checkpoint file tracks sent emails
- Rerun the script - it automatically skips sent addresses
- Check checkpoint file to see what was sent:
Get-Content C:\temp\smtp_send_checkpoint.txt - Start fresh (optional) - delete checkpoint to resend all:
Remove-Item C:\temp\smtp_send_checkpoint.txt
# Check CIDs match
Select-String -Path .\M365_Digest_Template.htm -Pattern "cid:"
# Expected: cid:m365_icon, cid:exchange_icon, etc.
# Must match ContentId in $inlineImages array# Test manually
$cred = Get-Credential
Send-MailMessage -To 'test@example.com' `
-From 'sender@example.com' `
-Subject 'Test' -Body 'Test' `
-SmtpServer 'smtp.office365.com' -Port 587 `
-UseSsl -Credential $cred# Test token acquisition manually
$tokenUrl = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
$body = @{
client_id = $ClientId
client_secret = $ClientSecret
scope = "https://outlook.office365.com/.default"
grant_type = "client_credentials"
}
Invoke-RestMethod -Method Post -Uri $tokenUrl -Body $body# Count successfully sent emails
$sent = Get-Content C:\temp\smtp_send_checkpoint.txt
Write-Host "Sent: $($sent.Count) emails"# In another PowerShell window while campaign runs
Get-Content C:\temp\smtp_send_checkpoint.txt -Wait# Count sent every 10 seconds
while ($true) {
$count = (Get-Content C:\temp\smtp_send_checkpoint.txt).Count
Write-Host "Sent: $count emails @ $(Get-Date -Format 'HH:mm:ss')"
Start-Sleep -Seconds 10
}Get-Command -Module M365DigestEmailModule$images = @('logo.png', 'm365.png', 'exchange.png')
$images | ForEach-Object {
$path = "C:\temp\$_"
if (Test-Path $path) {
$size = (Get-Item $path).Length / 1KB
Write-Host "β $_ exists (${size} KB)"
} else {
Write-Host "β $_ NOT FOUND"
}
}$html = Get-Content .\M365_Digest_Template.htm -Raw
$html -match 'cid:' | Out-Null
$Matches$csv = Import-Csv .\recipients.csv -Delimiter ';' -Encoding UTF8
$csv | Select-Object -First 3 | Format-Table
$csv | Measure-Object | Select-Object Count# In HTML
<div>CUSTOM_FIELD</div>
# In script
$replacements['CUSTOM_FIELD'] = $row.CustomColumnif ($row.UserType -eq 'Premium') {
$replacements['CARD3_TITLE'] = 'Premium Features'
} else {
$replacements['CARD3_TITLE'] = 'Upgrade Available'
}$smtpConfig.Subject = "Digest for $($row.DisplayName_email)"Before running production campaign:
β‘ Test configuration validated (green)
β‘ Test email sent successfully
β‘ HTML renders correctly in Outlook/Gmail
β‘ Images display correctly
β‘ All links functional
β‘ Unsubscribe link works
β‘ Batch size appropriate for volume
β‘ Authentication credentials secured
β‘ Checkpoint file path configured
β‘ BCC address set for monitoring
β‘ CSV file has valid email addresses
β‘ Backup checkpoint file: Copy-Item checkpoint.txt checkpoint.bak
If you need to stop a running campaign:
- Press Ctrl+C in PowerShell window
- Checkpoint is safe - already-sent emails are tracked
- Resume later by rerunning the same script
- Test first: Always run with
-ConfigMode Testinitially - Monitor logs: Keep PowerShell window visible during send
- Check inbox: Send test email to yourself first
- Verify images: Open test email in multiple clients (Outlook, Gmail, Apple Mail)
- Backup checkpoint: Copy checkpoint file before production runs
- Stagger sending: Use WindowMinutes=5 for large campaigns (>1000 emails)
Need more help? See full README.md for detailed documentation.