2017-11-12 23:02:23 +00:00
|
|
|
|
// @flow
|
2020-06-20 20:59:15 +00:00
|
|
|
|
import nodemailer from "nodemailer";
|
|
|
|
|
import Oy from "oy-vey";
|
2020-08-09 05:53:59 +00:00
|
|
|
|
import * as React from "react";
|
2019-06-25 05:14:59 +00:00
|
|
|
|
import {
|
2020-08-09 05:53:59 +00:00
|
|
|
|
type Props as CollectionNotificationEmailT,
|
|
|
|
|
CollectionNotificationEmail,
|
|
|
|
|
collectionNotificationEmailText,
|
|
|
|
|
} from "./emails/CollectionNotificationEmail";
|
2018-12-05 06:24:30 +00:00
|
|
|
|
import {
|
|
|
|
|
type Props as DocumentNotificationEmailT,
|
|
|
|
|
DocumentNotificationEmail,
|
|
|
|
|
documentNotificationEmailText,
|
2020-06-20 20:59:15 +00:00
|
|
|
|
} from "./emails/DocumentNotificationEmail";
|
2021-08-28 21:27:07 +00:00
|
|
|
|
import {
|
|
|
|
|
ExportFailureEmail,
|
|
|
|
|
exportEmailFailureText,
|
|
|
|
|
} from "./emails/ExportFailureEmail";
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
ExportSuccessEmail,
|
|
|
|
|
exportEmailSuccessText,
|
|
|
|
|
} from "./emails/ExportSuccessEmail";
|
2018-12-05 06:24:30 +00:00
|
|
|
|
import {
|
2020-08-09 05:53:59 +00:00
|
|
|
|
type Props as InviteEmailT,
|
|
|
|
|
InviteEmail,
|
|
|
|
|
inviteEmailText,
|
|
|
|
|
} from "./emails/InviteEmail";
|
|
|
|
|
import { SigninEmail, signinEmailText } from "./emails/SigninEmail";
|
|
|
|
|
import { WelcomeEmail, welcomeEmailText } from "./emails/WelcomeEmail";
|
|
|
|
|
import { baseStyles } from "./emails/components/EmailLayout";
|
2021-09-15 01:04:35 +00:00
|
|
|
|
import Logger from "./logging/logger";
|
2021-08-28 04:42:13 +00:00
|
|
|
|
import { emailsQueue } from "./queues";
|
2018-06-21 04:33:21 +00:00
|
|
|
|
|
2020-12-22 03:27:14 +00:00
|
|
|
|
const useTestEmailService =
|
2021-03-10 20:04:42 +00:00
|
|
|
|
process.env.NODE_ENV === "development" && !process.env.SMTP_USERNAME;
|
2018-06-21 04:33:21 +00:00
|
|
|
|
|
2021-09-01 00:41:57 +00:00
|
|
|
|
export type EmailTypes =
|
|
|
|
|
| "welcome"
|
|
|
|
|
| "export"
|
|
|
|
|
| "invite"
|
|
|
|
|
| "signin"
|
|
|
|
|
| "exportFailure"
|
|
|
|
|
| "exportSuccess";
|
2017-11-12 23:02:23 +00:00
|
|
|
|
|
2021-08-28 04:42:13 +00:00
|
|
|
|
export type EmailSendOptions = {
|
2017-11-12 23:02:23 +00:00
|
|
|
|
to: string,
|
|
|
|
|
properties?: any,
|
|
|
|
|
title: string,
|
|
|
|
|
previewText?: string,
|
|
|
|
|
text: string,
|
2018-05-05 23:16:08 +00:00
|
|
|
|
html: React.Node,
|
2017-11-12 23:02:23 +00:00
|
|
|
|
headCSS?: string,
|
2018-06-21 04:33:21 +00:00
|
|
|
|
};
|
|
|
|
|
|
2017-11-12 23:02:23 +00:00
|
|
|
|
/**
|
|
|
|
|
* Mailer
|
|
|
|
|
*
|
|
|
|
|
* Mailer class to contruct and send emails.
|
|
|
|
|
*
|
2017-11-13 00:35:23 +00:00
|
|
|
|
* To preview emails, add a new preview to `emails/index.js` if they
|
|
|
|
|
* require additional data (properties). Otherwise preview will work automatically.
|
2017-11-12 23:02:23 +00:00
|
|
|
|
*
|
|
|
|
|
* HTML: http://localhost:3000/email/:email_type/html
|
|
|
|
|
* TEXT: http://localhost:3000/email/:email_type/text
|
|
|
|
|
*/
|
2018-12-05 06:24:30 +00:00
|
|
|
|
export class Mailer {
|
2017-11-12 23:02:23 +00:00
|
|
|
|
transporter: ?any;
|
|
|
|
|
|
2021-08-28 04:42:13 +00:00
|
|
|
|
constructor() {
|
|
|
|
|
this.loadTransport();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async loadTransport() {
|
|
|
|
|
if (process.env.SMTP_HOST) {
|
|
|
|
|
let smtpConfig = {
|
|
|
|
|
host: process.env.SMTP_HOST,
|
|
|
|
|
port: process.env.SMTP_PORT,
|
|
|
|
|
secure:
|
|
|
|
|
"SMTP_SECURE" in process.env
|
|
|
|
|
? process.env.SMTP_SECURE === "true"
|
|
|
|
|
: process.env.NODE_ENV === "production",
|
|
|
|
|
auth: undefined,
|
|
|
|
|
tls:
|
|
|
|
|
"SMTP_TLS_CIPHERS" in process.env
|
|
|
|
|
? { ciphers: process.env.SMTP_TLS_CIPHERS }
|
|
|
|
|
: undefined,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (process.env.SMTP_USERNAME) {
|
|
|
|
|
smtpConfig.auth = {
|
|
|
|
|
user: process.env.SMTP_USERNAME,
|
|
|
|
|
pass: process.env.SMTP_PASSWORD,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.transporter = nodemailer.createTransport(smtpConfig);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (useTestEmailService) {
|
2021-09-15 01:04:35 +00:00
|
|
|
|
Logger.info(
|
|
|
|
|
"email",
|
|
|
|
|
"SMTP_USERNAME not provided, generating test account…"
|
|
|
|
|
);
|
2021-08-28 04:42:13 +00:00
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
let testAccount = await nodemailer.createTestAccount();
|
|
|
|
|
|
|
|
|
|
const smtpConfig = {
|
|
|
|
|
host: "smtp.ethereal.email",
|
|
|
|
|
port: 587,
|
|
|
|
|
secure: false,
|
|
|
|
|
auth: {
|
|
|
|
|
user: testAccount.user,
|
|
|
|
|
pass: testAccount.pass,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.transporter = nodemailer.createTransport(smtpConfig);
|
|
|
|
|
} catch (err) {
|
2021-09-15 01:04:35 +00:00
|
|
|
|
Logger.error(
|
|
|
|
|
"Couldn't generate a test account with ethereal.email",
|
|
|
|
|
err
|
|
|
|
|
);
|
2021-08-28 04:42:13 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sendMail = async (data: EmailSendOptions): ?Promise<*> => {
|
2017-11-13 00:35:23 +00:00
|
|
|
|
const { transporter } = this;
|
|
|
|
|
|
|
|
|
|
if (transporter) {
|
2017-11-12 23:02:23 +00:00
|
|
|
|
const html = Oy.renderTemplate(data.html, {
|
|
|
|
|
title: data.title,
|
2020-06-20 20:59:15 +00:00
|
|
|
|
headCSS: [baseStyles, data.headCSS].join(" "),
|
2017-11-12 23:02:23 +00:00
|
|
|
|
previewText: data.previewText,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
try {
|
2021-09-15 01:04:35 +00:00
|
|
|
|
Logger.info("email", `Sending email "${data.title}" to ${data.to}`);
|
2020-12-22 03:27:14 +00:00
|
|
|
|
const info = await transporter.sendMail({
|
2017-11-18 21:11:12 +00:00
|
|
|
|
from: process.env.SMTP_FROM_EMAIL,
|
|
|
|
|
replyTo: process.env.SMTP_REPLY_EMAIL || process.env.SMTP_FROM_EMAIL,
|
2017-11-12 23:02:23 +00:00
|
|
|
|
to: data.to,
|
|
|
|
|
subject: data.title,
|
|
|
|
|
html: html,
|
|
|
|
|
text: data.text,
|
|
|
|
|
});
|
2020-12-22 03:27:14 +00:00
|
|
|
|
|
|
|
|
|
if (useTestEmailService) {
|
2021-09-15 01:04:35 +00:00
|
|
|
|
Logger.info(
|
|
|
|
|
"email",
|
|
|
|
|
`Preview Url: ${nodemailer.getTestMessageUrl(info)}`
|
|
|
|
|
);
|
2020-12-22 03:27:14 +00:00
|
|
|
|
}
|
2018-06-21 04:33:21 +00:00
|
|
|
|
} catch (err) {
|
2021-09-15 01:04:35 +00:00
|
|
|
|
Logger.error(`Error sending email to ${data.to}`, err);
|
2018-06-21 04:33:21 +00:00
|
|
|
|
throw err; // Re-throw for queue to re-try
|
2017-11-12 23:02:23 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2019-01-27 12:30:53 +00:00
|
|
|
|
welcome = async (opts: { to: string, teamUrl: string }) => {
|
2017-11-12 23:02:23 +00:00
|
|
|
|
this.sendMail({
|
2017-12-19 04:47:48 +00:00
|
|
|
|
to: opts.to,
|
2020-06-20 20:59:15 +00:00
|
|
|
|
title: "Welcome to Outline",
|
2017-11-12 23:02:23 +00:00
|
|
|
|
previewText:
|
2020-06-20 20:59:15 +00:00
|
|
|
|
"Outline is a place for your team to build and share knowledge.",
|
2019-01-27 12:30:53 +00:00
|
|
|
|
html: <WelcomeEmail {...opts} />,
|
|
|
|
|
text: welcomeEmailText(opts),
|
2017-11-12 23:02:23 +00:00
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2021-08-29 19:35:55 +00:00
|
|
|
|
exportSuccess = async (opts: { to: string, id: string, teamUrl: string }) => {
|
2018-06-21 04:33:21 +00:00
|
|
|
|
this.sendMail({
|
|
|
|
|
to: opts.to,
|
2020-06-20 20:59:15 +00:00
|
|
|
|
title: "Your requested export",
|
2018-06-21 04:33:21 +00:00
|
|
|
|
previewText: "Here's your request data export from Outline",
|
2021-08-28 21:27:07 +00:00
|
|
|
|
html: <ExportSuccessEmail id={opts.id} teamUrl={opts.teamUrl} />,
|
|
|
|
|
text: exportEmailSuccessText,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2021-08-29 19:35:55 +00:00
|
|
|
|
exportFailure = async (opts: { to: string, teamUrl: string }) => {
|
2021-08-28 21:27:07 +00:00
|
|
|
|
this.sendMail({
|
|
|
|
|
to: opts.to,
|
|
|
|
|
title: "Your requested export",
|
|
|
|
|
previewText: "Sorry, your requested data export has failed",
|
|
|
|
|
html: <ExportFailureEmail teamUrl={opts.teamUrl} />,
|
|
|
|
|
text: exportEmailFailureText,
|
2018-06-21 04:33:21 +00:00
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2019-06-25 05:14:59 +00:00
|
|
|
|
invite = async (opts: { to: string } & InviteEmailT) => {
|
|
|
|
|
this.sendMail({
|
|
|
|
|
to: opts.to,
|
2020-08-09 01:53:11 +00:00
|
|
|
|
title: `${opts.actorName} invited you to join ${opts.teamName}’s knowledge base`,
|
2019-06-25 05:14:59 +00:00
|
|
|
|
previewText:
|
2020-06-20 20:59:15 +00:00
|
|
|
|
"Outline is a place for your team to build and share knowledge.",
|
2019-06-25 05:14:59 +00:00
|
|
|
|
html: <InviteEmail {...opts} />,
|
|
|
|
|
text: inviteEmailText(opts),
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2019-12-16 02:46:08 +00:00
|
|
|
|
signin = async (opts: { to: string, token: string, teamUrl: string }) => {
|
2021-11-16 00:05:58 +00:00
|
|
|
|
const signInLink = signinEmailText(opts);
|
|
|
|
|
|
|
|
|
|
if (process.env.NODE_ENV === "development") {
|
|
|
|
|
Logger.debug("email", `Sign-In link: ${signInLink}`);
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-16 02:46:08 +00:00
|
|
|
|
this.sendMail({
|
|
|
|
|
to: opts.to,
|
2020-06-20 20:59:15 +00:00
|
|
|
|
title: "Magic signin link",
|
|
|
|
|
previewText: "Here’s your link to signin to Outline.",
|
2019-12-16 02:46:08 +00:00
|
|
|
|
html: <SigninEmail {...opts} />,
|
2021-11-16 00:05:58 +00:00
|
|
|
|
text: signInLink,
|
2019-12-16 02:46:08 +00:00
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2018-12-05 06:24:30 +00:00
|
|
|
|
documentNotification = async (
|
|
|
|
|
opts: { to: string } & DocumentNotificationEmailT
|
|
|
|
|
) => {
|
|
|
|
|
this.sendMail({
|
|
|
|
|
to: opts.to,
|
2020-04-05 23:04:46 +00:00
|
|
|
|
title: `“${opts.document.title}” ${opts.eventName}`,
|
2018-12-05 06:24:30 +00:00
|
|
|
|
previewText: `${opts.actor.name} ${opts.eventName} a new document`,
|
|
|
|
|
html: <DocumentNotificationEmail {...opts} />,
|
|
|
|
|
text: documentNotificationEmailText(opts),
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
collectionNotification = async (
|
|
|
|
|
opts: { to: string } & CollectionNotificationEmailT
|
|
|
|
|
) => {
|
|
|
|
|
this.sendMail({
|
|
|
|
|
to: opts.to,
|
2020-04-05 23:04:46 +00:00
|
|
|
|
title: `“${opts.collection.name}” ${opts.eventName}`,
|
2018-12-05 06:24:30 +00:00
|
|
|
|
previewText: `${opts.actor.name} ${opts.eventName} a collection`,
|
|
|
|
|
html: <CollectionNotificationEmail {...opts} />,
|
|
|
|
|
text: collectionNotificationEmailText(opts),
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2021-08-28 04:42:13 +00:00
|
|
|
|
sendTemplate = async (type: EmailTypes, opts?: Object = {}) => {
|
|
|
|
|
await emailsQueue.add(
|
|
|
|
|
{
|
|
|
|
|
type,
|
|
|
|
|
opts,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
attempts: 5,
|
|
|
|
|
backoff: {
|
|
|
|
|
type: "exponential",
|
|
|
|
|
delay: 60 * 1000,
|
|
|
|
|
},
|
2021-06-22 04:40:28 +00:00
|
|
|
|
}
|
2021-08-28 04:42:13 +00:00
|
|
|
|
);
|
|
|
|
|
};
|
2017-11-12 23:02:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const mailer = new Mailer();
|
2018-12-05 06:24:30 +00:00
|
|
|
|
export default mailer;
|