Send a Mailgun email via PowerShell

Mailgun is a popular email delivery service that is built for developers and combines high reliability with scalability.

PowerShell is a cross-platform scripting language that allows tasks to be automated on Windows, Linux, and macOS using the power of .NET.

When developing PowerShell scripts that are running automatically on a schedule, it’s often useful to make these scripts capable of notifying us via email whenever errors or other events occur. While Mailgun provides lots of great documentation on how to send emails using their API, there isn’t currently any official documentation on how to send a Mailgun email using PowerShell.

In this article, I share a PowerShell function that simulates the built-in Send-MailMessage cmdlet, providing a way to call the Mailgun API natively within PowerShell scripts.

Mailgun setup

If you haven’t heard of Mailgun before I encourage you to head over to the official Mailgun website and check it out.

You can sign up for a free account on the sign-up page to get started.

After you’ve signed up and logged into the Mailgun portal, click on the ‘Sending’ link on the left-hand navigation pane and then press the ‘Select’ button within the ‘API’ box.

Following this, the page will display your API key, API base URL, and some sample code for sending a Mailgun email.

Mailgun portal
Mailgun portal

As you can see from the screenshot above, there is sample code available for cUrl, Python, Java, etc. but there is no code sample for PowerShell.

Of course, we could just use cURL from within our PowerShell script, but wouldn’t it be nice to have something that integrates more seamlessly?

Let’s look at implementing a suitable PowerShell function in the next section.

PowerShell

PowerShell provides a built-in Send-MailMessage cmdlet that allows emails to be sent via the standard SMTP protocol.

However, the official Microsoft documentation clearly states that the Send-MailMessage cmdlet is obsolete. This is due to the cmdlet using the deprecated SmtpClient class underneath.

The documentation also states that there is no immediate replacement for Send-MailMessage. This is where the Mailgun API comes into play, allowing us to send emails via a REST API endpoint. As a result, this helps to work around any potential firewall/port or other security issues and abstracts away the SMTP protocol from us.

Function

The PowerShell Send-MailgunMessage function shown below imitates the built-in Send-MailMessage cmdlet, providing most of the same parameters.

The function has been developed to work with PowerShell v5.1 or greater, offering a high degree of compatibility across systems that are reasonably up to date.

If you’re new to PowerShell, feel free to check out my PowerShell Quickstart blog article before studying the code samples in the current article.

