You’ve Got Mail! Automating a Welcome Email with PowerShell

Today, we are going to send a simple “Welcome to the company!” email with an attachment to a newly created email address. Even though we are on-prem, let’s leverage MSGraph because I need to get more acquainted with Graph commands. This is relatively easy with the trickiest part being credential management.

We’re going to be managing two sets of credentials:

  1. On-prem creds for sending an email
  2. Entra creds for pulling newly created users

Realistically, we can probably use on-prem creds for everything because Entra is just pulling the AD user properties, but this breakdown can help cloud only environments, and we as admins need to learn the new stuff.

TL;DR >>>>> GitHub

Section 1 | App Registration

I’m going to copy/paste from a prior post of mine, MFA Token Zapping, in order to save some finger strength for the script breakdown.

Lets make a Registered App in Entra ID. Simply login to EntraID with an account that has at least the Application Developer permission assigned. Now navigate to the following in EntraID: Applications > App registrations and select “New registration at the top”

Now enter a superficial name that sums up your use, and be sure to select the “Accounts in this organizational directory only” radio button under the “Supported account types” selection.

Now that you have created the app, go to “Certificates & secrets” in the blade menu: create a new client secret, enter a description and set your expiration accordingly.

Now save a copy of your Secret ID somewhere safe. We will need it for this next step. If you leave this page without saving, it will obscure and you cannot retrieve it again.

Next, let’s encrypt our registered app’s secret ID by utilizing another of my scripts. This is a two part process. Check out the script here on GitHub: Encrypt-GraphAppReg.ps1

Run part one to create an encryption key. This will output to the path of your choosing.

