I’m currently involved in a large-scale migration to Microsoft 365 in an organisation with over 18.000 employees. As each step in a migration takes time, automation is a must. Also to inform users that their OneDrive for Business migration has been completed and ready to use. Email templates and PowerShell can help to automate this.
The solution in this post is actually something I inherited from a project a few years back. I recently decided to rewrite it, as there are plenty of benefits in using automated personalised emails and it was applicable to the project that I’m currently working on.
Setting the stage
Let’s say you need to migrate the local files and folders for a lot of users from a network drive to OneDrive for Business for a prolonged period. So, think about frequent migrations using a standardized automated process.
Once the migration completes, you need to inform the users that their OneDrive for Business drive has been populated with their files and ready to use. The best way to do this at scale, is to use a personalised email notification message.
Requirements
To do this with an attractive email message including corporate look and feel, you can use an HTML-file as a template.

As you can see in the example, the html-template contains several placeholders, recognizable by ~{placeholderName}~. This is basically the foundation of the personalized message. The template contains the static text and e.g. links to a support portal. The logo image (Contoso in the example), is embedded in the templates using base64 encoding. To generate the encoded image, upload the original image file to a popular conversion service like Base 64 Guru and include it in the HTML-template.
The HTML for the image would like something like this:
<img alt="Contoso logo" src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAkACQAAD/4QAiRXhpZgAA
TU0AKgAAAAgAAQESAAMAAAABAAEAAAAAAAD/4gKgSUNDX1BST0ZJTEUAAQE
AAAKQbGNtcwQwAABtbnRyUkdCIFhZWiAH4g........UUUAFFFFABRRRQAUUUUAFFF
FABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUU
AFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFAH/9k=" />
Please note that the example HTML has been truncated for display purposes. Usually the HTML would be a lot of lines.
Replacing placeholders
The logic behind the use of the HTML-template, is to replace the placeholders with actual data for each user. The input file (csv-format) with all required data would obviously be the same as for OneDrive provisioning and migration.
displayName | userPrincipalName | sourceFolder | url |
Adele Vance | AdeleV@contoso.com | \\srv-45-ad\homedrive\AdeleV | {} |
Bianca Peters | BiancaP@contoso.com | \\srv-45-ad\homedrive\BiancaP | {} |
Chris Green | ChrisG@contoso.com | \\srv-45-ad\homedrive\ChrisG | {} |
Once the csv-file is imported, the basic code in the script would be something like the following:
foreach ($row in $table) {
#Define the replacement parameters
$replaceParams = @{
displayName = $row.userPrincipalName
sourceFolder = $row.sourceFolder
url = $row.url
}
#Set the request parameters
$requestParams = @{
emailTemplatePath = "$templatesPath\OneDriveMigration_Anouncement.htm"
toEmailAddress = $row.userPrincipalName
fromEmailAddress = $migrationTeamEmail
emailSubject = 'Welcome to OneDrive for Business'
smtpServer = $smtpServer
smtpServerPort = $smtpServerPort
smtpCredential = $smtpCredential
replaceParams = $replaceParams
}
#Send the email
Send-FactoryEmail @requestParams
}
So, based on the input file data, you can create a $replaceParams splat including all elements to replace. This splat is than added to a $requestParams splat which includes all parameters for the ‘Send-FactoryEmail’ function. This also included the smtp server data to actually send the email.
Send-FactoryEmail
The magic happens in the ‘Send-FactoryEmail’ function.
function Send-FactoryEmail {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$emailTemplatePath,
[Parameter(Mandatory = $true)]
[string]$toEmailAddress,
[Parameter(Mandatory = $true)]
[string]$fromEmailAddress,
[Parameter(Mandatory = $true)]
[string]$emailSubject,
[Parameter(Mandatory = $true)]
[string]$smtpServer,
[Parameter(Mandatory = $true)]
[string]$smtpServerPort,
[Parameter(Mandatory = $false)]
[pscredential]$smtpCredential,
[Parameter(Mandatory = $true)]
[hashtable]$replaceParams)
#Get template for email Body
[string]$body = Get-Content -Path $emailTemplatePath
#Replace variables with parameter data
foreach ($replaceParam in $replaceParams.GetEnumerator()) {
$body = $body.Replace("~$($replaceParam.Name)~", $replaceParam.Value)
}
#Set the request parameters
$requestParams = @{
Subject = $emailSubject
Body = $body
BodyAsHtml = $true
To = $toEmailAddress
From = $fromEmailAddress
SmtpServer = $smtpServer
Port = $smtpServerPort
UseSsl = $true
}
#Add additional parameters based on input
if ($smtpCredential) {
$requestParams.Add('Credential', $smtpCredential)
}
#Send the message
Send-MailMessage @requestParams
}
The first part of the function is to import the HTML from the template as a string in to a variable called $body.
#Get template for email Body
[string]$body = Get-Content -Path $emailTemplatePath
Next, the replacement takes place for each of the provided replaceParams. Obviously, these have to match the placeholders in the template.
#Replace variables with parameter data
foreach ($replaceParam in $replaceParams.GetEnumerator()) {
$body = $body.Replace("~$($replaceParam.Name)~", $replaceParam.Value)
}
Notice, that the replace wraps the replaceParam names in ‘~’-characters as a naming convention for the placeholders in the template.
After the replacement completes, the body of the email has the personalised elements similar to a mail-merge in Microsoft Word.

Last, but not least, the email message needs to be send. Using the email and smtp server data, the ‘Send-Message’ function is called and the email is send out.
Conclusion
Automation can save time, especially in migrations at scale. And communication is a key part of a successful migration project. HTML templates ensure a professional look and feel of the message. The Communications department of your organisation can provide the foundational template to ensure compliance with corporate identity and the replacement parameters do the rest.
And it’s flexible through the function approach: multiple template files and replace parameters can support different scenarios in which people in the organisation need to be informed or called to action.