Contact
+
+ + Fill the form below to send us a message. +
+ +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 001/149] 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 @@ + + + + + + +
+ + Fill the form below to send us a message. +
+ +
+ + Your message has been sent! +
+
+ + This is demo application. You can ue this app to ensure + implementation with Chat GPT works properly. Use input below to + enter prompts and get a response. +
+
+ + This is demo application. You can use this app to ensure + implementation with Perspective API works properly. Use input below + to enter prompt and get toxicity of the message as response. +
+
+ + This is demo application. You can ue this app to ensure + implementation with Redact API works properly. Use input below to + enter text and get censored message as response. +
+
+ + This is demo application. You can ue this app to ensure sync between + Appwrite Databases and Algolia Seach was successful. Use search + input below to search your data. +
+
+ + This function listens to incoming webhooks from Vonage regarding + WhatsApp messages, and responds to them. To use the function, send + message to the WhatsApp user provided by Vonage. +
+
+ + This is demo application. You can ue this app to ensure sync between + Appwrite Databases and Meilisearch Seach was successful. Use search + input below to search your data. +
+
+