How to send an email with PowerShell using SendGrid API

I really like event-driven notifications that can trigger different webhooks and it’s really fun putting them together like pieces of lego to automate workflows.

The most common and simple notification method is via email, but there are scenarios where environments for security reasons and by design have just access to the internet on port 80/443  and this connection is often mediated via a web proxy.

Not having access to SMTP protocol can be a roadblock but, in this article, we will implement a solution to send an email with PowerShell under these limitations without changing Firewall Rules or NSG.

How can we send an email without SMTP (or IMAP)?

SMTP protocol requires TCP port 25 or 587 (with TLS encryption) as the default port for mail submission if those ports are closed my obvious choice is using a web service that will require an API Key as authentication and authorization via web request on HTTPS (using port 443).

In this example, I used Twilio SendGrid (This is not a sponsored article).  It’s very popular and the free tier for this application is probably enough for most testing applications or scenarios.

Once you’ve signed up and created your profile you can click on Settings, API KEYS (https://app.sendgrid.com/settings/api_keys) and CREATE API KEY.

My Personal TIP:  Remember that API KEYs are free, generate one for each of your apps, so it can be considered as a unique identifier so if it gets compromised it’s possible to identify it and to delete it without causing any interruption for other apps using/sharing the same service or account.

Once created, you’ve got everything you need to start using this script.

Send-MailWithSendGrid

I’ve created a PowerShell function that is a wrapper for Invoke-RestMethod and posts the JSON containing the email message the API Key that SendGrid is expecting to receive.

To use this function you can simply import with dot sourcing

Replace these variables with your test values

And finally, run this one-liner

Take advantage of Splatting

Oliver in one of the comments below has pointed out the use of parameter splatting. Indeed, if you have a long list of parameters to pass to a cmd-let or a function in genera is to pack all your variables into a hast table and pass that object to the function.

There are multiple benefits of splatting,  it will not just make your code more readable, but all parameters will be stored into a single object ( hash table),  that will be easy to manipulate, count or iterate if needed.

If you require a proxy

As I’ve mentioned this function is a wrapper of Invoke-RestMethod so it’s simple to check the documentation and for instance adding the proxy address to the Invoke-RestMethod cmd-let.

Wrap-Up

I still think that email communication can be still used effectively today and the choice of having other valid alternatives, for instance, Teams/Slack , can help us to make the right decision when it is okay to use it or simply avoid abusing it.
When we decide that the email is the right solution even when SMTP protocol is not available we can replace the cmd-let of Send-MailMessage with this Send-EmailWithSendGrid using the SendGrid RESTAPI.
As usual, you can find this script on my GitHub Repository.

28 Replies to “How to send an email with PowerShell using SendGrid API”

  1. Hello Paolo,
    can I suggest the use of splatting to pass the parameters to your function
    i.e.
    $MailParams = @{
    From = “email@address”
    To = “email@address”
    APIKEY = “MY_API_KEY”
    Subject = “TEST”
    Body =”SENDGRID 123″
    }
    Send-EmailWithSendGrid @MailParams

    The commandline is shoter, and more readable and easily customizable.

    Regards
    Olivier

    1. Hi Oliver,
      Thanks for your comment, yes splatting can make the line for the cmd-let shorter, as much as I like hash tables and I wanted to make the code even simpler.
      I will edit this post and include your comment, I didn’t think to include splatting in this, but, in general, you’re right and it’s a nice option to have all parameters into a single hash table object if you want for instance iterate or manipulate it.

    1. Hi Spizzy,
      Thanks for your comment. According to the official documentation of SendGrid https://sendgrid.com/docs/api-reference/ you can send attachments(an array of objects) but you need to convert your file with base64 into a string the attachment yourself you cannot pass the absolute path of the file.
      Each attachment in the array will be a set of 2 required strings filename and content.
      I suggest breaking this into a separate helper function where you pass a filename as an argument and it returns 2 strings (or a hash table) filename and content. So you can keep your code still close to a one-liner.

      I will publish an article tomorrow on how to encode in base64 using .NET framework, but I’ve already pasted in my github repository the code if you want to have a look: https://github.com/PaoloFrigo/scriptinglibrary/blob/master/Blog/PowerShell/Encode-FileToBase64.ps1

      I hope that I have answered your question. Regards

      1. Hey Paolo, this helps. That part im missing is what the json request with the attachment part added in. json is totally new to me and the structure is strange. how would the attachment in the json structure look?

        thanks, sir.

        1. Hi Spizzy,
          I strongly encourage you to get familiar with XML, JSON, YAML. It doesn’t matter what role you’re in right now, you’ll soon need to use them and know pros and cons of all of these especially when you are the developer and you need to choose which one to implement.

          Have you read this article on how to convert a file into a base64 string?
          https://www.scriptinglibrary.com/languages/powershell/base64-encoding-with-powershell-and-net-framework/

          My script above doesn’t require an attachment, but on the JSON request add the previous article and this code, it should be a matter of just joining all the pieces together.


          attachments: [
          {
          content: "attachment_converted_into_base64string",
          filename: "name_of_your_attachment_with_extension",

          }
          ]

          Experiment and let me know if you need help.

          Regards

    2. Use the below code

      $attachmentContent = @()
      foreach($file in $AttachmentFileArray)
      {
      $attachmentContent += New-Object PSObject -Property @{
      ‘content’ = [Convert]::ToBase64String([IO.File]::ReadAllBytes($file))
      ‘filename’ = $file.Split(‘\’)[-1]
      }
      }

  2. I have been trying to send to multiple recipients using Splatting but getting the below error, Please help.

    To = “[email protected]” , “[email protected]
    ——————————————————————–
    Send-EmailWithSendGrid : Cannot process argument transformation on parameter ‘To’. Cannot convert value to type System.String.
    At line:82 char:24
    + Send-EmailWithSendGrid @MailParams
    + ~~~~~~~~~~~
    + CategoryInfo : InvalidData: (:) [Send-EmailWithSendGrid], ParameterBindingArgumentTransformationException
    + FullyQualifiedErrorId : ParameterArgumentTransformationError,Send-EmailWithSendGrid

    1. Hi Ayush,
      Thanks for your comment. If you look at the beginning of the Send-EmailWithSendGrid function definition you will notice that To is not an Array or List of Strings, but a simple String object. So the error you are getting is espected.

      If you want to pass an array of strings as mentioned in your comment changing that function is the first change needed.

      The second step and most important one is consulting the SendGrid API to double-check if that change is allowed:
      https://sendgrid.com/docs/API_Reference/Web_API_v3/Mail/index.html
      Ideally, knowing that function depends on this API this research should be done upfront to avoid wasting time.
      Yes, this is possible, the only limitation is that the size of that array must be less than 1000, so I suggest to validate the size of the To array and making your function a little bit smarter.

      This is what I’m referring to:

      The total number of recipients must be less than 1000. This includes all recipients defined within the to, cc, and bcc parameters, across each object that you include in the personalizations array.

      If you are not successful in your attempts, feel free to reach out.

      Another option (more like a workaround) if you want to send the same email to your list of recipients, you can call the same function sending it out one user at a time by simply using a for loop.
      This would be like sending individual emails, instead of an email with the same ‘to’ list of addresses in the mail header. In your comment I guess you want all users to know each other address and can reply-to-all, instead of in this workaround method each one will receive an email with one single address in the ‘to’ field.
      It may work for you anyway, but it depends on the result you want to achieve.

      Regards

  3. Hi Paolo,

    Thanks for your response.

    I am very new to PowerShell, I have tried to change the string type of $To to array of string but no lock. I am just trying to send emails to multiple recipients and will definitely less than 1000 recipients, not individually but to everyone added to the $To list. Also I have tried to add cc and bcc to the function it didn’t work.

    Can you please suggest what changes you are referring to in the above reply for changing the function which will allow me to send emails to multiple recipients. Also if you can suggest me how to add cc and bcc into the function.

  4. Is there a way to pass an array into the content?
    Much as the question above, but within the body

    So in this case, the result of a cmdlet call to a child runbook, which is stored as array.

    1. Hi Jamie,
      Thanks for your comment. This question is more about data types and conversions than using sendgrid api.

      I can show you a simple code snippet to hopefully will point you in the right direction:

      # This is a simple array
      $MyArray = 1..10

      # This is using a join command to turn it into a single string with the comma separator
      $MyString= $MyArray -join ","

      $MyArray is mocking the output of your cmd-let or function. $MyString will be the string variable that you can add the body.

      I hope that example is simple and clear enough.
      Thanks,
      Regards

    1. Hi Veera,
      Thanks for your comment. A zip file is like any other file or attachment so it needs to be encoded into base64 format, please see some of my previous responses.
      Regards

  5. I used your solution in my send email scripts, I added a couple things, now you can send to multiple emails by supplying a comma separated list of addresses, it also will send a bcc to another email – only 1 per SG api. The type is also html so you can send a html formatted email

    if($To.Contains(“,”) -eq $true){

    [array]$emails = $To.Split(“,”)

    }else{

    [array]$emails = $To
    }

    $jsonRequest = [ordered]@{ personalizations= @(
    @{to = @($emails | %{ @{email = “$_”} })
    bcc = @( @{email = “$($TechEmail)”})
    subject = “$SubJect” })
    from = @{email = “[email protected]”}
    content = @( @{ type = “text/html”
    value = “$body” })} | ConvertTo-Json -Depth 10

    1. Hi Mike,
      Thanks for your comment and for adding your code snippet. I’m sure it will be useful to other users as well!
      Regards

  6. A bit out of the wheel-house, runs but no error in PS and no email received. Any hints to share? Thanks in advance.

    1. Hi,
      thanks for your comment.

      The script is basically a one-liner, if all arguments passed are correct you should have the expected result.

      The only thing I would try is to double-check if I’ve copied correctly my API token and eventually review the official documentation
      https://sendgrid.com/docs/for-developers/sending-email/curl-examples/#hello-world
      to use the same API key with CURL like this one:

      curl –request POST \
      –url https://api.sendgrid.com/v3/mail/send \
      –header ‘Authorization: Bearer YOUR_API_KEY’ \
      –header ‘Content-Type: application/json’ \
      –data ‘{“personalizations”: [{“to”: [{“email”: “[email protected]”}]}],”from”: {“email”: “[email protected]”},”subject”: “Hello, World!”,”content”: [{“type”: “text/plain”, “value”: “Heya!”}]}’

      I hope this answers your question or at least it will point you in the right direction.

      Regards

  7. I still don’t get how to send to multiple recipients.
    function Send-SendGridEmail {
    param(
    [Parameter(Mandatory = $true)]
    [String] $destEmailAddress,
    [Parameter(Mandatory = $true)]
    [String] $fromEmailAddress,
    [Parameter(Mandatory = $true)]
    [String] $subject,
    [Parameter(Mandatory = $false)]
    [string]$contentType = ‘text/plain’,
    [Parameter(Mandatory = $true)]
    [String] $contentBody,

    [parameter(Mandatory = $false)]
    [string]$FileName,
    [parameter(Mandatory = $false)]
    [string]$FileNameWithFilePath,
    [parameter(Mandatory = $false)]
    [string]$AttachementType
    )

    ############ Update with your SendGrid API Key ####################
    $apiKey = “key”

    $headers = @{
    ‘Authorization’ = ‘Bearer ‘ + $apiKey
    ‘Content-Type’ = ‘application/json’
    }

    $base64string = [Convert]::ToBase64String([IO.File]::ReadAllBytes($FileNameWithFilePath))

    $body = @{
    personalizations = @(
    @{
    to = @(
    @{email = $destEmailAddress}
    )
    }
    )
    from = @{
    email = $fromEmailAddress
    }
    subject = $subject
    content = @(
    @{
    type = $contentType
    value = $contentBody
    }
    )
    attachments = @(
    @{

    #content=$EncodedFile
    #content=$content
    content=$base64string
    filename=$FileName
    type= $AttachementType
    disposition=”attachment”
    }
    )
    }

    try {
    $bodyJson = $body | ConvertTo-Json -Depth 4
    }
    catch {
    $ErrorMessage = $_.Exception.message
    write-error (‘Error converting body to json ‘ + $ErrorMessage)
    Break
    }

    try {
    Invoke-RestMethod -Uri https://api.sendgrid.com/v3/mail/send -Method Post -Headers $headers -Body $bodyJson
    }
    catch {
    $ErrorMessage = $_.Exception.message
    write-error (‘Error with Invoke-RestMethod ‘ + $ErrorMessage)
    Break
    }

    }

    #CALL
    $splat2 = @{
    destEmailAddress = ’[email protected],[email protected]
    fromEmailAddress = ‘[email protected]
    subject = ‘Test Email’
    contentType = ‘text/html’
    contentBody = $htmlBody
    FileName = “file.xlsx”
    FileNameWithFilePath = “path\file.xlsx”
    AttachementType = “application/vnd.openxmlformats-officedocument.spreadsheetml.sheet”
    }
    the above works for sending to one person but i don’t know the format for sending to multiple recipients. This will run in a loop so the number of recipients if dynamic.

    1. Hi Milan,
      Thanks for your comment, I’m happy that you are using the script and you have found the article useful.
      In one of the comments, Mike has suggested a solution to add multiple addresses, you should try that option if that is what you are looking for.

      Regards

      1. Thats the thing. My code strcuture is different because i used the skeleton from a different github. So i don’t see how to implement it in mine. This API is very new to me and I am having a hard time working it.
        Thank you for responding

      2. Hi Paolo,
        The trick to sending to multiple emails is creating the JSON request object with multiple to – I did it by checking the $To variable, if it is a comma separated list it created an array object using .split otherwise it creates an array of 1 email.

        if($To.Contains(“,”) -eq $true){
        [array]$emails = $To.Split(“,”)
        }else{
        [array]$emails = $To
        }

        Then when you create the JSNO object this line adds however many emails there are to the object
        @{to = @($emails | %{ @{email = “$_”} })

        part of this code block
        $jsonRequest = [ordered]@{ personalizations= @(
        @{to = @($emails | %{ @{email = “$_”} })
        bcc = @( @{email = “$($TechEmail)”})
        subject = “$SubJect” })
        from = @{email = “[email protected]”}
        content = @( @{ type = “text/html”
        value = “$body” })} | ConvertTo-Json -Depth 10

        Send Grid handles the looping etc, you can remove the bcc if you dont use it

  8. Hi Team,

    can we user Invoke-AzRestMenthod powershell command to call send grid api for sending mails?
    I got stuck in this.

    1. Hi, the article cover exactly this use case. The function is a wrapper of the Invoke-AzRestMethod, but writing a helper functions makes it reusable and more intuitive to leverage the SendGrid API in my opinion.

  9. Tagged off Mike’s additions.
    Added conditional CC.
    Added BCC, but commented out since that is less frequent (in our environment):

    function Send-EmailWithSendGrid {
    Param
    (
    [Parameter(Mandatory=$true)]
    [string] $From,

    [Parameter(Mandatory=$true)]
    [String] $To,

    [Parameter(Mandatory=$true)]
    [string] $ApiKey,

    [Parameter(Mandatory=$true)]
    [string] $Subject,

    [Parameter(Mandatory=$true)]
    [string] $Body,

    [Parameter(Mandatory=$false)]
    [string] $CCto,

    [Parameter(Mandatory=$false)]
    [string] $BCCto

    )

    ## Check for multiple addresses in the TO:
    if($To.Contains(“,”) -eq $true){
    [array]$emails = $To.Split(“,”) }
    else{
    [array]$emails = $To
    }

    $headers = @{}
    $headers.Add(“Authorization”,”Bearer $apiKey”)
    $headers.Add(“Content-Type”, “application/json”)

    if($CCto) {

    $jsonRequest = [ordered]@{ personalizations= @(
    @{to = @($emails | %{ @{email = “$_”} })
    cc = @( @{email = “$($CCto)”})
    # bcc = @( @{email = “$($BCCto)”})
    subject = “$SubJect” })
    from = @{email = “$From”}
    content = @( @{ type = “text/html”
    value = “$body” })} | ConvertTo-Json -Depth 10
    }
    else {
    $jsonRequest = [ordered]@{ personalizations= @(
    @{to = @($emails | %{ @{email = “$_”} })
    # bcc = @( @{email = “$($BCCto)”})
    subject = “$SubJect” })
    from = @{email = “$From”}
    content = @( @{ type = “text/html”
    value = “$body” })} | ConvertTo-Json -Depth 10
    }

    Invoke-RestMethod -Uri “https://api.sendgrid.com/v3/mail/send” -Method Post -Headers $headers -Body $jsonRequest

    }

    1. Hi Jay,
      Thanks for your comment. I have reviewed your code and I would suggest some minor changes avoiding some repetition and with the goal of making it shorter and simpler. I hope you’ll find them useful.

      function Send-EmailWithSendGrid {
      Param
      (
      [Parameter(Mandatory=$true)]
      [string] $From,
      [Parameter(Mandatory=$true)]
      [String] $To,
      [Parameter(Mandatory=$true)]
      [string] $ApiKey,
      [Parameter(Mandatory=$true)]
      [string] $Subject,
      [Parameter(Mandatory=$true)]
      [string] $Body,
      [Parameter(Mandatory=$false)]
      [string] $CCto,
      [Parameter(Mandatory=$false)]
      [string] $BCCto
      )
      [array]$emails = $To.Contains(“,”) -eq $true ? $To.Split(“,”) : $To

      $headers = @{}
      $headers.Add(“Authorization”,”Bearer $apiKey”)
      $headers.Add(“Content-Type”, “application/json”)

      $jsonRequest = [ordered]@{ personalizations= @(
      @{to = @($emails | %{ @{email = “$_”} })
      subject = “$SubJect” })
      from = @{email = “$From”}
      content = @( @{ type = “text/html”
      value = “$body” })}

      if($CCto) {
      $jsonRequest += [ordered]@{ cc = @( @{email = “$($CCto)”})}
      }
      if($BCCto) {
      $jsonRequest += [ordered]@{ bcc = @( @{email = “$($BCCto)”})}
      }
      $jsonRequest = $jsonRequest | ConvertTo-Json -Depth 10
      Invoke-RestMethod -Uri “https://api.sendgrid.com/v3/mail/send” -Method Post -Headers $headers -Body $jsonRequest
      }

      Please note that operators were added in Powershell core/v7 only. If you use an older version you would stick to your code.

      Regards

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.