$key = New-Object byte[] 32
[Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($key)

# Save the key to a file (replace path if you want)
$keyFilePath = "<path>\graphAes.key"
$key | Set-Content -Path $keyFilePath -Encoding Byte

Write-Host "AES key saved to $keyFilePath"

Follow up with part two, where we will be prompted for our Secret ID, which will be encrypted and written to a .txt file.

# Read the key you saved earlier
$keyFilePath = "<path>\graphAes.key"
$key = Get-Content -Path $keyFilePath -Encoding Byte

# Prompt for your client secret securely
$secureSecret = Read-Host "Enter your Azure AD App Client Secret" -AsSecureString ## when prompted enter in the Value info from the app registration

# Encrypt the secret using the AES key and convert to string
$encryptedSecret = $secureSecret | ConvertFrom-SecureString -Key $key

# Save the encrypted secret to a file (replace path if needed)
$secretFilePath = "<path>\graphSecret.txt"
Set-Content -Path $secretFilePath -Value $encryptedSecret

Write-Host "Encrypted client secret saved to $secretFilePath"

Now that we have our .key file and our .txt file saved to the path of our choosing, let’s take a deeper look at the main star of this write up: Refresh-M365Tokens.ps1

First, flip back to EntraID and get the App ID for your newly created registered app.

Additionally, ensure that the registered app has the following EntraID permissions:

Microsoft Graph:
Directory.Read.All
Directory.ReadWrite.All
User.Read.All
User.ReadWrite.All

Section 2 | On-prem Credentials

Next, we’re going to encrypt the credentials of the account that will be sending the email. Use this block of code to encrypt your creds.

$Cred = Get-Credential
$key = (1..32 | ForEach-Object { Get-Random -Maximum 256 })
$EncryptedPassword = $Cred.Password | ConvertFrom-SecureString -Key $key
$Cred.UserName | Out-File "C:\temp\User.txt"
$EncryptedPassword | Out-File "C:\temp\Password.txt"

This will output to C:\temp. Remember to handle these two files cautiously. Encryption only goes so far. You should apply sufficient ACLs on these two files: User.txt and Password.txt

Section 3 | Meat n’ Potatoes – Let’s Break Down the Script

First we need to import some modules for MSGraph, fill out some variables, and declare our handy Write-Log function.

I did a pretty good job breaking down what needs filled in by you, dear sysadmin, via comments.

#region Import Modules Declare Variables and Functions
Import-Module Microsoft.Graph.Authentication -ErrorAction Stop
Import-Module Microsoft.Graph.Users -ErrorAction Stop

#Email Configs
$EmailFrom = "<enter sender email address>"
$Subject = "<enter subject>"
$Body = "<enter body>"
$AttachmentPath = "<enter path to file>" 
$SmtpServer = "<enter SMTP server>"

#Email Creds
$UserName = Get-Content "<path to encrypted username>"
$EncryptedPassword = Get-Content "<path to encrepted password>"
$key = Get-Content "<path to encryption key>"
$SecurePassword = ConvertTo-SecureString $EncryptedPassword -Key $key
$Cred = New-Object System.Management.Automation.PSCredential ($UserName, $SecurePassword)

#Tenant & App Ids
$appId = "<enter app registration ID"
$tenantId = "<enter tenant ID>"

function Write-Log {
    param(
        [string]$Message,
        [string]$Level = "Info",
        [string]$LogFile = "C:\Scripts\Logs\Email.log"
    )
    $Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $LogEntry = "$Timestamp [$Level] $Message"
    Add-Content -Path $LogFile -Value $LogEntry -Force
}
#endregion

Now let’s connect to MSGraph. Ope! I forgot to mention this, but we all should know by now: if you’re connecting to MSGraph it is highly recommended to use PowerShell 7.

You’ll need to fill out the path to where you securely stored your AES key and the encrypted secret.

#region Connecting to Microsoft Graph
Write-Log -Message "Loading encrypted client secret and AES key"
$keyPath = "<enter path to AES key>"
$secretPath = "<enter path to secret>"
$key = [System.IO.File]::ReadAllBytes($keyPath)
$encSecret = Get-Content -Path $secretPath -Raw
$secureSecret = $encSecret | ConvertTo-SecureString -Key $key

Write-Log -Message "Connecting to Microsoft Graph using ClientSecretCredential"
$clientSecretCredential = [PSCredential]::new($appId, $secureSecret)
Connect-MgGraph -TenantId $tenantId -ClientSecretCredential $clientSecretCredential
Write-Log -Message "Connection to Microsoft Graph successful"
#endregion

Next up: the fun part! Let’s cycle through our users from MSGraph, where the CreatedDateTime property is greater than Today minus 1 (aka yesterday). Then run a foreach loop against those users. Foreach user in our array of newly created users, send an email. Throw this is a Try/Catch block for safe measures!

<!> Now, heads up! I use the Send-MailMessage cmdlet which is about to be deprecated. I didn’t know that when I composed this script. Thanks for the feedback from a friend on its upcoming deprecation. I’ll make changes and update accordingly in the future.

#region Get User | Send Email
Write-Log -Message "Getting Newly Created Users"
$Users = Get-MgUser -All -Property UserPrincipalName, CreatedDateTime | Where-Object {$_.CreatedDateTime -ge (Get-Date).AddDays(-1)} | Select-Object UserPrincipalName, CreatedDateTime
foreach ($User in $Users){
Try{
    Write-Log -Message "Found New User: $($User.UserPrincipalName)"
    Write-Log -Message "Sending Email to $($User.UserPrincipalName)"
    Send-MailMessage `
        -To $User.UserPrincipalName `
        -From $EmailFrom `
        -Subject $Subject `
        -Body $Body `
        -Attachments $AttachmentPath `
        -SmtpServer $SmtpServer `
        -Credential $Cred `
        -UseSsl
    }
    Catch {
        Write-Log -Message  "ERROR! : " $_
    }
}
#endregion

Section 4 | The End

Now run this as a scheduled task and you should be pumping out emails to accounts created the day before.

This wraps up today’s lesson. I think this is a pretty useful automation to manage the transmission of HR related documents for employee onboarding, or even just a simple warm welcome to a new employee. Whatever your use case is, pieces of parts of this script should reside in your IT toolbelt.

Don’t forget:

This project is provided “as is” without any warranty of any kind, express or implied. Use it at your own risk. The authors and contributors are not responsible for any damage, data loss, or other issues that may arise from using this software. You are solely responsible for any actions taken based on this code.