Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6,566 changes: 5,316 additions & 1,250 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,10 @@
"@testing-library/react": "^12.1.2",
"@types/friendly-words": "^1.2.2",
"@types/jest": "^29.5.14",
"@types/mjml": "^4.7.4",
"@types/node": "^16.18.126",
"@types/nodemailer": "^7.0.1",
"@types/nodemailer-mailgun-transport": "^1.4.6",
"@types/react": "^16.14.0",
"@types/react-dom": "^16.9.25",
"@types/react-router-dom": "^5.3.3",
Expand Down
10 changes: 5 additions & 5 deletions server/controllers/user.controller.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import crypto from 'crypto';

import User from '../models/user';
import mail from '../utils/mail';
import { mailerService } from '../utils/mail';
import { renderEmailConfirmation, renderResetPassword } from '../views/mail';

export * from './user.controller/apiKey';
Expand Down Expand Up @@ -83,7 +83,7 @@ export async function createUser(req, res) {
});

try {
await mail.send(mailOptions);
await mailerService.send(mailOptions);
res.json(userResponse(user));
} catch (mailErr) {
console.error(mailErr);
Expand Down Expand Up @@ -155,7 +155,7 @@ export async function resetPasswordInitiate(req, res) {
to: user.email
});

await mail.send(mailOptions);
await mailerService.send(mailOptions);
res.json({
success: true,
message:
Expand Down Expand Up @@ -203,7 +203,7 @@ export async function emailVerificationInitiate(req, res) {
to: user.email
});
try {
await mail.send(mailOptions);
await mailerService.send(mailOptions);
} catch (mailErr) {
res.status(500).send({ error: 'Error sending mail' });
return;
Expand Down Expand Up @@ -334,7 +334,7 @@ export async function updateSettings(req, res) {
to: user.email
});

await mail.send(mailOptions);
await mailerService.send(mailOptions);
} else {
await saveUser(res, user);
}
Expand Down
9 changes: 5 additions & 4 deletions server/migrations/emailConsolidation.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
moveObjectToUserInS3,
copyObjectInS3
} from '../controllers/aws.controller';
import mail from '../utils/mail';
import { mailerService } from '../utils/mail';
import { renderAccountConsolidation } from '../views/mail';