function Send-MailgunMessage(
 
    [Alias("PsPath")]
    [ValidateNotNullOrEmpty()]
    [string[]]$Attachments,
 
    [ValidateNotNullOrEmpty()]
    [string[]]$Bcc,
 
    [ValidateNotNullOrEmpty()]
    [string]$Body,
 
    [Alias("BAH")]
    [switch]$BodyAsHtml,
 
    [ValidateNotNullOrEmpty()]
    [string[]]$Cc,
 
    [Parameter(Mandatory=$true)]
    [ValidateNotNullOrEmpty()]
    [string]$From,
 
    [Parameter(Mandatory=$true)]
    [ValidateNotNullOrEmpty()]
    [string]$Domain,
 
    [ValidateNotNullOrEmpty()]
    [System.Net.Mail.MailPriority]$Priority,
 
    [string[]]$ReplyTo,
 
    [Alias("sub")]
    [string]$Subject,
 
    [Parameter(Mandatory=$true)]
    [ValidateNotNullOrEmpty()]
    [string[]]$To,
 
    [ValidateNotNullOrEmpty()]
    [System.Management.Automation.PSCredential]$Credential,
 
    [switch]$UseSsl
)
{
    <#
    .DESCRIPTION
        Sends an email message via the Mailgun API.
 
    .PARAMETER Attachments
        Specifies the path and file names of files to be attached to the email message.
 
    .PARAMETER Bcc
        Specifies the email addresses that receive a copy of the mail but are not listed as recipients of the message.
 
    .PARAMETER Body
        Specifies the content of the email message.
 
    .PARAMETER BodyAsHtml
        Specifies that the value of the Body parameter contains HTML.
 
    .PARAMETER Cc
        Specifies the email addresses to which a carbon copy (CC) of the email message is sent.
 
    .PARAMETER From
        Specifies the sender's email address.
 
    .PARAMETER Domain
        Specifies the Mailgun Domain to use.
 
    .PARAMETER Priority
        Specifies the priority of the email message.
 
    .PARAMETER ReplyTo
        Specifies additional email addresses (other than the From address) to use to reply to this message.
 
    .PARAMETER Subject
        Specifies the subject of the email message.
 
    .PARAMETER To
        Specifies the recipient's email address.
 
    .PARAMETER Credential
        Specifies the API credentials to use.
 
    .PARAMETER UseSsl
        Specifies that the message must only be sent over a TLS connection.
 
    .EXAMPLE
        Send-MailgunMessage 
            -From "Your Name <mailgun@YOUR_DOMAIN_IDENTIFIER.mailgun.org>"
            -To "YOUR_IDENTIFIER@example.com"
            -Subject "Test Subject"
            -Body "Test Body"
            -Domain "YOUR_DOMAIN_IDENTIFIER.mailgun.org"
            -Credential (New-Object System.Management.Automation.PSCredential("api", (ConvertTo-SecureString "YOUR_API_KEY" -AsPlainText -Force)))
 
    .NOTES
        Returns an API response similar to the following example.
 
        {
          "message": "Queued. Thank you.",
          "id": "<20111114174239.25659.5817@samples.mailgun.org>"
        }
    #>
    
    # Build up the request parameters as a set of key-value pairs within an array.
    $requestParameters = [System.Collections.ArrayList]@(
        @{"from" = $from}
    )
 
    # Add the subject, if specified.
    if ($Subject.Length -gt 0)
    {
        $requestParameters.Add(@{"subject" = $Subject})
    }
 
    # Determine the correct parameter to hold the message body.
    if ($BodyAsHtml)
    { 
        $requestParameters.Add(@{"html" = $Body})
    }
    else
    { 
        $requestParameters.Add(@{"text" = $Body})
    }
 
    # Add the specified To, CC, and BCC addresses.
    $To  | ForEach-Object { $requestParameters.Add(@{"to"  = $_}) }
    $Cc  | ForEach-Object { $requestParameters.Add(@{"cc"  = $_}) }
    $Bcc | ForEach-Object { $requestParameters.Add(@{"bcc" = $_}) }
 
    # Add the specified 'Reply To' addresses.
    $ReplyTo | ForEach-Object { $requestParameters.Add(@{"h:Reply-To" = $_}) }
 
    # Set the Priority and TLS options.
    $requestParameters.Add(@{"h:X-Priority"  = [int]$Priority})
    $requestParameters.Add(@{"o:require-tls" = $UseSsl})
 
    # Determine the best Content Type depending on whether or not there are file attachments.
    $contentType = "application/x-www-form-urlencoded"
    $requestBody = ""
 
    if ($Attachments.Length -gt 0)
    {
        # If there are file attachments, add these to the request parameters.
        $Attachments | ForEach-Object { $requestParameters.Add(@{"attachment" = $_}) }
 
        # When attaching files the 'multipart/form-data' Content Type must be used.
        $boundary    = [Guid]::NewGuid().ToString()
        $contentType = "multipart/form-data; boundary=$boundary"
 
        # Add each request parameter with the appropriate formatting.
        foreach ($parameter in $requestParameters)
        {
            foreach ($key in $parameter.Keys)
            {
                $value = $parameter[$key]
 
                # Add the boundary divider.
                $requestBody += "--$boundary`r`n"
                $requestBody += "Content-Disposition: form-data; name=`"$key`""
 
                if ($key -eq "attachment") 
                {
                    # Get the filename and contents.
                    $fileName     = [System.IO.Path]::GetFileName($value)
                    $encoding     = [System.Text.Encoding]::GetEncoding("ISO-8859-1")
                    $fileContents = $encoding.GetString(([System.IO.File]::ReadAllBytes($value)))
 
                    # Add the filename and contents to the request body.
                    $requestBody += "; filename=`"$fileName`"`r`n"
                    $requestBody += "Content-Type: application/octet-stream"
                    $requestBody += "`r`n`r`n" + $fileContents + "`r`n"
                }
                else
                {
                    # Add the parameter value to the request body.
                    $requestBody += "`r`n`r`n" + $value + "`r`n"
                }
            }
        }
 
        # End with a final boundary divider.
        $requestBody += "--$boundary--"
 
        <#
        EXAMPLE REQUEST BODY
 
        --7c386e61-e7ab-4095-a8a7-28542010bc8e
        Content-Disposition: form-data; name="from"
 
        Your Name <mailgun@YOUR_DOMAIN_IDENTIFIER.mailgun.org>
        --7c386e61-e7ab-4095-a8a7-28542010bc8e
        Content-Disposition: form-data; name="subject"
 
        Test Subject
        --7c386e61-e7ab-4095-a8a7-28542010bc8e
        ...
        etc.
        #>
    }
    else
    {
        # When there are no file attachments, the 'application/x-www-form-urlencoded' 
        # Content Type can be used for greater efficiency.
        $parameterNumber = 1
 
        # Add each request parameter with the appropriate formatting.
        foreach ($parameter in $requestParameters)
        {
            foreach ($key in $parameter.Keys)
            {
                $value = $parameter[$key]
                
                $ampersand = "&"
 
                if ($parameterNumber -eq 1)
                {
                    # Don't add an ampersand character to the first parameter.
                    $ampersand = ""
                }
 
                # Add the parameter value with URL encoding to the request body.
                $requestBody += "$ampersand$key=$([Uri]::EscapeDataString($value))"
                
                $parameterNumber++
            }
        }
 
        <#
        EXAMPLE REQUEST BODY
        from=Your%20Name%20%3Cmailgun%40YOUR_DOMAIN_IDENTIFIER.mailgun.org%3E&subject=Test%20Subject&...etc.
        #>
    }
 
    # Call the Mailgun API and return the result.
    $result = Invoke-RestMethod `
        -Method "POST" `
        -Uri "https://api.mailgun.net/v3/$Domain/messages" `
        -Credential $Credential `
        -Body $requestBody `
        -ContentType $contentType
 
    return $result
}

Let me break down the key aspects of the above function for you in the following sub-sections.

Parameters

The Send-MailgunMessage function parameters are very similar to the built-in Send-MailMessage cmdlet and are defined in the same order as per the official documentation.

A notable exception is the Domain parameter that more or less replaces the SmtpServer parameter defined by the Send-MailMessage cmdlet. Since we’re calling the Mailgun API, the API Base URL is built up within the function based on the Domain, rather than specifying an SMTP Server host address.

Additionally, the following parameters are not supported by the function.

  • Encoding
  • DeliveryNotificationOption
  • Port (not required)

However, the function supports everything else, allowing multiple ‘To’ addresses and ‘CC’ addresses etc. to be passed in, as well as multiple file attachments and HTML body formatting if required.

I’ve based the parameter types and attributes on the official Send-MailMessage documentation and the C# implementation of the Send-MailMessage cmdlet on GitHub. The ValidateNotNullOrEmpty attribute is used on most parameters to disallow empty arrays or empty strings. Some of the parameters are marked as mandatory.

Parameter documentation

It’s good practice to document the parameters of your PowerShell functions.

I’ve lifted part of the help text for most of the parameters from the official Send-MailMessage documentation to help keep things as consistent as possible.

I’ve also added a note to document the type of response that the Mailgun API should return.

Building the request parameters

The first section of the function body deals with building up the request parameters that need to be passed to the Mailgun API.

An ArrayList containing a collection of hash tables (with a single key-value pair) is used to build the parameter names and values based on the specified function parameters.

By using the ForEach-Object cmdlet to add the address type parameters, the function is able to avoid too many conditional statements in this section that would otherwise clutter up the code unnecessarily.

All of the request parameter names (e.g. “h:Reply-To”) are based on the Mailgun API documentation.

Building the request body

The most involved part of the function is where the request body for the API call is built up.

Form Data

If any file attachments have been specified, the request body must use the ‘multipart/form-data’ Content Type. This involves separating each request parameter with a boundary string that tells the server where the details of each parameter start and end. The function uses a Guid to generate a globally unique identifier that will act as the unique boundary for the request.

The code is basically iterating through each parameter and building up the request body in the required format, with the file contents being read into a byte array and then converted to an encoded string that is placed in the request body.

I’ve included an example of the request body as a comment in the function to help with visualising the end result.

You can read more about the ‘multipart/form-data’ Content Type here.

URL Encoded

If there are no file attachments, the request body can be formatted using the ‘application/x-www-form-urlencoded’ Content Type, which is much simpler and concise than Form Data. Request parameters are separated with an ampersand (&) character and parameter names and values are joined with the equals (=) character.

The static EscapeDataString of the .NET Uri class is used to properly URL-encode each value.

An example of a URL-encoded request body is included as a comment in the function for reference.

Calling the API endpoint

The Send-MailgunMessage function uses the built-in Invoke-RestMethod cmdlet to make the Mailgun API call and returns the result to the caller.

The API call is a POST request to the Mailgun ‘Messages’ endpoint.

Credentials are passed to the API via the Credential parameter.

The Form Data or URL-encoded string that was built up is passed as the request body, along with the corresponding Content Type for the request.

Note that newer versions of the PowerShell Invoke-RestMethod cmdlet feature a Form parameter that makes sending Form Data much easier. However, the approach taken in the function we’ve walked through allows older versions of PowerShell to be supported for maximum compatibility across systems.

Calling the function

The Send-MailgunMessage function can be called like any other PowerShell function or cmdlet.

The code below demonstrates how to call the function with the full set of possible parameters.

$domain      = "YOUR_DOMAIN_IDENTIFIER.mailgun.org"
$apiKey      = ConvertTo-SecureString "YOUR_API_KEY" -AsPlainText -Force
$credentials = New-Object System.Management.Automation.PSCredential("api", $apiKey)
 
$result = Send-MailgunMessage `
    -From "Your Name <mailgun@YOUR_DOMAIN_IDENTIFIER.mailgun.org>" `
    -To "FIRST_IDENTIFIER@example.com", "SECOND_IDENTIFIER@example.com" `
    -Cc "THIRD_IDENTIFIER@example.com" `
    -Bcc "FOURTH_IDENTIFIER@example.com" `
    -ReplyTo "FIFTH_IDENTIFIER@example.com" `
    -Subject "Test Mailgun API email" `
    -Body "Hi, this is a test <b>Mailgun</b> message." `
    -BodyAsHtml `
    -Attachments ".\Test1.txt", ".\Test2.txt" `
    -Priority Normal `
    -Domain $domain `
    -Credential $credentials `
    -UseSsl
 
