This project uses Google Firebase as a back-end
for a basic email template system. It creates HTTP endpoints to generate emails
from templates. Requests include the name of the template file to use, and
what values to use for replacing the tags in the template. The system has two
endpoints for receiving requests: getEmail
responds back with the
generated email, and sendEmail
actually emails it after it is generated.
mustache.js is used for the template syntax, and SendGrid is used to send the emails.
I created this project as a learning exercise. It is not intended to be a robust and production-ready system. The method to prevent unauthorized connections is relatively basic, only one email can be included in each request, and the system has no mechanism to throttle requests.
Also, it's not practical to have the template files reside on the filesystem. A change to a template requires you to redeploy to Firebase. It would be better if the system used Firestore or Google Cloud Storage for the templates.
These instructions will get you a copy of the project up and running on your own system, and deploy it to Firebase.
- Node.js 14 and npm 7
- The Firebase CLI
- Google Cloud Platform account with a Billing Account. This is required to enable Cloud Functions, but you should remain well below free-tier usage during development.
- A SendGrid API Key for sending emails.
Log into the Firebase Cloud Console, and create a new Firebase Project. There is no need to enable Analytics on the project.
Go to the Functions page in the Cloud Console, and click the link to upgrade the project to the "Blaze" billing plan; a project must be assigned a Billing Account to use Cloud Functions. You shouldn't exceed the free tier usage during development, but you can set a Budget Alert for $1 just in case.
-
Go to the Firestore Database tab in the Firebase Cloud Console and click "Create database".
-
Make sure "Start in production mode" is selected, and click "Next".
-
Choose the Cloud Firestore location most suitable for your project, and click "Enable".
Go to the project's directory after downloading or cloning it to your system, and initialize Firebase:
cd my_project
firebase init
Make the following selections:
- Features: Functions, Emulators
- Use an existing project, then select your project
- Language: JavaScript
- ESLint: No
- Do NOT overwrite existing files in the /functions directory
- package.json
- index.js
- .gitignore
- Install dependencies: Yes
- Emulators: Functions Emulators
- Port: Choose the default port unless it conflicts with an existing service on your system
- Emulator UI: Optional
Copy functions/default_config.js
to functions/config.js
, and follow
the instructions in the file for filling in the values.
Run firebase functions:shell
on the command line and type install({})
to run the install endpoint (type .exit
to close the shell afterwards).
$ firebase functions:shell
⚠ functions: The Cloud Firestore emulator is not running, so calls to Firestore will affect production.
i functions: Loaded functions: install, getEmail, sendEmail
⚠ functions: The following emulators are not running, calls to these services will affect production: firestore, database, pubsub, storage
firebase > install({})
Sent request to function.
firebase > > {"verifications":{"app":"MISSING","auth":"MISSING"},"logging.googleapis.com/labels":{"firebase-log-type":"callable-request-verification"},"severity":"INFO","message":"Callable request verification passed"}
⚠ Google API requested!
- URL: "https://oauth2.googleapis.com/token"
- Be careful, this may be a production service.
> {"severity":"INFO","message":"No users, creating first..."}
RESPONSE RECEIVED FROM FUNCTION: 200, {
"result": "Initial account created. Send requests using Client ID L082w9MtPhgd7UXr2aeg and Key 9218cf63-5040-4ea3-b6bb-8d5f98469a2c"
}
########## type .exit to quit the shell ##########
.exit
This is a Callable Function,
and can only be called from the app or other authenticated source, like the
shell. It cannot be run from your browser or a REST client like getEmail
and sendEmail
can.
It will create a user in Firestore, if one doesn't already exist, with an ID and Key that will be needed to make requests to the other endpoints. It will fail if a user already exists.
To see existing user(s), along with their ID and Key, browse to the Firestore Console and click on the Firestore Database tab. You can also go there to create users manually.
Use the firebase deploy
command to create publicly accessible endpoints.
$ firebase deploy
=== Deploying to 'my-project'...
i deploying functions
i functions: ensuring required API cloudfunctions.googleapis.com is enabled...
i functions: ensuring required API cloudbuild.googleapis.com is enabled...
✔ functions: required API cloudfunctions.googleapis.com is enabled
✔ functions: required API cloudbuild.googleapis.com is enabled
i functions: preparing functions directory for uploading...
i functions: packaged functions (120.65 KB) for uploading
✔ functions: functions folder uploaded successfully
i functions: creating Node.js 14 function getEmail(us-central1)...
i functions: creating Node.js 14 function sendEmail(us-central1)...
i functions: creating Node.js 14 function install(us-central1)...
✔ functions[sendEmail(us-central1)]: Successful create operation.
✔ functions[getEmail(us-central1)]: Successful create operation.
✔ functions[install(us-central1)]: Successful create operation.
i functions: cleaning up build files...
✔ Deploy complete!
Project Console: https://console.firebase.google.com/project/my-project/overview
Send requests to the getEmail
or sendEmail
https endpoints. You can
find the URLs for the deployed endpoints by browsing to the "Functions" tab of
the Project Console in Firebase. The endpoints will respond the same to GET or
POST requests.
The request header should have content-type
set to application/json
,
and the request body should be a JSON object with a property named emailData
containing the data that the system will use to authenticate the request, build
the email from a template file, and then return or email it.
Requests to the getEmail
endpoint must include an emailData
object,
unless useDemoFiles
is set to true
in config.js
(see
the Demo/Tutorial section below). In addition to the
properties for replacing template tags, emailData
must have the following
properties:
- appUserId
- appUserKey
- templateFile (the name of a file in the "templates" directory)
The sendEmail
endpoint has the same requirements as getEmail
, but
its emailData
object must also include the properties related to sending
the email:
- appUserId
- appUserKey
- templateFile
- emailToAddress
- emailFromAddress
- emailFromName
- emailSubject
- sendGridApiKey (if not set in config.js)
The system assumes that the template contains an HTML version of the email. It
will create a plain-text version, and send a multi-part email with both. It will
respond with true
if the email was sent successfully, or false
otherwise.
Below is an example of a request that you can send in VSCode using the
REST Client extension.
It contains the same object that is in demo_data.js
, and uses
templates/demo_template.html
as the template file. Replace the values in
appUserId
and appUserKey
with the corresponding values for the user
created when installing the project.
POST http://localhost:5000/my-project/us-central1/getEmail HTTP/1.1
content-type: application/json
{
"emailData": {
"appUserId": "",
"appUserKey": "",
"templateFile": "demo_template.html",
"sendGridApiKey" : "",
"emailToAddress" : "foo@example.com",
"emailFromAddress": "bar@example.com",
"emailFromName": "Email template system",
"emailSubject": "Email template system demo",
"demo_check": "See 'Checking if demo_data.js is as expected' in templates/demo_template.html for information about this property.",
"second_list_item": "The third list item is {{ third_list_item }}, and will be blank because the 'third_list_item' property has been intentionally left out of demo_data.js",
"array_of_strings": [
"First list item",
"Second list item",
"Third list item"
],
"array_of_objects": [
{ "id": 1, "name": "Object 1 name" },
{ "id": 2, "name": "Object 2 name" },
{ "id": 3, "name": "Object 3 name" },
{ "id": 4 },
{ "id": 5, "name": "Object 5 name"}
]
}
}
There is a demo containing a short introduction / tutorial on how to use mustache.js to build templates. It can also be used to quickly verify that everything is configured correctly using your web browser.
To enable the demo, set useDemoFiles
in config.js
to true
.
Then copy default_demo_data.js
to demo_data.js
, and
templates/default_demo_template.html
to templates/demo_template.html
.
Open the demo_data.js
file, and set appUserId
and appUserKey
to the corresponding values for the user created when installing the project.
Start the Firebase Functions Emulator and browse to the URL for the
getEmail
endpoint, which will be in the form
http://localhost:PORT/PROJECT_ID/GOOGLE_CLOUD_REGION/getEmail
$ firebase serve
=== Serving from '/path/to/my_project'...
✔ functions: Using node@14 from host.
i functions: Watching "/path/to/my_project/functions" for Cloud Functions...
⚠ functions: The Cloud Firestore emulator is not running, so calls to Firestore will affect production.
✔ functions[us-central1-install]: http function initialized (http://localhost:5000/my-project/us-central1/install).
✔ functions[us-central1-getEmail]: http function initialized (http://localhost:5000/my-project/us-central1/getEmail).
✔ functions[us-central1-sendEmail]: http function initialized (http://localhost:5000/my-project/us-central1/sendEmail).
To test the sendEmail
endpoint, edit demo_data.js
and set values for
emailToAddress
, emailFromAddress
, and SendGridAPIKey
(unless
it is already set in config.js
). Browse to the URL for the sendEmail
endpoint, which is given when you run firebase serve
(see above), and the
system should email the demo to the email you specified in the
demo_data.js
file.
You can use the demo files to develop templates on your local system. Edit
demo_data.js
and change the values in emailData
with values for the
template you are working on. Set useDemoFiles
in config.js
to
true
, start the Firebase Functions Emulator and browse to the URL of an
endpoint. You can then make changes to your template or emailData
, and
refresh your browser to see the results.