const mongoConnectionString = process.env.MONGO_URL;
Expand All @@ -31,7 +31,8 @@ mongoose.connection.on('error', () => {
* https://mongodb.github.io/node-mongodb-native
*/

const agg = [ // eslint-disable-line
const agg = [
// eslint-disable-line
{
$project: {
email: {
Expand Down Expand Up @@ -187,7 +188,7 @@ async function consolidateAccount(email) {
});

return new Promise((resolve, reject) => {
mail.send(mailOptions, (mailErr, result) => {
mailerService.send(mailOptions, (mailErr, result) => {
console.log('Sent email.');
if (mailErr) {
return reject(mailErr);
Expand Down Expand Up @@ -226,7 +227,7 @@ async function consolidateAccount(email) {
// });

// return new Promise((resolve, reject) => {
// mail.send(mailOptions, (mailErr, result) => {
// mailerService.send(mailOptions, (mailErr, result) => {
// console.log('Sent email.');
// if (mailErr) {
// return reject(mailErr);
Expand Down
65 changes: 65 additions & 0 deletions server/types/email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/** Rendered mail data for the mailer service, without the 'from' property, which will be automatically added */
export interface RenderedMailerData {
to: string;
subject: string;
html?: string;
}

// -------- EMAIL OPTIONS --------
/** Options to generate the account consolidation email */
export interface AccountConsolidationEmailOptions {
body: {
domain: string;
username: string;
email: string;
};
to: string;
}
/** Options to generate the reset password email */
export interface ResetPasswordEmailOptions {
body: {
domain: string;
link: string;
};
to: string;
}
/** Options to generate the confirm email email */
export interface ConfirmEmailEmailOptions {
body: {
domain: string;
link: string;
};
to: string;
}

// -------- EMAIL RENDERING TEMPLATES --------
/** Base template for emails */
export interface BaseEmailTemplate {
domain: string;
headingText: string;
greetingText: string;
messageText: string;
directLinkText: string;
noteText: string;
meta: {
keywords: string;
description: string;
};
}
/** Template for an email with a primary button, which contains text and a link */
export interface EmailWithPrimaryButtonTemplate extends BaseEmailTemplate {
link: string;
buttonText: string;
}
/** Template for rendering the account consolidation email */
export interface AccountConsolidationEmailTemplate extends BaseEmailTemplate {
username: string;
email: string;
message2Text: string;
resetPasswordLink: string;
resetPasswordText: string;
}
/** Template for rendering the confirm email email */
export type ConfirmEmailEmailTemplate = EmailWithPrimaryButtonTemplate;
/** Template for rendering the reset password email */
export type ResetPasswordEmailTemplate = EmailWithPrimaryButtonTemplate;
25 changes: 17 additions & 8 deletions server/utils/mail.js → server/utils/mail.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
/**
* Mail service wrapping around mailgun
*/

import nodemailer from 'nodemailer';
import mg from 'nodemailer-mailgun-transport';
import { RenderedMailerData } from '../types/email';

if (!process.env.MAILGUN_KEY) {
throw new Error('Mailgun key missing');
}

const auth = {
api_key: process.env.MAILGUN_KEY,
domain: process.env.MAILGUN_DOMAIN
};

/** Mail service class wrapping around mailgun */
class Mail {
client: nodemailer.Transporter;

sendOptions: Pick<nodemailer.SendMailOptions, 'from'>;

constructor() {
this.client = nodemailer.createTransport(mg({ auth }));
this.sendOptions = {
from: process.env.EMAIL_SENDER
};
}

async sendMail(mailOptions) {
async sendMail(mailOptions: nodemailer.SendMailOptions) {
try {
const response = await this.client.sendMail(mailOptions);
return response;
Expand All @@ -28,8 +34,8 @@ class Mail {
}
}

async send(data) {
const mailOptions = {
async send(data: RenderedMailerData) {
const mailOptions: nodemailer.SendMailOptions = {
from: this.sendOptions.from,
to: data.to,
subject: data.subject,
Expand All @@ -46,4 +52,7 @@ class Mail {
}
}

export default new Mail();
/**
* Mail service wrapping around mailgun
*/
export const mailerService = new Mail();
13 changes: 0 additions & 13 deletions server/utils/renderMjml.js

This file was deleted.

14 changes: 14 additions & 0 deletions server/utils/renderMjml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/* eslint-disable consistent-return */
import mjml2html from 'mjml';

/** Parse template string containing mjml tags into html for nodemailer.SendMailOptions.html */
export function renderMjml(template: string): string | undefined {
try {
const output = mjml2html(template);
return output.html;
} catch (e) {
console.error(e);
// fall through to undefined (null is not valid for nodemailer.SendMailOptions.html)
return undefined;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small nit but can we explicitly return undefined?

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export default ({
import type { AccountConsolidationEmailTemplate } from '../types/email';

/** Returns mjml for an Account Consolidation email */
export const consolidationMailLayout = ({
domain,
headingText,
greetingText,
Expand All @@ -11,7 +14,7 @@ export default ({
resetPasswordText,
noteText,
meta
}) =>
}: AccountConsolidationEmailTemplate) =>
`
<mjml>
<mj-head>
Expand Down
33 changes: 24 additions & 9 deletions server/views/mail.js → server/views/mail.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import renderMjml from '../utils/renderMjml';
import mailLayout from './mailLayout';
import consolidationMailLayout from './consolidationMailLayout';
import { renderMjml } from '../utils/renderMjml';
import { mailLayout } from './mailLayout';
import { consolidationMailLayout } from './consolidationMailLayout';
import {
AccountConsolidationEmailOptions,
AccountConsolidationEmailTemplate,
ResetPasswordEmailOptions,
ResetPasswordEmailTemplate,
ConfirmEmailEmailOptions,
ConfirmEmailEmailTemplate,
RenderedMailerData
} from '../types/email';

export const renderAccountConsolidation = (data) => {
export const renderAccountConsolidation = (
data: AccountConsolidationEmailOptions
): RenderedMailerData => {
const subject = 'p5.js Web Editor Account Consolidation';
const templateOptions = {
const templateOptions: AccountConsolidationEmailTemplate = {
domain: data.body.domain,
headingText: 'Account Consolidation',
greetingText: 'Hello,',
Expand Down Expand Up @@ -40,9 +51,11 @@ export const renderAccountConsolidation = (data) => {
return Object.assign({}, data, { html, subject });
};

export const renderResetPassword = (data) => {
export const renderResetPassword = (
data: ResetPasswordEmailOptions
): RenderedMailerData => {
const subject = 'p5.js Web Editor Password Reset';
const templateOptions = {
const templateOptions: ResetPasswordEmailTemplate = {
domain: data.body.domain,
headingText: 'Reset your password',
greetingText: 'Hello,',
Expand Down Expand Up @@ -71,9 +84,11 @@ export const renderResetPassword = (data) => {
return Object.assign({}, data, { html, subject });
};

export const renderEmailConfirmation = (data) => {
export const renderEmailConfirmation = (
data: ConfirmEmailEmailOptions
): RenderedMailerData => {
const subject = 'p5.js Email Verification';
const templateOptions = {
const templateOptions: ConfirmEmailEmailTemplate = {
domain: data.body.domain,
headingText: 'Email Verification',
greetingText: 'Hello,',
Expand Down
7 changes: 5 additions & 2 deletions server/views/mailLayout.js → server/views/mailLayout.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export default ({
import { EmailWithPrimaryButtonTemplate } from '../types/email';

/** Returns mjml for a standard email with a primary action button */
export const mailLayout = ({
domain,
headingText,
greetingText,
Expand All @@ -8,7 +11,7 @@ export default ({
directLinkText,
noteText,
meta
}) =>
}: EmailWithPrimaryButtonTemplate) =>
`
<mjml>
<mj-head>
Expand Down