Write-Host "Result: $($result.message)"

If you want to simplify things, please be aware that the only required parameters are From, To, and Domain. You’ll also need to pass a Credential object, and ideally, your email should have a Body and Subject.

Note that the PowerShell Credential type requires the ‘password’ to be specified as a SecureString. It’s important to make sure you are considering security requirements when dealing with credentials.

To make things more readable and to avoid some code duplication, a few variables are initialised before calling the Send-MailgunMessage function. To test things out, replace the value of the variables with your Mailgun Domain and API key. You should also replace the email addresses and other values that are passed into the Send-MailgunMessage function accordingly.

If the function succeeds, a message that is similar to the following should be output to your terminal.

Result: Queued. Thank you.

Assuming that you specified your own email as one of the addresses to send the email to, now check your inbox!

Note, try checking the ‘Spam’ folder within your email client if there’s no sign of the message being received.

Summary

In this article, I shared a PowerShell Send-MailgunMessage function that imitates the functionality of the built-in Send-MailMessage cmdlet to send an email via the Mailgun API.

I started by pointing you in the right direction in regards to setting up Mailgun if you aren’t currently familiar with it.

I then provided the code listing for the custom Send-MailgunMessage function and walked through its implementation before demonstrating how to call the function to test it out.


I hope you enjoyed this post! Comments are always welcome and I respond to all questions.

If you like my content and it helped you out, please check out the button below 🙂

Comments