From 2f860eccd3c749a6e16bed858f0cde66ad1c814e Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Wed, 12 Jul 2023 11:38:08 +0100 Subject: [PATCH 01/14] feat: email contact form initial --- email-contact-form/.gitignore | 130 +++++++++++++++++++++++++ email-contact-form/.pretterrc.json | 6 ++ email-contact-form/README.md | 50 ++++++++++ email-contact-form/env.d.ts | 14 +++ email-contact-form/package-lock.json | 42 ++++++++ email-contact-form/package.json | 17 ++++ email-contact-form/src/cors.js | 21 ++++ email-contact-form/src/environment.js | 22 +++++ email-contact-form/src/mail.js | 24 +++++ email-contact-form/src/main.js | 124 +++++++++++++++++++++++ email-contact-form/static/index.html | 45 +++++++++ email-contact-form/static/success.html | 31 ++++++ 12 files changed, 526 insertions(+) create mode 100644 email-contact-form/.gitignore create mode 100644 email-contact-form/.pretterrc.json create mode 100644 email-contact-form/README.md create mode 100644 email-contact-form/env.d.ts create mode 100644 email-contact-form/package-lock.json create mode 100644 email-contact-form/package.json create mode 100644 email-contact-form/src/cors.js create mode 100644 email-contact-form/src/environment.js create mode 100644 email-contact-form/src/mail.js create mode 100644 email-contact-form/src/main.js create mode 100644 email-contact-form/static/index.html create mode 100644 email-contact-form/static/success.html diff --git a/email-contact-form/.gitignore b/email-contact-form/.gitignore new file mode 100644 index 00000000..6a7d6d8e --- /dev/null +++ b/email-contact-form/.gitignore @@ -0,0 +1,130 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* \ No newline at end of file diff --git a/email-contact-form/.pretterrc.json b/email-contact-form/.pretterrc.json new file mode 100644 index 00000000..fa51da29 --- /dev/null +++ b/email-contact-form/.pretterrc.json @@ -0,0 +1,6 @@ +{ + "trailingComma": "es5", + "tabWidth": 2, + "semi": false, + "singleQuote": true +} diff --git a/email-contact-form/README.md b/email-contact-form/README.md new file mode 100644 index 00000000..57ca4fa6 --- /dev/null +++ b/email-contact-form/README.md @@ -0,0 +1,50 @@ +# Email Contact Form Function + +## Overview + +This function facilitates email submission from HTML forms using Appwrite. It validates form data, sends an email through an SMTP server, and handles redirection of the user based on the success or failure of the submission. + +## Usage + +### HTML Form + +To use this function, set the `action` attribute of your HTML form to your function URL, and include a hidden input with the name `_next` and the path of the redirect to on successful form submission (e.g. `/success`). + +```html +
+ + + + +
+``` + +## Environment Variables + +This function depends on the following environment variables: + +- **SMTP_HOST** - SMTP server host +- **SMTP_PORT** - SMTP server port +- **SMTP_USERNAME** - SMTP server username +- **SMTP_PASSWORD** - SMTP server password +- **SUBMIT_EMAIL** - The email address to send form submissions +- **ALLOWED_ORIGINS** - An optional comma-separated list of allowed origins for CORS (defaults to `*`) + +## Request + +### Form Data + +- **_next_** - The URL to redirect to on successful form submission +- **email** - The sender's email address + +- _Additional form data will be included in the email body_ + +## Response + +### Success Redirect + +On successful form submission, the function will redirect users to the URL provided in the `_next` form data. + +### Error Redirect + +In the case of errors such as invalid request methods, missing form data, or SMTP configuration issues, the function will redirect users back to the form URL with an appended error code for more precise error handling. Error codes include `invalid-request`, `missing-form-fields`, and generic `server-error`. diff --git a/email-contact-form/env.d.ts b/email-contact-form/env.d.ts new file mode 100644 index 00000000..6e31c21d --- /dev/null +++ b/email-contact-form/env.d.ts @@ -0,0 +1,14 @@ +declare global { + namespace NodeJS { + interface ProcessEnv { + STMP_HOST?: string; + STMP_PORT?: string; + STMP_USERNAME?: string; + STMP_PASSWORD?: string; + SUBMIT_EMAIL?: string; + ALLOWED_ORIGINS?: string; + } + } +} + +export {}; diff --git a/email-contact-form/package-lock.json b/email-contact-form/package-lock.json new file mode 100644 index 00000000..1fbc6811 --- /dev/null +++ b/email-contact-form/package-lock.json @@ -0,0 +1,42 @@ +{ + "name": "email-contact-form", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "email-contact-form", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "nodemailer": "^6.9.3" + }, + "devDependencies": { + "prettier": "^3.0.0" + } + }, + "node_modules/nodemailer": { + "version": "6.9.3", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.3.tgz", + "integrity": "sha512-fy9v3NgTzBngrMFkDsKEj0r02U7jm6XfC3b52eoNV+GCrGj+s8pt5OqhiJdWKuw51zCTdiNR/IUD1z33LIIGpg==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/prettier": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz", + "integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + } + } +} diff --git a/email-contact-form/package.json b/email-contact-form/package.json new file mode 100644 index 00000000..85524702 --- /dev/null +++ b/email-contact-form/package.json @@ -0,0 +1,17 @@ +{ + "name": "email-contact-form", + "version": "1.0.0", + "description": "", + "main": "src/main.js", + "scripts": { + "format": "prettier --write src/**/*.js" + }, + "author": "", + "license": "MIT", + "dependencies": { + "nodemailer": "^6.9.3" + }, + "devDependencies": { + "prettier": "^3.0.0" + } +} diff --git a/email-contact-form/src/cors.js b/email-contact-form/src/cors.js new file mode 100644 index 00000000..cb7a0a0b --- /dev/null +++ b/email-contact-form/src/cors.js @@ -0,0 +1,21 @@ +const getEnvironment = require("./environment"); + +/** + * @param {string} origin + */ +module.exports = function CorsService(origin) { + const { ALLOWED_ORIGINS } = getEnvironment(); + + return { + isOriginPermitted: function () { + if (!ALLOWED_ORIGINS || ALLOWED_ORIGINS === "*") return true; + const allowedOriginsArray = ALLOWED_ORIGINS.split(","); + return allowedOriginsArray.includes(origin); + }, + getHeaders: function () { + return { + "Access-Control-Allow-Origin": ALLOWED_ORIGINS === "*" ? "*" : origin, + }; + }, + }; +}; diff --git a/email-contact-form/src/environment.js b/email-contact-form/src/environment.js new file mode 100644 index 00000000..397b9052 --- /dev/null +++ b/email-contact-form/src/environment.js @@ -0,0 +1,22 @@ +module.exports = function getEnvironment() { + return { + SUBMIT_EMAIL: getRequiredEnv("SUBMIT_EMAIL"), + SMTP_HOST: getRequiredEnv("SMTP_HOST"), + SMTP_PORT: process.env.SMTP_PORT || 587, + SMTP_USERNAME: getRequiredEnv("SMTP_USERNAME"), + SMTP_PASSWORD: getRequiredEnv("SMTP_PASSWORD"), + ALLOWED_ORIGINS: process.env.ALLOWED_ORIGINS || "*", + }; +}; + +/** + * @param {string} key + * @return {string} + */ +function getRequiredEnv(key) { + const value = process.env[key]; + if (value === undefined) { + throw new Error(`Environment variable ${key} is not set`); + } + return value; +} diff --git a/email-contact-form/src/mail.js b/email-contact-form/src/mail.js new file mode 100644 index 00000000..6b3b2999 --- /dev/null +++ b/email-contact-form/src/mail.js @@ -0,0 +1,24 @@ +const getEnvironment = require("./environment"); +const nodemailer = require("nodemailer"); + +module.exports = function MailService() { + const { SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD } = + getEnvironment(); + + const transport = nodemailer.createTransport({ + // @ts-ignore + // Not sure what's going on here. + host: SMTP_HOST, + port: SMTP_PORT, + auth: { user: SMTP_USERNAME, pass: SMTP_PASSWORD }, + }); + + return { + /** + * @param {import('nodemailer').SendMailOptions} mailOptions + */ + send: async function (mailOptions) { + await transport.sendMail(mailOptions); + }, + }; +}; diff --git a/email-contact-form/src/main.js b/email-contact-form/src/main.js new file mode 100644 index 00000000..5bbee69f --- /dev/null +++ b/email-contact-form/src/main.js @@ -0,0 +1,124 @@ +const querystring = require("node:querystring"); +const getEnvironment = require("./environment"); +const CorsService = require("./cors"); +const MailService = require("./mail"); +const fs = require("fs"); +const path = require("path"); + +const ErrorCode = { + INVALID_REQUEST: "invalid-request", + MISSING_FORM_FIELDS: "missing-form-fields", + SERVER_ERROR: "server-error", +}; + +const ROUTES = { + "/": "index.html", + "/index.html": "index.html", + "/success.html": "success.html", +}; + +const staticFolder = path.join(__dirname, "../static"); + +module.exports = async ({ req, res, log, error }) => { + const { SUBMIT_EMAIL, ALLOWED_ORIGINS } = getEnvironment(); + + if (ALLOWED_ORIGINS === "*") { + log( + "WARNING: Allowing requests from any origin - this is a security risk!" + ); + } + + if (req.method === "GET") { + const route = ROUTES[req.path]; + const html = fs.readFileSync(path.join(staticFolder, route)); + return res.send(html.toString(), 200, { + "Content-Type": "text/html; charset=utf-8", + }); + } + + const referer = req.headers["referer"]; + const origin = req.headers["origin"]; + if (!referer || !origin) { + log("Missing referer or origin headers."); + return res.json({ error: "Missing referer or origin headers." }, 400); + } + + if (req.headers["content-type"] !== "application/x-www-form-urlencoded") { + log("Invalid request."); + return res.redirect(urlWithCodeParam(referer, ErrorCode.INVALID_REQUEST)); + } + + const cors = CorsService(origin); + const mail = MailService(); + + if (!cors.isOriginPermitted()) { + error("Origin not permitted."); + return res.redirect(urlWithCodeParam(referer, ErrorCode.INVALID_REQUEST)); + } + + const form = querystring.parse(req.body); + + if ( + !( + form.email && + form._next && + typeof form.email === "string" && + typeof form._next === "string" + ) + ) { + error("Missing form data."); + return res.redirect( + urlWithCodeParam(referer, ErrorCode.MISSING_FORM_FIELDS), + 301, + cors.getHeaders() + ); + } + log("Form data is valid."); + + try { + mail.send({ + to: SUBMIT_EMAIL, + from: form.email, + subject: `New form submission: ${origin}`, + text: templateFormMessage(form), + }); + } catch (err) { + error(err.message); + return res.redirect( + urlWithCodeParam(referer, ErrorCode.SERVER_ERROR), + 301, + cors.getHeaders() + ); + } + + log("Email sent successfully."); + + return res.redirect( + new URL(form._next, origin).toString(), + 301, + cors.getHeaders() + ); +}; + +/** + * Build a message from the form data. + * @param {import("node:querystring").ParsedUrlQuery} form + * @returns {string} + */ +function templateFormMessage(form) { + return `You've received a new message.\n +${Object.entries(form) + .filter(([key]) => key !== "_next") + .map(([key, value]) => `${key}: ${value}`) + .join("\n")}`; +} + +/** + * @param {string} baseUrl + * @param {string} codeParam + */ +function urlWithCodeParam(baseUrl, codeParam) { + const url = new URL(baseUrl); + url.searchParams.set("code", codeParam); + return url.toString(); +} diff --git a/email-contact-form/static/index.html b/email-contact-form/static/index.html new file mode 100644 index 00000000..29670f76 --- /dev/null +++ b/email-contact-form/static/index.html @@ -0,0 +1,45 @@ + + + + + + + Email Contact Form + + + + +
+
+
+
+

