Why not Sendgrid?
How easy is Using Sendgrid?
Sendgrid is easy to use, we just need to plug our email information into sendgrid's webpage, then it will generate an API key for us.
Email can then be sent as simply as writing
import sgMail from '@sendgrid/mail'; import { EmailSender } from "../dto/dto"; sgMail.setApiKey(process.env.SENDGRID_API_KEY || "") const sendMessage: EmailSender = async (props: { to: string, subject: string, text: string, html: string }) => { const { html, subject, text, to } = props; const msg = { to, from: "abc@email.com", subject, text, html, } try { await sgMail.send(msg); } catch (err) { throw new Error(JSON.stringify(err)); } }
and I am sure this email-sending feature can be set up within less than 15 minutes with zero knowledge about this service.
Problem of Sendgrid
The problem lies in its pricing (click me). A feature that is supposed to be free (have you ever paid google for sending email?) has a 100 emails/day cap, and uncapping it (still not unlimited) requires at least 19.95 usd (approximately 155.79 hkd) per month.
Problem of Gmail API
The following
- Complexity of oAuth2 credential authentication and
- the ease of use of the Google Cloud Console are the only barrier, if we can get around this then there is no reason not to use gmail api (which is free).
Prerequisite: OAuth2 Credential in Json
First you need to create an OAuth2 Credential about your account in json format. Detailed precedures have been included in
under the Create Credentials session.
A gmailService File
Environment Variables
Let's create a gmailService.ts
in our service directory, then:
1import fs from "fs"; 2import path from "path"; 3import process from "process"; 4import { authenticate } from "@google-cloud/local-auth"; 5import google from "googleapis" 6import { EmailSender, GMailUserJson as GoogleUserJson } from "../dto/dto"; 7import nodemailer from "nodemailer" 8 9const SCOPES = ['https://mail.google.com/']; 10 11const { 12 GOOGLE_API_OAUTH2_CREDENTIAL_JSON = "", 13 GOOGLE_API_REQUIRE_LOGIN_FOR_NEW_TOKEN = "", 14 GOOGLE_API_CREDENTIAL_JSON = "", 15} = process.env; 16const OAUTH2_CREDENTIAL_PATH = path.join(process.cwd(), GOOGLE_API_OAUTH2_CREDENTIAL_JSON); 17const REQUIRE_LOGIN_FOR_NEW_TOKEN = GOOGLE_API_REQUIRE_LOGIN_FOR_NEW_TOKEN === "true"; 18const CREDENTIALS_PATH = path.join(process.cwd(), GOOGLE_API_CREDENTIAL_JSON);
Let's explain the usage of the 3 environment variables.
GOOGLE_API_OAUTH2_CREDENTIAL_JSON
Pointing to the oauth2 credentials relative to the root project level. This file must exist and should be obtained from the prerequisite above.
The oauth2 credentials should look like:{ "installed": { "client_id": "em0kcr5.apps.googleusercontent.com", "project_id": "gmailapi-123456", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_secret": "secret", "redirect_uris": [ "http://localhost" ] } }
REQUIRE_LOGIN_FOR_NEW_TOKEN=true (initially)
-
At the first time we run the application we will be asked to authenticate ourself by using
authorize
function below (line-68). -
After authentication succeeds, our login information will be saved in a json file saved at
GOOGLE_API_CREDENTIAL_JSON
. -
We can set
REQUIRE_LOGIN_FOR_NEW_TOKEN=false
after the first authentication succeeds given that we have made sure the file pointed byGOOGLE_API_CREDENTIAL_JSON
exists.
-
GOOGLE_API_CREDENTIAL_JSON
This is the path of the login information relative to the root project level. Initially we don't have this file yet.
The login credentials should look like:{ "type": "authorized_user", "client_id": "em0kcr5.apps.googleusercontent.com", "client_secret": "secret", "refresh_token": "1//some-string" }
Cache Useful Variables
Next let's create two variables that cache the variables we created when launching the application:
19let authClient: google.Auth.OAuth2Client | null = null; 20let loginJson: GoogleUserJson | null = null;
Function to Send Email
21const getCredentialJson = () => { 22 if (loginJson) { 23 return loginJson 24 } else { 25 const content = fs.readFileSync(CREDENTIALS_PATH, { encoding: "utf-8" }); 26 const credentials = JSON.parse(content) as GoogleUserJson; 27 loginJson = credentials; 28 return credentials; 29 } 30} 31 32const getAuthClient = () => { 33 if (authClient) { 34 return authClient; 35 } else { 36 try { 37 const content = fs.readFileSync(CREDENTIALS_PATH, { encoding: "utf-8" }); 38 const credentials = JSON.parse(content); 39 authClient = new google.Auth.OAuth2Client( 40 credentials.client_id, 41 credentials.client_secret, 42 'http://localhost' 43 ); 44 return authClient; 45 } catch (err) { 46 return null; 47 } 48 } 49} 50 51const saveCredentials = (client: google.Auth.OAuth2Client) => { 52 const content = fs.readFileSync(OAUTH2_CREDENTIAL_PATH, { encoding: "utf-8" }); 53 const keys = JSON.parse(content); 54 const key = keys.installed || keys.web; 55 const payload = JSON.stringify({ 56 type: 'authorized_user', 57 client_id: key.client_id, 58 client_secret: key.client_secret, 59 refresh_token: client.credentials.refresh_token, 60 }); 61 fs.writeFileSync(CREDENTIALS_PATH, payload); 62} 63 64/** 65 * Load or request or authorization to call APIs. 66 * 67 */ 68async function authorize() { 69 if (REQUIRE_LOGIN_FOR_NEW_TOKEN) { 70 let client = await getAuthClient(); 71 if (client) { 72 return client; 73 } 74 client = await authenticate({ 75 scopes: SCOPES, 76 keyfilePath: OAUTH2_CREDENTIAL_PATH, 77 }); 78 if (client.credentials) { 79 await saveCredentials(client); 80 } 81 } 82} 83 84const getTransporter = async () => { 85 const { client_id, client_secret, refresh_token, type } = getCredentialJson(); 86 87 return nodemailer.createTransport({ 88 service: "gmail", 89 auth: { 90 type: "OAuth2", 91 user: process.env?.GOOGLE_API_EMAIL_SEND_ACCOUNT || "", 92 clientId: client_id, 93 clientSecret: client_secret, 94 refreshToken: refresh_token 95 } 96 }); 97} 98 99const sendMessage: EmailSender = async ({ 100 html, subject, text, to 101}) => { 102 const t = await getTransporter(); 103 t.sendMail({ 104 html, subject, text, to 105 }) 106} 107 108export default { 109 authorize, 110 sendMessage 111}
Entrypoint for Google Authentication
Finally:
- Let's run
await gmailService.authorize()
beforeapp.listen()
. - Set
GOOGLE_API_REQUIRE_LOGIN_FOR_NEW_TOKEN=true
for the first time. - Turn it off
GOOGLE_API_REQUIRE_LOGIN_FOR_NEW_TOKEN=false
from the first time onwards.