├── .babelrc ├── .circleci └── config.yml ├── .env.json.example ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .storybook ├── addons.js ├── config.js └── webpack.config.js ├── .travis.yml ├── LICENSE ├── README.md ├── functions ├── userInvitation │ ├── .apexignore │ ├── .gitignore │ ├── schema.json │ ├── src │ │ ├── index.js │ │ ├── index.test.js │ │ ├── template.js │ │ ├── template.test.js │ │ └── template.text.js │ └── stories.js └── userReminder │ ├── .apexignore │ ├── .gitignore │ ├── schema.json │ ├── src │ ├── index.js │ ├── index.test.js │ ├── template.js │ ├── template.test.js │ └── template.text.js │ └── stories.js ├── impraise-logo.png ├── jest.config.js ├── package.json ├── project.json ├── shared ├── components │ ├── CallToAction.js │ ├── EmailContent.js │ ├── EmailWrapper.js │ ├── Footer.js │ └── Header.js ├── constants │ ├── colors.js │ ├── css.js │ ├── site.js │ ├── textVersion.js │ └── url.js ├── context.js ├── email.js ├── email.test.js └── images │ └── logo.png ├── utils ├── ci │ └── deploy └── tests │ ├── disableStyleWarnings.js │ ├── fileMock.js │ ├── setupEnzyme.js │ └── shims.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "targets": { 7 | "node": "6.10" 8 | } 9 | } 10 | ], 11 | "react", 12 | "stage-3" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: node:6 6 | steps: 7 | - checkout 8 | - run: 9 | name: Install dependencies 10 | command: yarn install --pure-lockfile 11 | - run: 12 | name: Run tests 13 | environment: 14 | JEST_JUNIT_OUTPUT: reports/junit/js-test-results.xml 15 | command: yarn run test --ci --testResultsProcessor="jest-junit" --coverage --verbose 16 | - store_test_results: 17 | path: reports/junit 18 | - store_artifacts: 19 | path: coverage 20 | destination: test-coverage 21 | - run: 22 | name: Install awscli 23 | command: apt-get update && apt-get install -yy awscli 24 | - deploy: 25 | command: ./utils/ci/deploy 26 | -------------------------------------------------------------------------------- /.env.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "POSTMARK_KEY": "paste_key_here" 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /functions/**/index.js 2 | /coverage 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | "standard", 4 | "prettier", 5 | "plugin:react/recommended", 6 | "plugin:jest/recommended" 7 | ], 8 | plugins: ["prettier", "react", "jest"], 9 | rules: { 10 | strict: 0, 11 | "prettier/prettier": "error" 12 | }, 13 | parser: "babel-eslint", 14 | env: { 15 | "jest/globals": true 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.env.json 3 | /coverage 4 | /build 5 | /functions/**/*.png 6 | .DS_Store 7 | /storybook-static 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /package.json 2 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import "@storybook/addon-options/register"; 2 | import "@storybook/addon-knobs/register"; 3 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from "@storybook/react"; 2 | import { setOptions } from "@storybook/addon-options"; 3 | 4 | setOptions({ 5 | name: "React-MailMerge", 6 | url: "https://github.com/impraise/react-mailmerge/", 7 | }); 8 | 9 | function loadStories() { 10 | require("../functions/userInvitation/stories.js"); 11 | require("../functions/userReminder/stories.js"); 12 | // You can require as many stories as you need. 13 | } 14 | 15 | configure(loadStories, module); 16 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | // module.exports = (baseConfig, env, defaultConfig) => { 4 | // defaultConfig.resolve = defaultConfig.resolve || {}; 5 | // defaultConfig.resolve.alias["shared"] = path.resolve(__dirname, "../shared"); 6 | // return defaultConfig; 7 | // }; 8 | 9 | module.exports = { 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.(png|jpg|gif)$/, 14 | use: { 15 | loader: "file-loader", 16 | options: { 17 | publicPath: process.env.ASSETS_BASE_URL 18 | } 19 | } 20 | } 21 | ] 22 | }, 23 | resolve: { 24 | alias: { 25 | shared: path.resolve(__dirname, "../shared") 26 | } 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: "node" 4 | 5 | install: 6 | - yarn install 7 | - yarn global add codecov 8 | 9 | script: 10 | - yarn test --coverage 11 | 12 | after_success: 13 | - codecov 14 | 15 | before_deploy: 16 | - yarn build-storybook 17 | 18 | deploy: 19 | provider: pages 20 | skip_cleanup: true 21 | github_token: $GITHUB_TOKEN 22 | local-dir: storybook-static 23 | on: 24 | branch: master 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Impraise 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React-MailMerge 2 | 3 | [![Build Status](https://travis-ci.org/impraise/react-mailmerge.svg?branch=master)](https://travis-ci.org/impraise/react-mailmerge) [![codecov](https://codecov.io/gh/impraise/react-mailmerge/branch/master/graph/badge.svg)](https://codecov.io/gh/impraise/react-mailmerge) 4 | 5 | Rendering templates is slow. If your website sends transactional email, 6 | it's probably rendering a lot of templates, since every email uses at least 7 | one template. 8 | 9 | Rather than making your backend servers render all those templates, 10 | React-MailMerge allows you to offload that work to AWS. React-MailMerge runs on 11 | [AWS Lambda](https://aws.amazon.com/lambda/), and exposes an API for each 12 | email template. Your backend servers can call these APIs with batches of 13 | user data, and React-MailMerge will render the templates on Lambda 14 | and pass the rendered versions to your mail delivery system. 15 | 16 | # AWS Lambda 17 | 18 | This project is designed to be deployed onto 19 | [AWS Lambda](https://aws.amazon.com/lambda) using the 20 | [Apex](http://apex.run) framework. 21 | 22 | ## AWS profile and credentials 23 | 24 | To deploy templates you will need an AWS account with access to 25 | lambda functions. The Apex documentation details the [minimum required 26 | IAM policy](http://apex.run/#aws-credentials) for this account. 27 | 28 | You should store your AWS credentials in the `~/.aws/credentials` file, 29 | like this: 30 | 31 | ``` 32 | [mailmerge] 33 | aws_access_key_id = xxxxxxxx 34 | aws_secret_access_key = xxxxxxxxxxxxxxxxxxxxxxxx 35 | ``` 36 | 37 | # Project Structure 38 | 39 | - `/functions` — Email templates. One template per function. 40 | - `/shared` — Code that is shared between templates. 41 | - `/utils` — Utility functions. 42 | 43 | # Credits 44 | 45 | **React-MailMerge** is developed and maintained by 46 | [Impraise](http://www.impraise.com/). 47 | Issue reports and pull requests are greatly appreciated! 48 | 49 | ![Impraise logo](impraise-logo.png) 50 | -------------------------------------------------------------------------------- /functions/userInvitation/.apexignore: -------------------------------------------------------------------------------- 1 | /src 2 | schema.json 3 | -------------------------------------------------------------------------------- /functions/userInvitation/.gitignore: -------------------------------------------------------------------------------- 1 | /index.js 2 | -------------------------------------------------------------------------------- /functions/userInvitation/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "baseUrl": { 5 | "description": "Links in the email should point to this base URL", 6 | "type": "string" 7 | }, 8 | "user": { 9 | "description": "The person receiving this email", 10 | "type": "object", 11 | "properties": { 12 | "name": { 13 | "type": "string" 14 | }, 15 | "email": { 16 | "type": "string" 17 | }, 18 | "invitationToken": { 19 | "type": "string" 20 | } 21 | }, 22 | "required": ["email", "invitationToken"] 23 | }, 24 | "invitedBy": { 25 | "description": "The existing user who invited the person receiving this email", 26 | "type": "object", 27 | "properties": { 28 | "name": { 29 | "type": "string" 30 | } 31 | }, 32 | "required": ["name"] 33 | }, 34 | "group": { 35 | "description": "The group that the person is being invited to join", 36 | "type": "object", 37 | "properties": { 38 | "name": { 39 | "type": "string" 40 | } 41 | }, 42 | "required": ["name"] 43 | } 44 | }, 45 | "required": ["baseUrl", "user", "invitedBy", "group"] 46 | } 47 | -------------------------------------------------------------------------------- /functions/userInvitation/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { renderEmail } from "react-html-email"; 3 | import Ajv from "ajv"; 4 | 5 | import schema from "../schema.json"; 6 | import Template from "./template"; 7 | import textTemplate from "./template.text"; 8 | import Client from "shared/email"; 9 | import * as site from "shared/constants/site"; 10 | 11 | const client = new Client(process.env.POSTMARK_TOKEN); 12 | 13 | export const handle = (data, context, callback) => { 14 | const ajv = new Ajv({ allErrors: true }); 15 | const isValid = ajv.validate(schema, data); 16 | if (!isValid) { 17 | return callback(`Validation errors: ${ajv.errorsText()}`); // eslint-disable-line 18 | } 19 | console.log( 20 | `Validated data. ${context.getRemainingTimeInMillis()}ms remaining until timeout.` 21 | ); 22 | 23 | const html = renderEmail(