Contact

+ +
+

+ Fill the form below to send us a message. +

+
+ + + + +
+
+
+
+ + diff --git a/email-contact-form/static/success.html b/email-contact-form/static/success.html new file mode 100644 index 00000000..94ba2a8d --- /dev/null +++ b/email-contact-form/static/success.html @@ -0,0 +1,31 @@ + + + + + + + Email Contact Form + + + + +
+
+
+
+

Success

+ +
+

+ Your message has been sent! +

+
+
+
+ + From d90ae027326516727a8fd29a3158f25c3bd12e55 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Wed, 12 Jul 2023 13:04:15 +0100 Subject: [PATCH 02/14] feat: migrate to esm --- .../{.pretterrc.json => .prettierrc.json} | 0 email-contact-form/package.json | 1 + email-contact-form/src/cors.js | 28 +++-- email-contact-form/src/environment.js | 28 ++--- email-contact-form/src/mail.js | 16 +-- email-contact-form/src/main.js | 110 +++++++++--------- 6 files changed, 96 insertions(+), 87 deletions(-) rename email-contact-form/{.pretterrc.json => .prettierrc.json} (100%) diff --git a/email-contact-form/.pretterrc.json b/email-contact-form/.prettierrc.json similarity index 100% rename from email-contact-form/.pretterrc.json rename to email-contact-form/.prettierrc.json diff --git a/email-contact-form/package.json b/email-contact-form/package.json index 85524702..44332858 100644 --- a/email-contact-form/package.json +++ b/email-contact-form/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "description": "", "main": "src/main.js", + "type": "module", "scripts": { "format": "prettier --write src/**/*.js" }, diff --git a/email-contact-form/src/cors.js b/email-contact-form/src/cors.js index cb7a0a0b..f0e6a5e4 100644 --- a/email-contact-form/src/cors.js +++ b/email-contact-form/src/cors.js @@ -1,21 +1,27 @@ -const getEnvironment = require("./environment"); +import getEnvironment from './environment' /** - * @param {string} origin + * @param {string} origin Origin header of the request */ -module.exports = function CorsService(origin) { - const { ALLOWED_ORIGINS } = getEnvironment(); +export default function CorsService(origin) { + const { ALLOWED_ORIGINS } = getEnvironment() return { + /** + * @returns {boolean} Whether the origin is allowed based on the ALLOWED_ORIGINS environment variable + */ isOriginPermitted: function () { - if (!ALLOWED_ORIGINS || ALLOWED_ORIGINS === "*") return true; - const allowedOriginsArray = ALLOWED_ORIGINS.split(","); - return allowedOriginsArray.includes(origin); + if (!ALLOWED_ORIGINS || ALLOWED_ORIGINS === '*') return true + const allowedOriginsArray = ALLOWED_ORIGINS.split(',') + return allowedOriginsArray.includes(origin) }, + /** + * @returns {Object} Access-Control-Allow-Origin header to be returned in the response + */ getHeaders: function () { return { - "Access-Control-Allow-Origin": ALLOWED_ORIGINS === "*" ? "*" : origin, - }; + 'Access-Control-Allow-Origin': ALLOWED_ORIGINS === '*' ? '*' : origin, + } }, - }; -}; + } +} diff --git a/email-contact-form/src/environment.js b/email-contact-form/src/environment.js index 397b9052..3ce204fe 100644 --- a/email-contact-form/src/environment.js +++ b/email-contact-form/src/environment.js @@ -1,22 +1,22 @@ -module.exports = function getEnvironment() { - return { - SUBMIT_EMAIL: getRequiredEnv("SUBMIT_EMAIL"), - SMTP_HOST: getRequiredEnv("SMTP_HOST"), - SMTP_PORT: process.env.SMTP_PORT || 587, - SMTP_USERNAME: getRequiredEnv("SMTP_USERNAME"), - SMTP_PASSWORD: getRequiredEnv("SMTP_PASSWORD"), - ALLOWED_ORIGINS: process.env.ALLOWED_ORIGINS || "*", - }; -}; - /** * @param {string} key * @return {string} */ function getRequiredEnv(key) { - const value = process.env[key]; + const value = process.env[key] if (value === undefined) { - throw new Error(`Environment variable ${key} is not set`); + throw new Error(`Environment variable ${key} is not set`) + } + return value +} + +export default function getEnvironment() { + return { + SUBMIT_EMAIL: getRequiredEnv('SUBMIT_EMAIL'), + SMTP_HOST: getRequiredEnv('SMTP_HOST'), + SMTP_PORT: process.env.SMTP_PORT || 587, + SMTP_USERNAME: getRequiredEnv('SMTP_USERNAME'), + SMTP_PASSWORD: getRequiredEnv('SMTP_PASSWORD'), + ALLOWED_ORIGINS: process.env.ALLOWED_ORIGINS || '*', } - return value; } diff --git a/email-contact-form/src/mail.js b/email-contact-form/src/mail.js index 6b3b2999..cb6a859b 100644 --- a/email-contact-form/src/mail.js +++ b/email-contact-form/src/mail.js @@ -1,9 +1,9 @@ -const getEnvironment = require("./environment"); -const nodemailer = require("nodemailer"); +import getEnvironment from './environment' +import nodemailer from 'nodemailer' -module.exports = function MailService() { +export default function MailService() { const { SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD } = - getEnvironment(); + getEnvironment() const transport = nodemailer.createTransport({ // @ts-ignore @@ -11,14 +11,14 @@ module.exports = function MailService() { host: SMTP_HOST, port: SMTP_PORT, auth: { user: SMTP_USERNAME, pass: SMTP_PASSWORD }, - }); + }) return { /** * @param {import('nodemailer').SendMailOptions} mailOptions */ send: async function (mailOptions) { - await transport.sendMail(mailOptions); + await transport.sendMail(mailOptions) }, - }; -}; + } +} diff --git a/email-contact-form/src/main.js b/email-contact-form/src/main.js index 5bbee69f..79af167c 100644 --- a/email-contact-form/src/main.js +++ b/email-contact-form/src/main.js @@ -1,79 +1,81 @@ -const querystring = require("node:querystring"); -const getEnvironment = require("./environment"); -const CorsService = require("./cors"); -const MailService = require("./mail"); -const fs = require("fs"); -const path = require("path"); +import querystring from 'node:querystring' +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import path from 'node:path' +import getEnvironment from './environment' +import CorsService from './cors' +import MailService from './mail' const ErrorCode = { - INVALID_REQUEST: "invalid-request", - MISSING_FORM_FIELDS: "missing-form-fields", - SERVER_ERROR: "server-error", -}; + INVALID_REQUEST: 'invalid-request', + MISSING_FORM_FIELDS: 'missing-form-fields', + SERVER_ERROR: 'server-error', +} const ROUTES = { - "/": "index.html", - "/index.html": "index.html", - "/success.html": "success.html", -}; + '/': 'index.html', + '/index.html': 'index.html', + '/success.html': 'success.html', +} + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) -const staticFolder = path.join(__dirname, "../static"); +const staticFolder = path.join(__dirname, '../static') -module.exports = async ({ req, res, log, error }) => { - const { SUBMIT_EMAIL, ALLOWED_ORIGINS } = getEnvironment(); +export default async ({ req, res, log, error }) => { + const { SUBMIT_EMAIL, ALLOWED_ORIGINS } = getEnvironment() - if (ALLOWED_ORIGINS === "*") { - log( - "WARNING: Allowing requests from any origin - this is a security risk!" - ); + if (ALLOWED_ORIGINS === '*') { + log('WARNING: Allowing requests from any origin - this is a security risk!') } - if (req.method === "GET") { - const route = ROUTES[req.path]; - const html = fs.readFileSync(path.join(staticFolder, route)); + if (req.method === 'GET') { + const route = ROUTES[req.path] + const html = readFileSync(path.join(staticFolder, route)) return res.send(html.toString(), 200, { - "Content-Type": "text/html; charset=utf-8", - }); + 'Content-Type': 'text/html; charset=utf-8', + }) } - const referer = req.headers["referer"]; - const origin = req.headers["origin"]; + const referer = req.headers['referer'] + const origin = req.headers['origin'] if (!referer || !origin) { - log("Missing referer or origin headers."); - return res.json({ error: "Missing referer or origin headers." }, 400); + log('Missing referer or origin headers.') + return res.json({ error: 'Missing referer or origin headers.' }, 400) } - if (req.headers["content-type"] !== "application/x-www-form-urlencoded") { - log("Invalid request."); - return res.redirect(urlWithCodeParam(referer, ErrorCode.INVALID_REQUEST)); + if (req.headers['content-type'] !== 'application/x-www-form-urlencoded') { + log('Invalid request.') + return res.redirect(urlWithCodeParam(referer, ErrorCode.INVALID_REQUEST)) } - const cors = CorsService(origin); - const mail = MailService(); + const cors = CorsService(origin) + const mail = MailService() if (!cors.isOriginPermitted()) { - error("Origin not permitted."); - return res.redirect(urlWithCodeParam(referer, ErrorCode.INVALID_REQUEST)); + error('Origin not permitted.') + return res.redirect(urlWithCodeParam(referer, ErrorCode.INVALID_REQUEST)) } - const form = querystring.parse(req.body); + const form = querystring.parse(req.body) if ( !( form.email && form._next && - typeof form.email === "string" && - typeof form._next === "string" + typeof form.email === 'string' && + typeof form._next === 'string' ) ) { - error("Missing form data."); + error('Missing form data.') return res.redirect( urlWithCodeParam(referer, ErrorCode.MISSING_FORM_FIELDS), 301, cors.getHeaders() - ); + ) } - log("Form data is valid."); + log('Form data is valid.') try { mail.send({ @@ -81,24 +83,24 @@ module.exports = async ({ req, res, log, error }) => { from: form.email, subject: `New form submission: ${origin}`, text: templateFormMessage(form), - }); + }) } catch (err) { - error(err.message); + error(err.message) return res.redirect( urlWithCodeParam(referer, ErrorCode.SERVER_ERROR), 301, cors.getHeaders() - ); + ) } - log("Email sent successfully."); + log('Email sent successfully.') return res.redirect( new URL(form._next, origin).toString(), 301, cors.getHeaders() - ); -}; + ) +} /** * Build a message from the form data. @@ -108,9 +110,9 @@ module.exports = async ({ req, res, log, error }) => { function templateFormMessage(form) { return `You've received a new message.\n ${Object.entries(form) - .filter(([key]) => key !== "_next") + .filter(([key]) => key !== '_next') .map(([key, value]) => `${key}: ${value}`) - .join("\n")}`; + .join('\n')}` } /** @@ -118,7 +120,7 @@ ${Object.entries(form) * @param {string} codeParam */ function urlWithCodeParam(baseUrl, codeParam) { - const url = new URL(baseUrl); - url.searchParams.set("code", codeParam); - return url.toString(); + const url = new URL(baseUrl) + url.searchParams.set('code', codeParam) + return url.toString() } From 9551dccf6fc56d07304932adb0d173d5fb46052a Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Wed, 12 Jul 2023 13:43:16 +0100 Subject: [PATCH 03/14] feat: default success page --- email-contact-form/src/main.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/email-contact-form/src/main.js b/email-contact-form/src/main.js index 79af167c..799a4823 100644 --- a/email-contact-form/src/main.js +++ b/email-contact-form/src/main.js @@ -60,14 +60,7 @@ export default async ({ req, res, log, error }) => { const form = querystring.parse(req.body) - if ( - !( - form.email && - form._next && - typeof form.email === 'string' && - typeof form._next === 'string' - ) - ) { + if (!(form.email && typeof form.email === 'string')) { error('Missing form data.') return res.redirect( urlWithCodeParam(referer, ErrorCode.MISSING_FORM_FIELDS), @@ -77,6 +70,9 @@ export default async ({ req, res, log, error }) => { } log('Form data is valid.') + const successUrl = + typeof form._next === 'string' && form._next ? form._next : '/success' + try { mail.send({ to: SUBMIT_EMAIL, @@ -96,7 +92,7 @@ export default async ({ req, res, log, error }) => { log('Email sent successfully.') return res.redirect( - new URL(form._next, origin).toString(), + new URL(successUrl, origin).toString(), 301, cors.getHeaders() ) From e3201c488d14b8a7e3bf91b509b4aa3b10cfa8cd Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Mon, 17 Jul 2023 10:45:10 +0100 Subject: [PATCH 04/14] fix: esm migration --- email-contact-form/src/cors.js | 6 ++---- email-contact-form/src/environment.js | 4 +++- email-contact-form/src/mail.js | 6 ++---- email-contact-form/src/main.js | 13 +++++++------ 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/email-contact-form/src/cors.js b/email-contact-form/src/cors.js index f0e6a5e4..11c4d6b0 100644 --- a/email-contact-form/src/cors.js +++ b/email-contact-form/src/cors.js @@ -1,10 +1,8 @@ -import getEnvironment from './environment' - /** * @param {string} origin Origin header of the request */ -export default function CorsService(origin) { - const { ALLOWED_ORIGINS } = getEnvironment() +export default function CorsService(origin, environment) { + const { ALLOWED_ORIGINS } = environment return { /** diff --git a/email-contact-form/src/environment.js b/email-contact-form/src/environment.js index 3ce204fe..026927b2 100644 --- a/email-contact-form/src/environment.js +++ b/email-contact-form/src/environment.js @@ -10,7 +10,7 @@ function getRequiredEnv(key) { return value } -export default function getEnvironment() { +function EnvironmentService() { return { SUBMIT_EMAIL: getRequiredEnv('SUBMIT_EMAIL'), SMTP_HOST: getRequiredEnv('SMTP_HOST'), @@ -20,3 +20,5 @@ export default function getEnvironment() { ALLOWED_ORIGINS: process.env.ALLOWED_ORIGINS || '*', } } + +export default EnvironmentService diff --git a/email-contact-form/src/mail.js b/email-contact-form/src/mail.js index cb6a859b..d98c1a70 100644 --- a/email-contact-form/src/mail.js +++ b/email-contact-form/src/mail.js @@ -1,9 +1,7 @@ -import getEnvironment from './environment' import nodemailer from 'nodemailer' -export default function MailService() { - const { SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD } = - getEnvironment() +export default function MailService(environment) { + const { SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD } = environment const transport = nodemailer.createTransport({ // @ts-ignore diff --git a/email-contact-form/src/main.js b/email-contact-form/src/main.js index 799a4823..49883a87 100644 --- a/email-contact-form/src/main.js +++ b/email-contact-form/src/main.js @@ -2,9 +2,9 @@ import querystring from 'node:querystring' import { readFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' import path from 'node:path' -import getEnvironment from './environment' -import CorsService from './cors' -import MailService from './mail' +import CorsService from './cors.js' +import MailService from './mail.js' +import EnvironmentService from './environment.js' const ErrorCode = { INVALID_REQUEST: 'invalid-request', @@ -24,7 +24,8 @@ const __dirname = path.dirname(__filename) const staticFolder = path.join(__dirname, '../static') export default async ({ req, res, log, error }) => { - const { SUBMIT_EMAIL, ALLOWED_ORIGINS } = getEnvironment() + const environment = EnvironmentService() + const { SUBMIT_EMAIL, ALLOWED_ORIGINS } = environment if (ALLOWED_ORIGINS === '*') { log('WARNING: Allowing requests from any origin - this is a security risk!') @@ -50,8 +51,8 @@ export default async ({ req, res, log, error }) => { return res.redirect(urlWithCodeParam(referer, ErrorCode.INVALID_REQUEST)) } - const cors = CorsService(origin) - const mail = MailService() + const cors = CorsService(origin, environment) + const mail = MailService(environment) if (!cors.isOriginPermitted()) { error('Origin not permitted.') From ec21ea280420307938999a748c4891178dd6b486 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Wed, 19 Jul 2023 10:42:01 +0100 Subject: [PATCH 05/14] chore: add semis, del pjson extras --- email-contact-form/.prettierrc.json | 2 +- email-contact-form/package.json | 2 - email-contact-form/src/cors.js | 12 ++-- email-contact-form/src/environment.js | 10 +-- email-contact-form/src/mail.js | 10 +-- email-contact-form/src/main.js | 88 ++++++++++++++------------- 6 files changed, 62 insertions(+), 62 deletions(-) diff --git a/email-contact-form/.prettierrc.json b/email-contact-form/.prettierrc.json index fa51da29..0a725205 100644 --- a/email-contact-form/.prettierrc.json +++ b/email-contact-form/.prettierrc.json @@ -1,6 +1,6 @@ { "trailingComma": "es5", "tabWidth": 2, - "semi": false, + "semi": true, "singleQuote": true } diff --git a/email-contact-form/package.json b/email-contact-form/package.json index 44332858..17dde79e 100644 --- a/email-contact-form/package.json +++ b/email-contact-form/package.json @@ -7,8 +7,6 @@ "scripts": { "format": "prettier --write src/**/*.js" }, - "author": "", - "license": "MIT", "dependencies": { "nodemailer": "^6.9.3" }, diff --git a/email-contact-form/src/cors.js b/email-contact-form/src/cors.js index 11c4d6b0..e8ed5cbe 100644 --- a/email-contact-form/src/cors.js +++ b/email-contact-form/src/cors.js @@ -2,16 +2,16 @@ * @param {string} origin Origin header of the request */ export default function CorsService(origin, environment) { - const { ALLOWED_ORIGINS } = environment + const { ALLOWED_ORIGINS } = environment; return { /** * @returns {boolean} Whether the origin is allowed based on the ALLOWED_ORIGINS environment variable */ isOriginPermitted: function () { - if (!ALLOWED_ORIGINS || ALLOWED_ORIGINS === '*') return true - const allowedOriginsArray = ALLOWED_ORIGINS.split(',') - return allowedOriginsArray.includes(origin) + if (!ALLOWED_ORIGINS || ALLOWED_ORIGINS === '*') return true; + const allowedOriginsArray = ALLOWED_ORIGINS.split(','); + return allowedOriginsArray.includes(origin); }, /** * @returns {Object} Access-Control-Allow-Origin header to be returned in the response @@ -19,7 +19,7 @@ export default function CorsService(origin, environment) { getHeaders: function () { return { 'Access-Control-Allow-Origin': ALLOWED_ORIGINS === '*' ? '*' : origin, - } + }; }, - } + }; } diff --git a/email-contact-form/src/environment.js b/email-contact-form/src/environment.js index 026927b2..3a0f8893 100644 --- a/email-contact-form/src/environment.js +++ b/email-contact-form/src/environment.js @@ -3,11 +3,11 @@ * @return {string} */ function getRequiredEnv(key) { - const value = process.env[key] + const value = process.env[key]; if (value === undefined) { - throw new Error(`Environment variable ${key} is not set`) + throw new Error(`Environment variable ${key} is not set`); } - return value + return value; } function EnvironmentService() { @@ -18,7 +18,7 @@ function EnvironmentService() { SMTP_USERNAME: getRequiredEnv('SMTP_USERNAME'), SMTP_PASSWORD: getRequiredEnv('SMTP_PASSWORD'), ALLOWED_ORIGINS: process.env.ALLOWED_ORIGINS || '*', - } + }; } -export default EnvironmentService +export default EnvironmentService; diff --git a/email-contact-form/src/mail.js b/email-contact-form/src/mail.js index d98c1a70..4d15942f 100644 --- a/email-contact-form/src/mail.js +++ b/email-contact-form/src/mail.js @@ -1,7 +1,7 @@ -import nodemailer from 'nodemailer' +import nodemailer from 'nodemailer'; export default function MailService(environment) { - const { SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD } = environment + const { SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD } = environment; const transport = nodemailer.createTransport({ // @ts-ignore @@ -9,14 +9,14 @@ export default function MailService(environment) { host: SMTP_HOST, port: SMTP_PORT, auth: { user: SMTP_USERNAME, pass: SMTP_PASSWORD }, - }) + }); return { /** * @param {import('nodemailer').SendMailOptions} mailOptions */ send: async function (mailOptions) { - await transport.sendMail(mailOptions) + await transport.sendMail(mailOptions); }, - } + }; } diff --git a/email-contact-form/src/main.js b/email-contact-form/src/main.js index 49883a87..c01899c2 100644 --- a/email-contact-form/src/main.js +++ b/email-contact-form/src/main.js @@ -1,78 +1,80 @@ -import querystring from 'node:querystring' -import { readFileSync } from 'node:fs' -import { fileURLToPath } from 'node:url' -import path from 'node:path' -import CorsService from './cors.js' -import MailService from './mail.js' -import EnvironmentService from './environment.js' +import querystring from 'node:querystring'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; +import CorsService from './cors.js'; +import MailService from './mail.js'; +import EnvironmentService from './environment.js'; const ErrorCode = { INVALID_REQUEST: 'invalid-request', MISSING_FORM_FIELDS: 'missing-form-fields', SERVER_ERROR: 'server-error', -} +}; const ROUTES = { '/': 'index.html', '/index.html': 'index.html', '/success.html': 'success.html', -} +}; -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); -const staticFolder = path.join(__dirname, '../static') +const staticFolder = path.join(__dirname, '../static'); export default async ({ req, res, log, error }) => { - const environment = EnvironmentService() - const { SUBMIT_EMAIL, ALLOWED_ORIGINS } = environment + const environment = EnvironmentService(); + const { SUBMIT_EMAIL, ALLOWED_ORIGINS } = environment; if (ALLOWED_ORIGINS === '*') { - log('WARNING: Allowing requests from any origin - this is a security risk!') + log( + 'WARNING: Allowing requests from any origin - this is a security risk!' + ); } if (req.method === 'GET') { - const route = ROUTES[req.path] - const html = readFileSync(path.join(staticFolder, route)) + const route = ROUTES[req.path]; + const html = readFileSync(path.join(staticFolder, route)); return res.send(html.toString(), 200, { 'Content-Type': 'text/html; charset=utf-8', - }) + }); } - const referer = req.headers['referer'] - const origin = req.headers['origin'] + const referer = req.headers['referer']; + const origin = req.headers['origin']; if (!referer || !origin) { - log('Missing referer or origin headers.') - return res.json({ error: 'Missing referer or origin headers.' }, 400) + log('Missing referer or origin headers.'); + return res.json({ error: 'Missing referer or origin headers.' }, 400); } if (req.headers['content-type'] !== 'application/x-www-form-urlencoded') { - log('Invalid request.') - return res.redirect(urlWithCodeParam(referer, ErrorCode.INVALID_REQUEST)) + log('Invalid request.'); + return res.redirect(urlWithCodeParam(referer, ErrorCode.INVALID_REQUEST)); } - const cors = CorsService(origin, environment) - const mail = MailService(environment) + const cors = CorsService(origin, environment); + const mail = MailService(environment); if (!cors.isOriginPermitted()) { - error('Origin not permitted.') - return res.redirect(urlWithCodeParam(referer, ErrorCode.INVALID_REQUEST)) + error('Origin not permitted.'); + return res.redirect(urlWithCodeParam(referer, ErrorCode.INVALID_REQUEST)); } - const form = querystring.parse(req.body) + const form = querystring.parse(req.body); if (!(form.email && typeof form.email === 'string')) { - error('Missing form data.') + error('Missing form data.'); return res.redirect( urlWithCodeParam(referer, ErrorCode.MISSING_FORM_FIELDS), 301, cors.getHeaders() - ) + ); } - log('Form data is valid.') + log('Form data is valid.'); const successUrl = - typeof form._next === 'string' && form._next ? form._next : '/success' + typeof form._next === 'string' && form._next ? form._next : '/success'; try { mail.send({ @@ -80,24 +82,24 @@ export default async ({ req, res, log, error }) => { from: form.email, subject: `New form submission: ${origin}`, text: templateFormMessage(form), - }) + }); } catch (err) { - error(err.message) + error(err.message); return res.redirect( urlWithCodeParam(referer, ErrorCode.SERVER_ERROR), 301, cors.getHeaders() - ) + ); } - log('Email sent successfully.') + log('Email sent successfully.'); return res.redirect( new URL(successUrl, origin).toString(), 301, cors.getHeaders() - ) -} + ); +}; /** * Build a message from the form data. @@ -109,7 +111,7 @@ function templateFormMessage(form) { ${Object.entries(form) .filter(([key]) => key !== '_next') .map(([key, value]) => `${key}: ${value}`) - .join('\n')}` + .join('\n')}`; } /** @@ -117,7 +119,7 @@ ${Object.entries(form) * @param {string} codeParam */ function urlWithCodeParam(baseUrl, codeParam) { - const url = new URL(baseUrl) - url.searchParams.set('code', codeParam) - return url.toString() + const url = new URL(baseUrl); + url.searchParams.set('code', codeParam); + return url.toString(); } From 0cea85cd4dbb44c0ca6bed1938df0d6e3ea82929 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Wed, 19 Jul 2023 10:51:02 +0100 Subject: [PATCH 06/14] chore: refactor --- email-contact-form/src/main.js | 38 +++++++++++++++------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/email-contact-form/src/main.js b/email-contact-form/src/main.js index c01899c2..f0438618 100644 --- a/email-contact-form/src/main.js +++ b/email-contact-form/src/main.js @@ -1,26 +1,13 @@ import querystring from 'node:querystring'; -import { readFileSync } from 'node:fs'; +import fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import path from 'node:path'; import CorsService from './cors.js'; import MailService from './mail.js'; import EnvironmentService from './environment.js'; -const ErrorCode = { - INVALID_REQUEST: 'invalid-request', - MISSING_FORM_FIELDS: 'missing-form-fields', - SERVER_ERROR: 'server-error', -}; - -const ROUTES = { - '/': 'index.html', - '/index.html': 'index.html', - '/success.html': 'success.html', -}; - const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); - const staticFolder = path.join(__dirname, '../static'); export default async ({ req, res, log, error }) => { @@ -33,9 +20,8 @@ export default async ({ req, res, log, error }) => { ); } - if (req.method === 'GET') { - const route = ROUTES[req.path]; - const html = readFileSync(path.join(staticFolder, route)); + if (req.method === 'GET' && req.path === '/') { + const html = fs.readFileSync(path.join(staticFolder, 'index.html')); return res.send(html.toString(), 200, { 'Content-Type': 'text/html; charset=utf-8', }); @@ -73,9 +59,6 @@ export default async ({ req, res, log, error }) => { } log('Form data is valid.'); - const successUrl = - typeof form._next === 'string' && form._next ? form._next : '/success'; - try { mail.send({ to: SUBMIT_EMAIL, @@ -94,8 +77,15 @@ export default async ({ req, res, log, error }) => { log('Email sent successfully.'); + if (typeof form._next !== 'string' || !form._next) { + const html = fs.readFileSync(path.join(staticFolder, 'success.html')); + return res.send(html.toString(), 200, { + 'Content-Type': 'text/html; charset=utf-8', + }); + } + return res.redirect( - new URL(successUrl, origin).toString(), + new URL(form._next, origin).toString(), 301, cors.getHeaders() ); @@ -114,6 +104,12 @@ ${Object.entries(form) .join('\n')}`; } +const ErrorCode = { + INVALID_REQUEST: 'invalid-request', + MISSING_FORM_FIELDS: 'missing-form-fields', + SERVER_ERROR: 'server-error', +}; + /** * @param {string} baseUrl * @param {string} codeParam From b272bd1edd03bafcaf03019f6cc27d07bc6910d9 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Wed, 19 Jul 2023 20:59:08 +0100 Subject: [PATCH 07/14] chore: use classes --- email-contact-form/src/cors.js | 53 +++++++++++++++------------ email-contact-form/src/environment.js | 16 ++++---- email-contact-form/src/mail.js | 39 +++++++++++--------- email-contact-form/src/main.js | 11 +++--- 4 files changed, 64 insertions(+), 55 deletions(-) diff --git a/email-contact-form/src/cors.js b/email-contact-form/src/cors.js index e8ed5cbe..20509ce0 100644 --- a/email-contact-form/src/cors.js +++ b/email-contact-form/src/cors.js @@ -1,25 +1,32 @@ -/** - * @param {string} origin Origin header of the request - */ -export default function CorsService(origin, environment) { - const { ALLOWED_ORIGINS } = environment; +class CorsService { + /** + * @param {*} req - Request object + * @param {import('./environment').default} env - Environment variables + */ + constructor(req, env) { + this.env = env; + this.origin = req.headers['origin']; + } - return { - /** - * @returns {boolean} Whether the origin is allowed based on the ALLOWED_ORIGINS environment variable - */ - isOriginPermitted: function () { - if (!ALLOWED_ORIGINS || ALLOWED_ORIGINS === '*') return true; - const allowedOriginsArray = ALLOWED_ORIGINS.split(','); - return allowedOriginsArray.includes(origin); - }, - /** - * @returns {Object} Access-Control-Allow-Origin header to be returned in the response - */ - getHeaders: function () { - return { - 'Access-Control-Allow-Origin': ALLOWED_ORIGINS === '*' ? '*' : origin, - }; - }, - }; + /** + * @returns {boolean} Whether the origin is allowed based on the ALLOWED_ORIGINS environment variable + */ + isOriginPermitted() { + if (!this.env.ALLOWED_ORIGINS || this.env.ALLOWED_ORIGINS === '*') + return true; + const allowedOriginsArray = this.env.ALLOWED_ORIGINS.split(','); + return allowedOriginsArray.includes(this.origin); + } + + /** + * @returns {Object} Access-Control-Allow-Origin header to be returned in the response + */ + getHeaders() { + return { + 'Access-Control-Allow-Origin': + this.env.ALLOWED_ORIGINS === '*' ? '*' : this.origin, + }; + } } + +export default CorsService; diff --git a/email-contact-form/src/environment.js b/email-contact-form/src/environment.js index 3a0f8893..5aa7fd1f 100644 --- a/email-contact-form/src/environment.js +++ b/email-contact-form/src/environment.js @@ -10,15 +10,13 @@ function getRequiredEnv(key) { return value; } -function EnvironmentService() { - return { - SUBMIT_EMAIL: getRequiredEnv('SUBMIT_EMAIL'), - SMTP_HOST: getRequiredEnv('SMTP_HOST'), - SMTP_PORT: process.env.SMTP_PORT || 587, - SMTP_USERNAME: getRequiredEnv('SMTP_USERNAME'), - SMTP_PASSWORD: getRequiredEnv('SMTP_PASSWORD'), - ALLOWED_ORIGINS: process.env.ALLOWED_ORIGINS || '*', - }; +class EnvironmentService { + SUBMIT_EMAIL = getRequiredEnv('SUBMIT_EMAIL'); + SMTP_HOST = getRequiredEnv('SMTP_HOST'); + SMTP_PORT = process.env.SMTP_PORT || 587; + SMTP_USERNAME = getRequiredEnv('SMTP_USERNAME'); + SMTP_PASSWORD = getRequiredEnv('SMTP_PASSWORD'); + ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS || '*'; } export default EnvironmentService; diff --git a/email-contact-form/src/mail.js b/email-contact-form/src/mail.js index 4d15942f..e18b07cc 100644 --- a/email-contact-form/src/mail.js +++ b/email-contact-form/src/mail.js @@ -1,22 +1,27 @@ import nodemailer from 'nodemailer'; -export default function MailService(environment) { - const { SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD } = environment; +class MailService { + /** + * @param {import('./environment').default} env + */ + constructor(env) { + this.env = env; - const transport = nodemailer.createTransport({ - // @ts-ignore - // Not sure what's going on here. - host: SMTP_HOST, - port: SMTP_PORT, - auth: { user: SMTP_USERNAME, pass: SMTP_PASSWORD }, - }); + this.transport = nodemailer.createTransport({ + // @ts-ignore + // Not sure what's going on here. + host: env.SMTP_HOST, + port: env.SMTP_PORT, + auth: { user: env.SMTP_USERNAME, pass: env.SMTP_PASSWORD }, + }); + } - return { - /** - * @param {import('nodemailer').SendMailOptions} mailOptions - */ - send: async function (mailOptions) { - await transport.sendMail(mailOptions); - }, - }; + /** + * @param {import('nodemailer').SendMailOptions} mailOptions + */ + async send(mailOptions) { + await this.transport.sendMail(mailOptions); + } } + +export default MailService; diff --git a/email-contact-form/src/main.js b/email-contact-form/src/main.js index f0438618..95882444 100644 --- a/email-contact-form/src/main.js +++ b/email-contact-form/src/main.js @@ -11,10 +11,9 @@ const __dirname = path.dirname(__filename); const staticFolder = path.join(__dirname, '../static'); export default async ({ req, res, log, error }) => { - const environment = EnvironmentService(); - const { SUBMIT_EMAIL, ALLOWED_ORIGINS } = environment; + const env = new EnvironmentService(); - if (ALLOWED_ORIGINS === '*') { + if (env.ALLOWED_ORIGINS === '*') { log( 'WARNING: Allowing requests from any origin - this is a security risk!' ); @@ -39,8 +38,8 @@ export default async ({ req, res, log, error }) => { return res.redirect(urlWithCodeParam(referer, ErrorCode.INVALID_REQUEST)); } - const cors = CorsService(origin, environment); - const mail = MailService(environment); + const cors = new CorsService(req, env); + const mail = new MailService(env); if (!cors.isOriginPermitted()) { error('Origin not permitted.'); @@ -61,7 +60,7 @@ export default async ({ req, res, log, error }) => { try { mail.send({ - to: SUBMIT_EMAIL, + to: env.SUBMIT_EMAIL, from: form.email, subject: `New form submission: ${origin}`, text: templateFormMessage(form), From c46568ef68fc4df95ca325af8c86e0aed64a8f37 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Thu, 20 Jul 2023 10:35:38 +0100 Subject: [PATCH 08/14] chore: new structure --- {email-contact-form => node/email-contact-form}/.gitignore | 0 {email-contact-form => node/email-contact-form}/.prettierrc.json | 0 {email-contact-form => node/email-contact-form}/README.md | 0 {email-contact-form => node/email-contact-form}/env.d.ts | 0 {email-contact-form => node/email-contact-form}/package-lock.json | 0 {email-contact-form => node/email-contact-form}/package.json | 0 {email-contact-form => node/email-contact-form}/src/cors.js | 0 .../email-contact-form}/src/environment.js | 0 {email-contact-form => node/email-contact-form}/src/mail.js | 0 {email-contact-form => node/email-contact-form}/src/main.js | 0 {email-contact-form => node/email-contact-form}/static/index.html | 0 .../email-contact-form}/static/success.html | 0 12 files changed, 0 insertions(+), 0 deletions(-) rename {email-contact-form => node/email-contact-form}/.gitignore (100%) rename {email-contact-form => node/email-contact-form}/.prettierrc.json (100%) rename {email-contact-form => node/email-contact-form}/README.md (100%) rename {email-contact-form => node/email-contact-form}/env.d.ts (100%) rename {email-contact-form => node/email-contact-form}/package-lock.json (100%) rename {email-contact-form => node/email-contact-form}/package.json (100%) rename {email-contact-form => node/email-contact-form}/src/cors.js (100%) rename {email-contact-form => node/email-contact-form}/src/environment.js (100%) rename {email-contact-form => node/email-contact-form}/src/mail.js (100%) rename {email-contact-form => node/email-contact-form}/src/main.js (100%) rename {email-contact-form => node/email-contact-form}/static/index.html (100%) rename {email-contact-form => node/email-contact-form}/static/success.html (100%) diff --git a/email-contact-form/.gitignore b/node/email-contact-form/.gitignore similarity index 100% rename from email-contact-form/.gitignore rename to node/email-contact-form/.gitignore diff --git a/email-contact-form/.prettierrc.json b/node/email-contact-form/.prettierrc.json similarity index 100% rename from email-contact-form/.prettierrc.json rename to node/email-contact-form/.prettierrc.json diff --git a/email-contact-form/README.md b/node/email-contact-form/README.md similarity index 100% rename from email-contact-form/README.md rename to node/email-contact-form/README.md diff --git a/email-contact-form/env.d.ts b/node/email-contact-form/env.d.ts similarity index 100% rename from email-contact-form/env.d.ts rename to node/email-contact-form/env.d.ts diff --git a/email-contact-form/package-lock.json b/node/email-contact-form/package-lock.json similarity index 100% rename from email-contact-form/package-lock.json rename to node/email-contact-form/package-lock.json diff --git a/email-contact-form/package.json b/node/email-contact-form/package.json similarity index 100% rename from email-contact-form/package.json rename to node/email-contact-form/package.json diff --git a/email-contact-form/src/cors.js b/node/email-contact-form/src/cors.js similarity index 100% rename from email-contact-form/src/cors.js rename to node/email-contact-form/src/cors.js diff --git a/email-contact-form/src/environment.js b/node/email-contact-form/src/environment.js similarity index 100% rename from email-contact-form/src/environment.js rename to node/email-contact-form/src/environment.js diff --git a/email-contact-form/src/mail.js b/node/email-contact-form/src/mail.js similarity index 100% rename from email-contact-form/src/mail.js rename to node/email-contact-form/src/mail.js diff --git a/email-contact-form/src/main.js b/node/email-contact-form/src/main.js similarity index 100% rename from email-contact-form/src/main.js rename to node/email-contact-form/src/main.js diff --git a/email-contact-form/static/index.html b/node/email-contact-form/static/index.html similarity index 100% rename from email-contact-form/static/index.html rename to node/email-contact-form/static/index.html diff --git a/email-contact-form/static/success.html b/node/email-contact-form/static/success.html similarity index 100% rename from email-contact-form/static/success.html rename to node/email-contact-form/static/success.html From 4a20a5102ed003924f4ff790d8247d03e2cd3992 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Thu, 20 Jul 2023 12:06:07 +0100 Subject: [PATCH 09/14] chore: prettier script --- node/email-contact-form/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/email-contact-form/package.json b/node/email-contact-form/package.json index 17dde79e..420dd797 100644 --- a/node/email-contact-form/package.json +++ b/node/email-contact-form/package.json @@ -5,7 +5,7 @@ "main": "src/main.js", "type": "module", "scripts": { - "format": "prettier --write src/**/*.js" + "format": "prettier --write ." }, "dependencies": { "nodemailer": "^6.9.3" From 978ce45c1ab1c57bf626718ba0c3c5cb9b8e9cae Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Mon, 24 Jul 2023 16:14:15 +0100 Subject: [PATCH 10/14] docs: update to template --- node/email-contact-form/README.md | 109 +++++++++++++++++++++++------- 1 file changed, 84 insertions(+), 25 deletions(-) diff --git a/node/email-contact-form/README.md b/node/email-contact-form/README.md index 57ca4fa6..a059e730 100644 --- a/node/email-contact-form/README.md +++ b/node/email-contact-form/README.md @@ -1,27 +1,48 @@ -# Email Contact Form Function +# ⚡ Email Contact Form Function -## Overview +Sends an email to the configured address with the contents of a contact form submission. -This function facilitates email submission from HTML forms using Appwrite. It validates form data, sends an email through an SMTP server, and handles redirection of the user based on the success or failure of the submission. +## 🧰 Usage -## Usage +### `GET` -### HTML Form +Returns a sample HTML form -To use this function, set the `action` attribute of your HTML form to your function URL, and include a hidden input with the name `_next` and the path of the redirect to on successful form submission (e.g. `/success`). +### `POST` -```html -
- - - - -
+Submit form data to send an email + +**Parameters** + +| Name | Description | Location | Type | Sample Value | +| ------ | --------------------------------- | ---------- | ------ | -------------------------------- | +| \_next | URL for redirect after submission | Form Param | String | `https://mywebapp.org/success` | +| \* | Any form values to send in email | Form Param | String | `Hey, I'd like to get in touch!` | + +**Response** + +Sample `200` Response: + +```text +Location: https://mywebapp.org/success ``` -## Environment Variables +Sample `400` Response: -This function depends on the following environment variables: +```text +Location: https://mywebapp.org/referer?error=Invalid+email+address +``` + +## ⚙️ Configuration + +| Setting | Value | +| ----------------- | --------------- | +| Runtime | Node (18.0) | +| Entrypoint | `src/main.js` | +| Build Commands | `npm install` | +| | `npm run setup` | +| Permissions | `any` | +| Timeout (Seconds) | 15 | - **SMTP_HOST** - SMTP server host - **SMTP_PORT** - SMTP server port @@ -30,21 +51,59 @@ This function depends on the following environment variables: - **SUBMIT_EMAIL** - The email address to send form submissions - **ALLOWED_ORIGINS** - An optional comma-separated list of allowed origins for CORS (defaults to `*`) -## Request +## 🔒 Environment Variables + +### SMTP_HOST + +The address of your SMTP server. Many STMP providers will provide this information in their documentation. Some popular providers include: Mailgun, SendGrid, and Gmail. + +| Question | Answer | +| ------------ | ------------------ | +| Required | Yes | +| Sample Value | `smtp.mailgun.org` | + +### SMTP_PORT + +The port of your STMP server. Commnly used ports include `25`, `465`, and `587`. + +| Question | Answer | +| ------------ | ------ | +| Required | Yes | +| Sample Value | `25` | + +### SMTP_USERNAME + +The username for your SMTP server. This is commonly your email address. + +| Question | Answer | +| ------------ | ----------------------- | +| Required | Yes | +| Sample Value | `no-reply@mywebapp.org` | + +### SMTP_PASSWORD -### Form Data +The password for your SMTP server. -- **_next_** - The URL to redirect to on successful form submission -- **email** - The sender's email address +| Question | Answer | +| ------------ | --------------------- | +| Required | Yes | +| Sample Value | `5up3r5tr0ngP4ssw0rd` | -- _Additional form data will be included in the email body_ +### SUBMIT_EMAIL -## Response +The email address to send form submissions to. -### Success Redirect +| Question | Answer | +| ------------ | ----------------- | +| Required | Yes | +| Sample Value | `me@mywebapp.org` | -On successful form submission, the function will redirect users to the URL provided in the `_next` form data. +### ALLOWED_ORIGINS -### Error Redirect +An optional comma-separated list of allowed origins for CORS (defaults to `*`). This is an important security measure to prevent malicious users from abusing your function. -In the case of errors such as invalid request methods, missing form data, or SMTP configuration issues, the function will redirect users back to the form URL with an appended error code for more precise error handling. Error codes include `invalid-request`, `missing-form-fields`, and generic `server-error`. +| Question | Answer | +| ------------- | ------------------------------------------------------------------- | +| Required | No | +| Sample Value | `https://mywebapp.org,https://mywebapp.com` | +| Documentation | [MDN: CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) | From 27545bf013680acde363f010b98dfd8014fd3b69 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Fri, 28 Jul 2023 12:39:17 +0100 Subject: [PATCH 11/14] chore: new utils --- node/email-contact-form/src/cors.js | 41 ++------ node/email-contact-form/src/environment.js | 22 ---- node/email-contact-form/src/mail.js | 16 ++- node/email-contact-form/src/main.js | 113 ++++++++------------- node/email-contact-form/src/utils.js | 57 +++++++++++ 5 files changed, 119 insertions(+), 130 deletions(-) delete mode 100644 node/email-contact-form/src/environment.js create mode 100644 node/email-contact-form/src/utils.js diff --git a/node/email-contact-form/src/cors.js b/node/email-contact-form/src/cors.js index 20509ce0..a2633a0d 100644 --- a/node/email-contact-form/src/cors.js +++ b/node/email-contact-form/src/cors.js @@ -1,32 +1,13 @@ -class CorsService { - /** - * @param {*} req - Request object - * @param {import('./environment').default} env - Environment variables - */ - constructor(req, env) { - this.env = env; - this.origin = req.headers['origin']; - } - - /** - * @returns {boolean} Whether the origin is allowed based on the ALLOWED_ORIGINS environment variable - */ - isOriginPermitted() { - if (!this.env.ALLOWED_ORIGINS || this.env.ALLOWED_ORIGINS === '*') - return true; - const allowedOriginsArray = this.env.ALLOWED_ORIGINS.split(','); - return allowedOriginsArray.includes(this.origin); - } - - /** - * @returns {Object} Access-Control-Allow-Origin header to be returned in the response - */ - getHeaders() { - return { - 'Access-Control-Allow-Origin': - this.env.ALLOWED_ORIGINS === '*' ? '*' : this.origin, - }; - } +export function isOriginPermitted(req) { + if (!process.env.ALLOWED_ORIGINS || process.env.ALLOWED_ORIGINS === '*') + return true; + const allowedOriginsArray = process.env.ALLOWED_ORIGINS.split(','); + return allowedOriginsArray.includes(this.origin); } -export default CorsService; +export function getCorsHeaders(req) { + return { + 'Access-Control-Allow-Origin': + process.env.ALLOWED_ORIGINS === '*' ? '*' : this.origin, + }; +} diff --git a/node/email-contact-form/src/environment.js b/node/email-contact-form/src/environment.js deleted file mode 100644 index 5aa7fd1f..00000000 --- a/node/email-contact-form/src/environment.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @param {string} key - * @return {string} - */ -function getRequiredEnv(key) { - const value = process.env[key]; - if (value === undefined) { - throw new Error(`Environment variable ${key} is not set`); - } - return value; -} - -class EnvironmentService { - SUBMIT_EMAIL = getRequiredEnv('SUBMIT_EMAIL'); - SMTP_HOST = getRequiredEnv('SMTP_HOST'); - SMTP_PORT = process.env.SMTP_PORT || 587; - SMTP_USERNAME = getRequiredEnv('SMTP_USERNAME'); - SMTP_PASSWORD = getRequiredEnv('SMTP_PASSWORD'); - ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS || '*'; -} - -export default EnvironmentService; diff --git a/node/email-contact-form/src/mail.js b/node/email-contact-form/src/mail.js index e18b07cc..3ed2c982 100644 --- a/node/email-contact-form/src/mail.js +++ b/node/email-contact-form/src/mail.js @@ -1,18 +1,16 @@ import nodemailer from 'nodemailer'; class MailService { - /** - * @param {import('./environment').default} env - */ - constructor(env) { - this.env = env; - + constructor() { this.transport = nodemailer.createTransport({ // @ts-ignore // Not sure what's going on here. - host: env.SMTP_HOST, - port: env.SMTP_PORT, - auth: { user: env.SMTP_USERNAME, pass: env.SMTP_PASSWORD }, + host: process.env.SMTP_HOST, + port: process.env.SMTP_PORT || 587, + auth: { + user: process.env.SMTP_USERNAME, + pass: process.env.SMTP_PASSWORD, + }, }); } diff --git a/node/email-contact-form/src/main.js b/node/email-contact-form/src/main.js index 95882444..dfdc6188 100644 --- a/node/email-contact-form/src/main.js +++ b/node/email-contact-form/src/main.js @@ -1,120 +1,95 @@ import querystring from 'node:querystring'; -import fs from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import path from 'node:path'; -import CorsService from './cors.js'; +import { getCorsHeaders, isOriginPermitted } from './cors.js'; import MailService from './mail.js'; -import EnvironmentService from './environment.js'; +import { + getStaticFile, + throwIfMissing, + urlWithCodeParam, + templateFormMessage, +} from './utils.js'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const staticFolder = path.join(__dirname, '../static'); +const ErrorCode = { + INVALID_REQUEST: 'invalid-request', + MISSING_FORM_FIELDS: 'missing-form-fields', + SERVER_ERROR: 'server-error', +}; export default async ({ req, res, log, error }) => { - const env = new EnvironmentService(); - - if (env.ALLOWED_ORIGINS === '*') { + throwIfMissing(process.env, [ + 'SUBMIT_EMAIL', + 'SMTP_HOST', + 'SMTP_USERNAME', + 'SMTP_PASSWORD', + ]); + + if (!process.env.ALLOWED_ORIGINS || process.env.ALLOWED_ORIGINS === '*') { log( 'WARNING: Allowing requests from any origin - this is a security risk!' ); } if (req.method === 'GET' && req.path === '/') { - const html = fs.readFileSync(path.join(staticFolder, 'index.html')); - return res.send(html.toString(), 200, { + return res.send(getStaticFile('index.html'), 200, { 'Content-Type': 'text/html; charset=utf-8', }); } - const referer = req.headers['referer']; - const origin = req.headers['origin']; - if (!referer || !origin) { - log('Missing referer or origin headers.'); - return res.json({ error: 'Missing referer or origin headers.' }, 400); - } + throwIfMissing(req.headers, ['referer', 'origin']); if (req.headers['content-type'] !== 'application/x-www-form-urlencoded') { log('Invalid request.'); - return res.redirect(urlWithCodeParam(referer, ErrorCode.INVALID_REQUEST)); + return res.redirect( + urlWithCodeParam(req.headers['referer'], 'Invalid request.') + ); } - const cors = new CorsService(req, env); - const mail = new MailService(env); + const mail = new MailService(); - if (!cors.isOriginPermitted()) { + if (!isOriginPermitted(req.headers['origin'])) { error('Origin not permitted.'); - return res.redirect(urlWithCodeParam(referer, ErrorCode.INVALID_REQUEST)); + return res.redirect( + urlWithCodeParam(req.headers['referer'], ErrorCode.INVALID_REQUEST) + ); } const form = querystring.parse(req.body); - - if (!(form.email && typeof form.email === 'string')) { - error('Missing form data.'); + try { + throwIfMissing(form, ['email']); + log('Form data is valid.'); + } catch (err) { return res.redirect( - urlWithCodeParam(referer, ErrorCode.MISSING_FORM_FIELDS), + urlWithCodeParam(req.headers['referer'], err.message), 301, - cors.getHeaders() + getCorsHeaders(req.headers['origin']) ); } - log('Form data is valid.'); try { mail.send({ - to: env.SUBMIT_EMAIL, - from: form.email, + to: process.env.SUBMIT_EMAIL, + from: /** @type {string} */ (form['email']), subject: `New form submission: ${origin}`, text: templateFormMessage(form), }); + log('Email sent successfully.'); } catch (err) { error(err.message); return res.redirect( - urlWithCodeParam(referer, ErrorCode.SERVER_ERROR), + urlWithCodeParam(req.headers['referer'], ErrorCode.SERVER_ERROR), 301, - cors.getHeaders() + getCorsHeaders(req.headers['origin']) ); } - log('Email sent successfully.'); - if (typeof form._next !== 'string' || !form._next) { - const html = fs.readFileSync(path.join(staticFolder, 'success.html')); - return res.send(html.toString(), 200, { + return res.send(getStaticFile('success.html'), 200, { 'Content-Type': 'text/html; charset=utf-8', }); } return res.redirect( - new URL(form._next, origin).toString(), + new URL(form._next, req.headers['origin']).toString(), 301, - cors.getHeaders() + getCorsHeaders(req.headers['origin']) ); }; - -/** - * Build a message from the form data. - * @param {import("node:querystring").ParsedUrlQuery} form - * @returns {string} - */ -function templateFormMessage(form) { - return `You've received a new message.\n -${Object.entries(form) - .filter(([key]) => key !== '_next') - .map(([key, value]) => `${key}: ${value}`) - .join('\n')}`; -} - -const ErrorCode = { - INVALID_REQUEST: 'invalid-request', - MISSING_FORM_FIELDS: 'missing-form-fields', - SERVER_ERROR: 'server-error', -}; - -/** - * @param {string} baseUrl - * @param {string} codeParam - */ -function urlWithCodeParam(baseUrl, codeParam) { - const url = new URL(baseUrl); - url.searchParams.set('code', codeParam); - return url.toString(); -} diff --git a/node/email-contact-form/src/utils.js b/node/email-contact-form/src/utils.js new file mode 100644 index 00000000..4a6e6d5c --- /dev/null +++ b/node/email-contact-form/src/utils.js @@ -0,0 +1,57 @@ +import path from 'path'; +import { fileURLToPath } from 'url'; +import fs from 'fs'; + +/** + * Throws an error if any of the keys are missing from the object + * @param {*} obj + * @param {string[]} keys + * @throws {Error} + */ +export function throwIfMissing(obj, keys) { + const missing = []; + for (let key of keys) { + if (!(key in obj) || !obj[key]) { + missing.push(key); + } + } + if (missing.length > 0) { + throw new Error(`Missing required fields: ${missing.join(', ')}`); + } +} + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const staticFolder = path.join(__dirname, '../static'); + +/** + * Returns the contents of a file in the static folder + * @param {string} fileName + * @returns {string} Contents of static/{fileName} + */ +export function getStaticFile(fileName) { + return fs.readFileSync(path.join(staticFolder, fileName)).toString(); +} + +/** + * Build a message from the form data. + * @param {import("node:querystring").ParsedUrlQuery} form + * @returns {string} + */ +export function templateFormMessage(form) { + return `You've received a new message.\n + ${Object.entries(form) + .filter(([key]) => key !== '_next') + .map(([key, value]) => `${key}: ${value}`) + .join('\n')}`; +} + +/** + * @param {string} baseUrl + * @param {string} codeParam + */ +export function urlWithCodeParam(baseUrl, codeParam) { + const url = new URL(baseUrl); + url.searchParams.set('code', codeParam); + return url.toString(); +} From de81b35cf044de09825a0ee13b57232ddfb1ebf4 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Fri, 28 Jul 2023 12:41:03 +0100 Subject: [PATCH 12/14] chore: remove excess logs --- node/email-contact-form/src/main.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/node/email-contact-form/src/main.js b/node/email-contact-form/src/main.js index dfdc6188..b25d5eb2 100644 --- a/node/email-contact-form/src/main.js +++ b/node/email-contact-form/src/main.js @@ -37,7 +37,6 @@ export default async ({ req, res, log, error }) => { throwIfMissing(req.headers, ['referer', 'origin']); if (req.headers['content-type'] !== 'application/x-www-form-urlencoded') { - log('Invalid request.'); return res.redirect( urlWithCodeParam(req.headers['referer'], 'Invalid request.') ); @@ -55,7 +54,6 @@ export default async ({ req, res, log, error }) => { const form = querystring.parse(req.body); try { throwIfMissing(form, ['email']); - log('Form data is valid.'); } catch (err) { return res.redirect( urlWithCodeParam(req.headers['referer'], err.message), @@ -71,7 +69,6 @@ export default async ({ req, res, log, error }) => { subject: `New form submission: ${origin}`, text: templateFormMessage(form), }); - log('Email sent successfully.'); } catch (err) { error(err.message); return res.redirect( From ca84ab2d44e404cb4d89270d11864d17d862c3eb Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Mon, 31 Jul 2023 10:47:15 +0100 Subject: [PATCH 13/14] chore: env.d.ts --- node/email-contact-form/env.d.ts | 8 ++++---- node/email-contact-form/src/cors.js | 6 ++++-- node/email-contact-form/src/mail.js | 25 ------------------------- node/email-contact-form/src/main.js | 6 ++---- node/email-contact-form/src/utils.js | 18 ++++++++++++++++++ 5 files changed, 28 insertions(+), 35 deletions(-) delete mode 100644 node/email-contact-form/src/mail.js diff --git a/node/email-contact-form/env.d.ts b/node/email-contact-form/env.d.ts index 6e31c21d..8da210de 100644 --- a/node/email-contact-form/env.d.ts +++ b/node/email-contact-form/env.d.ts @@ -1,11 +1,11 @@ declare global { namespace NodeJS { interface ProcessEnv { - STMP_HOST?: string; + STMP_HOST: string; STMP_PORT?: string; - STMP_USERNAME?: string; - STMP_PASSWORD?: string; - SUBMIT_EMAIL?: string; + STMP_USERNAME: string; + STMP_PASSWORD: string; + SUBMIT_EMAIL: string; ALLOWED_ORIGINS?: string; } } diff --git a/node/email-contact-form/src/cors.js b/node/email-contact-form/src/cors.js index a2633a0d..29d597e6 100644 --- a/node/email-contact-form/src/cors.js +++ b/node/email-contact-form/src/cors.js @@ -2,12 +2,14 @@ export function isOriginPermitted(req) { if (!process.env.ALLOWED_ORIGINS || process.env.ALLOWED_ORIGINS === '*') return true; const allowedOriginsArray = process.env.ALLOWED_ORIGINS.split(','); - return allowedOriginsArray.includes(this.origin); + return allowedOriginsArray.includes(req.headers['origin']); } export function getCorsHeaders(req) { return { 'Access-Control-Allow-Origin': - process.env.ALLOWED_ORIGINS === '*' ? '*' : this.origin, + !process.env.ALLOWED_ORIGINS || process.env.ALLOWED_ORIGINS === '*' + ? '*' + : req.headers['origin'], }; } diff --git a/node/email-contact-form/src/mail.js b/node/email-contact-form/src/mail.js deleted file mode 100644 index 3ed2c982..00000000 --- a/node/email-contact-form/src/mail.js +++ /dev/null @@ -1,25 +0,0 @@ -import nodemailer from 'nodemailer'; - -class MailService { - constructor() { - this.transport = nodemailer.createTransport({ - // @ts-ignore - // Not sure what's going on here. - host: process.env.SMTP_HOST, - port: process.env.SMTP_PORT || 587, - auth: { - user: process.env.SMTP_USERNAME, - pass: process.env.SMTP_PASSWORD, - }, - }); - } - - /** - * @param {import('nodemailer').SendMailOptions} mailOptions - */ - async send(mailOptions) { - await this.transport.sendMail(mailOptions); - } -} - -export default MailService; diff --git a/node/email-contact-form/src/main.js b/node/email-contact-form/src/main.js index b25d5eb2..5381e090 100644 --- a/node/email-contact-form/src/main.js +++ b/node/email-contact-form/src/main.js @@ -1,11 +1,11 @@ import querystring from 'node:querystring'; import { getCorsHeaders, isOriginPermitted } from './cors.js'; -import MailService from './mail.js'; import { getStaticFile, throwIfMissing, urlWithCodeParam, templateFormMessage, + sendEmail, } from './utils.js'; const ErrorCode = { @@ -42,8 +42,6 @@ export default async ({ req, res, log, error }) => { ); } - const mail = new MailService(); - if (!isOriginPermitted(req.headers['origin'])) { error('Origin not permitted.'); return res.redirect( @@ -63,7 +61,7 @@ export default async ({ req, res, log, error }) => { } try { - mail.send({ + sendEmail({ to: process.env.SUBMIT_EMAIL, from: /** @type {string} */ (form['email']), subject: `New form submission: ${origin}`, diff --git a/node/email-contact-form/src/utils.js b/node/email-contact-form/src/utils.js index 4a6e6d5c..5cbdd4db 100644 --- a/node/email-contact-form/src/utils.js +++ b/node/email-contact-form/src/utils.js @@ -1,6 +1,7 @@ import path from 'path'; import { fileURLToPath } from 'url'; import fs from 'fs'; +import nodemailer from 'nodemailer'; /** * Throws an error if any of the keys are missing from the object @@ -55,3 +56,20 @@ export function urlWithCodeParam(baseUrl, codeParam) { url.searchParams.set('code', codeParam); return url.toString(); } + +/** + * @param {import('nodemailer').SendMailOptions} options + */ +export async function sendEmail(options) { + const transport = nodemailer.createTransport({ + // @ts-ignore + // Not sure what's going on here. + host: process.env.SMTP_HOST, + port: process.env.SMTP_PORT || 587, + auth: { + user: process.env.SMTP_USERNAME, + pass: process.env.SMTP_PASSWORD, + }, + }); + await transport.sendMail(options); +} From d0d53715989a835466eb3e48b8752a879fb88089 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 1 Aug 2023 09:40:32 +0000 Subject: [PATCH 14/14] PR review changes --- node/email-contact-form/README.md | 13 +++---------- node/email-contact-form/src/main.js | 3 ++- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/node/email-contact-form/README.md b/node/email-contact-form/README.md index a059e730..88d654ca 100644 --- a/node/email-contact-form/README.md +++ b/node/email-contact-form/README.md @@ -1,12 +1,12 @@ -# ⚡ Email Contact Form Function +# 📬 Node.js Email Contact Form Function -Sends an email to the configured address with the contents of a contact form submission. +Sends an email with the contents of a HTML form. ## 🧰 Usage ### `GET` -Returns a sample HTML form +HTML form for interacting with the function. ### `POST` @@ -44,13 +44,6 @@ Location: https://mywebapp.org/referer?error=Invalid+email+address | Permissions | `any` | | Timeout (Seconds) | 15 | -- **SMTP_HOST** - SMTP server host -- **SMTP_PORT** - SMTP server port -- **SMTP_USERNAME** - SMTP server username -- **SMTP_PASSWORD** - SMTP server password -- **SUBMIT_EMAIL** - The email address to send form submissions -- **ALLOWED_ORIGINS** - An optional comma-separated list of allowed origins for CORS (defaults to `*`) - ## 🔒 Environment Variables ### SMTP_HOST diff --git a/node/email-contact-form/src/main.js b/node/email-contact-form/src/main.js index 5381e090..4a560f2c 100644 --- a/node/email-contact-form/src/main.js +++ b/node/email-contact-form/src/main.js @@ -37,8 +37,9 @@ export default async ({ req, res, log, error }) => { throwIfMissing(req.headers, ['referer', 'origin']); if (req.headers['content-type'] !== 'application/x-www-form-urlencoded') { + error('Incorrect content type.'); return res.redirect( - urlWithCodeParam(req.headers['referer'], 'Invalid request.') + urlWithCodeParam(req.headers['referer'], ErrorCode.INVALID_REQUEST) ); }