To start with
Initially I started out writing a generic Azure function, which I did before, not two months ago even. So I figured this was going to be an easy job. The premise was as follows: We have a contact form on the website, which was supposed to handle a simple thing: send an email to the standard email by simply grabbing the data from the form, and using this data to call the Azure function that would handle this data from a queue.
So I picked up what I remembered from a few months ago, and started writing.
The function
I went into the Azure Portal, and clicked through the resources to find the "Azure Function App" which should have already triggered my memory, as it was simply called "Azure Function" before. I created the resource, by filling in all the data Azure prompted me to fill in, and went over to the "Create New Function" section. Here, I was pointed to something that did trigger my memory. When I first created a function here, I had this "in the portal" editor where I could create the function and the rest would be handled for me in Azure. I didn't have to think about the rest of the handles, only about the triggers and the logic in the code itself. Now Azure was telling me to install an extension in VSCode, and create the entire thing in there. I asked myself, "How hard can this be?", and I installed the extension and other items as explained by Azure, and then followed the wizard that VSCode provided.
I used TypeScript as the language, and I noticed "SendGrid" as trigger amongst the options for triggers. After the wizard was done setting things up, I found an index.ts
file where my function was.
I hadn't heard of sendGrid before, so I went into the Microsoft Docs to read about it.
I should have known, the documentation provided me with little help on how to start this up, but rather with only options and endpoints I could use. Which, granted, isn't a bad thing, but wasn't what I was looking for...
This project turned into a "trial and error" kind of project, where I thought it would be a simple "add the function, and be done with it".
How to then?
After a lengthy search, trial, error, retry, and some more searching, Bryce found a blog post of someone who had set up this very thing with Azure Functions and SendGrid himself. This was a massive help!
First of all, I found that I had to make an account in SendGrid, and had to authenticate the cloudshift.nl
domain there, which required Bryce to enter some DNS records at the host.
SendGrid actually provides some good help and a step-by-step tutorial on this.
So I'm reading through this blog post, and sadly all the information provided was the code of the function, not how to call it from my app. But it was a start already.
I went ahead and read his blog, and found the working code for my problem to be the following:
const qs = require('qs');
const sgMail = require('@sendgrid/mail')
module.exports = async function (context, req) {
context.log('HTTP trigger function processed a request.');
let name = '';
let subject = '';
let email = '';
let content = '';
context.log(req);
if ( req.body ) {
let formData = qs.parse(req.body);
name = formData.name;
subject = formData.subject;
email = formData.email;
content = formData.content;
}
sgMail.setApiKey(process.env["SendgridApiKey"]);
const msg = {
to: 'info@cloudshift.nl',
from: 'website@cloudshift.nl',
subject: subject,
text: `${name} sent you the following message:\n${content}\n\nAfzender: ${email}`
}
context.log(msg);
await sgMail.send(msg);
context.res = {
status: 302,
headers: {
'Location' : '/'
},
body: 'Redirecting...'
}
context.done(null, context.res);
}
I just had to install qs
, and @sendgrid/mail
with node package manager, and I was already on my way there.
The API key from SendGrid, which was generated after the DNS records were authenticated and verified, was placed within the Azure Portal inside the configuration under SendgridApiKey
. The value for this configuration was the key I generated in SendGrid.
Technically this should be an easy change when the account that holds SendGrid changes from my email address to another email address.
From here, I looked up how to continue, but the blog post ended here, and locally testing this resulted in a success. So what to do now? How do I call this function from my API? I found somewhere online that these functions should have a url to call the function, and in the headers I should provide the data I wanted to mail. Searching in the Azure Portal did not result in a url to call the function. So how then?
The website
Once again, after a lengthy search, Bryce once again found an article that said I had to add all of the files of the Azure Function within an "api" folder inside the static website we were building. From here it was simply calling it by a route. The given example of this was /api/message
, but we called the function CloudShiftMailer
, and the folder structure was different from the example. That is a simple translation, though.
So inside the component we used on the static website, I tried to use node-fetch
to call the function using the route /api/CloudShiftMailing
This resulted in a 500 error, and the other routes gave a 404.
Oops?
The kind of errors seen in the browser are limited in their information, and I needed to get my hands on more detailed information.
Eventually this meant I had to gain access to the website's resource in Azure Portal where all of the API key information was placed, and where the website is hosted.
But we needed Application Insights to be activated for me to see what errors came my way when calling the function.
The simplest way to get the errors resolved was by using Application Insights' Live Metrics, and call the API once again. Here I found that using import sgMail from "@sendgric/mail"
was not allowed, and using require()
inside TypeScript was going to be too much of a pain and hassle to make work.
So, I created a new function in VSCode but this time with JavaScript as base language, and node 14 LTS and re-wrote my code into the example seen above.
Then, finally inside the website near the contact form, I created a script that sent an email from the website using a fake email address but with the verified domain inside sendGrid. An an email was sent to an existing mail address inside the verified domain, using the user's input to create a message.
Here's the eventual code I created:
async function submit() {
const sendData = {
name: name,
email: email,
subject: subject,
content: content
}
let headers = new Headers();
headers.append("Content-Type", "application/json");
let options = {
method: 'POST',
headers: headers,
body: JSON.stringify(sendData)
}
await fetch('/api/mailing', options)
.then( async (response) => console.log(response))
.catch( (err) => console.log(err) );
}
Here I have the constant sendData
, which holds all the data the user provided in the form. We have a simple header with only Content-Type: application/json
in it, and the method is POST, so the azure function will actually send the mail instead of storing it.
The body holds all the data as a Json Object.
Now for the fetch()
part. This was a bit finicky, but when I had application insights, I quickly came to a solution.
The url finally became "/mailing", because inside my function.json
I added a parameter "route" with the value "mailing". This turned the route into a more readable, and less CamelCased url. I added .then( (response) => console.log(response) )
only for the purposes of getting a confirmation that the email was sent. And sure enough, I finally had my 200 success! Perfect.
We checked the inbox of the email address we used and sure enough, we had an email!
The relevant code
The files that were most relevant to the success of this function were the following files:
function.json
{
"bindings": [{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"get",
"post"
],
"route": "mailing"
},
{
"type": "http",
"direction": "out",
"name": "res"
}]
}
index.js
const qs = require('qs');
const sgMail = require('@sendgrid/mail')
module.exports = async function (context, req) {
context.log('HTTP trigger function processed a request.');
let name = '';
let subject = '';
let email = '';
let content = '';
context.log(req);
if ( req.body ) {
let formData = qs.parse(req.body);
name = formData.name;
subject = formData.subject;
email = formData.email;
content = formData.content;
}
sgMail.setApiKey(process.env["SendgridApiKey"]);
const msg = {
to: 'info@cloudshift.nl',
from: 'website@cloudshift.nl',
subject: subject,
text: `${name} sent you the following message:\n${content}\n\nAfzender: ${email}`
}
context.log(msg);
await sgMail.send(msg);
context.res = {
status: 302,
headers: {
'Location' : '/'
},
body: 'Redirecting...'
}
context.done(null, context.res);
}
package.json
{
// basic information
// dependencies were important
"dependencies": {
"@sendgrid/mail": "^7.6.1",
"qs": "^6.10.3"
},
"devDependencies": {
"@azure/functions": "^1.2.3"
}
}
The rest was left on default, and only in local.settings.json
did I toss my api key for local testing. But this proved to be more tricky, and probably subject to another blog.