Added email templating, and user welcome email
This commit is contained in:
66
server/__snapshots__/mailer.test.js.snap
Normal file
66
server/__snapshots__/mailer.test.js.snap
Normal file
@@ -0,0 +1,66 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Mailer #welcome 1`] = `
|
||||
Object {
|
||||
"from": "Outline <hello@mail.getoutline.com>",
|
||||
"html": "
|
||||
<!DOCTYPE html PUBLIC \\"-//W3C//DTD XHTML 1.0 Strict//EN\\" \\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\\">
|
||||
<html
|
||||
|
||||
dir=\\"ltr\\"
|
||||
xmlns=\\"http://www.w3.org/1999/xhtml\\"
|
||||
xmlns:v=\\"urn:schemas-microsoft-com:vml\\"
|
||||
xmlns:o=\\"urn:schemas-microsoft-com:office:office\\">
|
||||
<head>
|
||||
<meta http-equiv=\\"Content-Type\\" content=\\"text/html; charset=utf-8\\" />
|
||||
<meta http-equiv=\\"X-UA-Compatible\\" content=\\"IE=edge\\" />
|
||||
<meta name=\\"viewport\\" content=\\"width=device-width\\"/>
|
||||
|
||||
<title>Welcome to Outline</title>
|
||||
|
||||
<style type=\\"text/css\\">
|
||||
#__bodyTable__{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;font-size:16px;line-height:1.5}
|
||||
|
||||
#__bodyTable__ {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100% !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!--[if gte mso 9]>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body bgcolor=\\"#FFFFFF\\" width=\\"100%\\" style=\\"-webkit-font-smoothing: antialiased; width:100% !important; background:#FFFFFF;-webkit-text-size-adjust:none; margin:0; padding:0; min-width:100%; direction: ltr;\\">
|
||||
<table bgcolor=\\"#FFFFFF\\" id=\\"__bodyTable__\\" width=\\"100%\\" style=\\"-webkit-font-smoothing: antialiased; width:100% !important; background:#FFFFFF;-webkit-text-size-adjust:none; margin:0; padding:0; min-width:100%\\">
|
||||
<tr>
|
||||
<td align=\\"center\\">
|
||||
<span style=\\"display: none !important; color: #FFFFFF; margin:0; padding:0; font-size:1px; line-height:1px;\\">Outline is a place for your team to build and share knowledge.</span>
|
||||
<table width=\\"550\\" padding=\\"40\\" border=\\"0\\" cellSpacing=\\"0\\" cellPadding=\\"0\\"><tbody><tr><td align=\\"left\\"><table width=\\"100%\\" border=\\"0\\" cellSpacing=\\"0\\" cellPadding=\\"0\\"><tbody><tr><td><table width=\\"100%\\" border=\\"0\\" cellSpacing=\\"0\\" cellPadding=\\"0\\"><tbody><tr><td width=\\"100%\\" height=\\"40px\\" style=\\"line-height:40px;font-size:1px;mso-line-height-rule:exactly\\"> </td></tr></tbody></table><p><strong>Welcome to Outline!</strong></p><p>Outline is a place for your team to build and share knowledge.</p><p>To get started, head to your dashboard and try creating a collection to help document your workflow, create playbooks or help with team onboarding.</p><p>You can also import existing Markdown document by drag and dropping them to your collections</p><table width=\\"100%\\" border=\\"0\\" cellSpacing=\\"0\\" cellPadding=\\"0\\"><tbody><tr><td width=\\"100%\\" height=\\"10px\\" style=\\"line-height:10px;font-size:1px;mso-line-height-rule:exactly\\"> </td></tr></tbody></table><p><a href=\\"http://localhost:3000/dashboard\\" style=\\"display:inline-block;padding:10px 20px;color:#FFFFFF;background:#000000;border-radius:4px;font-weight:500;text-decoration:none;cursor:pointer\\">View my dashboard</a></p><table width=\\"100%\\" border=\\"0\\" cellSpacing=\\"0\\" cellPadding=\\"0\\"><tbody><tr><td width=\\"100%\\" height=\\"40px\\" style=\\"line-height:40px;font-size:1px;mso-line-height-rule:exactly\\"> </td></tr></tbody></table></td></tr></tbody></table><table width=\\"100%\\" border=\\"0\\" cellSpacing=\\"0\\" cellPadding=\\"0\\"><tbody><tr><td width=\\"75%\\" style=\\"padding:20px 0;border-top:1px solid #e8e8e8;color:#9BA6B2;font-size:14px\\"><a href=\\"http://localhost:3000\\" style=\\"color:#9BA6B2;text-decoration:none\\">Outline</a></td></tr></tbody></table></td></tr></tbody></table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
",
|
||||
"subject": "Welcome to Outline",
|
||||
"text": "
|
||||
Welcome to Outline!
|
||||
|
||||
Outline is a place for your team to build and share knowledge.
|
||||
|
||||
To get started, head to your dashboard and try creating a collection to help document your workflow, create playbooks or help with team onboarding.
|
||||
|
||||
You can also import existing Markdown document by drag and dropping them to your collections
|
||||
|
||||
http://localhost:3000/dashboard
|
||||
",
|
||||
"to": "user@example.com",
|
||||
}
|
||||
`;
|
||||
@@ -10,6 +10,12 @@ afterAll(server.close);
|
||||
|
||||
describe.skip('#auth.signup', async () => {
|
||||
it('should signup a new user', async () => {
|
||||
const welcomeEmailMock = jest.fn();
|
||||
jest.doMock('../mailer', () => {
|
||||
return {
|
||||
welcome: welcomeEmailMock,
|
||||
};
|
||||
});
|
||||
const res = await server.post('/api/auth.signup', {
|
||||
body: {
|
||||
username: 'testuser',
|
||||
@@ -23,6 +29,7 @@ describe.skip('#auth.signup', async () => {
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.ok).toBe(true);
|
||||
expect(body.data.user).toBeTruthy();
|
||||
expect(welcomeEmailMock).toBeCalledWith('new.user@example.com');
|
||||
});
|
||||
|
||||
it('should require params', async () => {
|
||||
|
||||
52
server/emails/WelcomeEmail.js
Normal file
52
server/emails/WelcomeEmail.js
Normal file
@@ -0,0 +1,52 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import EmailTemplate from './components/EmailLayout';
|
||||
import Body from './components/Body';
|
||||
import Button from './components/Button';
|
||||
import Footer from './components/Footer';
|
||||
import EmptySpace from './components/EmptySpace';
|
||||
|
||||
export const welcomeEmailText = `
|
||||
Welcome to Outline!
|
||||
|
||||
Outline is a place for your team to build and share knowledge.
|
||||
|
||||
To get started, head to your dashboard and try creating a collection to help document your workflow, create playbooks or help with team onboarding.
|
||||
|
||||
You can also import existing Markdown document by drag and dropping them to your collections
|
||||
|
||||
${process.env.URL}/dashboard
|
||||
`;
|
||||
|
||||
export const WelcomeEmail = () => {
|
||||
return (
|
||||
<EmailTemplate>
|
||||
<Body>
|
||||
<p>
|
||||
<strong>Welcome to Outline!</strong>
|
||||
</p>
|
||||
|
||||
<p>Outline is a place for your team to build and share knowledge.</p>
|
||||
<p>
|
||||
To get started, head to your dashboard and try creating a collection
|
||||
to help document your workflow, create playbooks or help with team
|
||||
onboarding.
|
||||
</p>
|
||||
<p>
|
||||
You can also import existing Markdown document by drag and dropping
|
||||
them to your collections
|
||||
</p>
|
||||
|
||||
<EmptySpace height={10} />
|
||||
|
||||
<p>
|
||||
<Button href={`${process.env.URL}/dashboard`}>
|
||||
View my dashboard
|
||||
</Button>
|
||||
</p>
|
||||
</Body>
|
||||
|
||||
<Footer />
|
||||
</EmailTemplate>
|
||||
);
|
||||
};
|
||||
25
server/emails/components/Body.js
Normal file
25
server/emails/components/Body.js
Normal file
@@ -0,0 +1,25 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import { Table, TBody, TR, TD } from 'oy-vey';
|
||||
|
||||
import EmptySpace from './EmptySpace';
|
||||
|
||||
type Props = {
|
||||
children: React$Element<*>,
|
||||
};
|
||||
|
||||
export default ({ children }: Props) => {
|
||||
return (
|
||||
<Table width="100%">
|
||||
<TBody>
|
||||
<TR>
|
||||
<TD>
|
||||
<EmptySpace height={40} />
|
||||
{children}
|
||||
<EmptySpace height={40} />
|
||||
</TD>
|
||||
</TR>
|
||||
</TBody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
17
server/emails/components/Button.js
Normal file
17
server/emails/components/Button.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
|
||||
export default (props: { href: string, children: React.Element<*> }) => {
|
||||
const style = {
|
||||
display: 'inline-block',
|
||||
padding: '10px 20px',
|
||||
color: '#FFFFFF',
|
||||
background: '#000000',
|
||||
borderRadius: '4px',
|
||||
fontWeight: 500,
|
||||
textDecoration: 'none',
|
||||
cursor: 'pointer',
|
||||
};
|
||||
|
||||
return <a {...props} style={style} />;
|
||||
};
|
||||
26
server/emails/components/EmailLayout.js
Normal file
26
server/emails/components/EmailLayout.js
Normal file
@@ -0,0 +1,26 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import { Table, TBody, TR, TD } from 'oy-vey';
|
||||
import { fonts } from '../../../shared/styles/constants';
|
||||
|
||||
type Props = {
|
||||
children: React$Element<*>,
|
||||
};
|
||||
|
||||
export default (props: Props) => (
|
||||
<Table width="550" padding="40">
|
||||
<TBody>
|
||||
<TR>
|
||||
<TD align="left">{props.children}</TD>
|
||||
</TR>
|
||||
</TBody>
|
||||
</Table>
|
||||
);
|
||||
|
||||
export const baseStyles = `
|
||||
#__bodyTable__ {
|
||||
font-family: ${fonts.regular};
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
`;
|
||||
29
server/emails/components/EmptySpace.js
Normal file
29
server/emails/components/EmptySpace.js
Normal file
@@ -0,0 +1,29 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import { Table, TBody, TR, TD } from 'oy-vey';
|
||||
|
||||
const EmptySpace = ({ height }: { height?: number }) => {
|
||||
height = height || 16;
|
||||
const style = {
|
||||
lineHeight: `${height}px`,
|
||||
fontSize: '1px',
|
||||
msoLineHeightRule: 'exactly',
|
||||
};
|
||||
|
||||
return (
|
||||
<Table width="100%">
|
||||
<TBody>
|
||||
<TR>
|
||||
<TD
|
||||
width="100%"
|
||||
height={`${height}px`}
|
||||
style={style}
|
||||
dangerouslySetInnerHTML={{ __html: ' ' }}
|
||||
/>
|
||||
</TR>
|
||||
</TBody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmptySpace;
|
||||
31
server/emails/components/Footer.js
Normal file
31
server/emails/components/Footer.js
Normal file
@@ -0,0 +1,31 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import { Table, TBody, TR, TD } from 'oy-vey';
|
||||
|
||||
export default () => {
|
||||
const style = {
|
||||
padding: '20px 0',
|
||||
borderTop: '1px solid #e8e8e8',
|
||||
color: '#9BA6B2',
|
||||
fontSize: '14px',
|
||||
};
|
||||
|
||||
const linkStyle = {
|
||||
color: '#9BA6B2',
|
||||
textDecoration: 'none',
|
||||
};
|
||||
|
||||
return (
|
||||
<Table width="100%">
|
||||
<TBody>
|
||||
<TR>
|
||||
<TD width="75%" style={style}>
|
||||
<a href={process.env.URL} style={linkStyle}>
|
||||
Outline
|
||||
</a>
|
||||
</TD>
|
||||
</TR>
|
||||
</TBody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
35
server/emails/index.js
Normal file
35
server/emails/index.js
Normal file
@@ -0,0 +1,35 @@
|
||||
// @flow
|
||||
import Koa from 'koa';
|
||||
import Router from 'koa-router';
|
||||
import { Mailer } from '../mailer';
|
||||
|
||||
const emailPreviews = new Koa();
|
||||
const router = new Router();
|
||||
|
||||
router.get('/:type/:format', async ctx => {
|
||||
const previewMailer = new Mailer();
|
||||
let mailerOutput;
|
||||
previewMailer.transporter = {
|
||||
sendMail: data => (mailerOutput = data),
|
||||
};
|
||||
|
||||
switch (ctx.params.type) {
|
||||
case 'welcome':
|
||||
previewMailer.welcome('user@example.com');
|
||||
break;
|
||||
default:
|
||||
console.log(1);
|
||||
}
|
||||
|
||||
if (!mailerOutput) return;
|
||||
|
||||
if (ctx.params.format === 'text') {
|
||||
ctx.body = mailerOutput.text;
|
||||
} else {
|
||||
ctx.body = mailerOutput.html;
|
||||
}
|
||||
});
|
||||
|
||||
emailPreviews.use(router.routes());
|
||||
|
||||
export default emailPreviews;
|
||||
@@ -8,6 +8,7 @@ import bugsnag from 'bugsnag';
|
||||
import updates from './utils/updates';
|
||||
|
||||
import api from './api';
|
||||
import emails from './emails';
|
||||
import routes from './routes';
|
||||
|
||||
const app = new Koa();
|
||||
@@ -71,6 +72,10 @@ if (process.env.NODE_ENV === 'production' && process.env.BUGSNAG_KEY) {
|
||||
app.on('error', bugsnag.koaHandler);
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
app.use(mount('/emails', emails));
|
||||
}
|
||||
|
||||
app.use(mount('/api', api));
|
||||
app.use(mount(routes));
|
||||
|
||||
@@ -85,7 +90,7 @@ app.use(
|
||||
|
||||
/**
|
||||
* Production updates and anonymous analytics.
|
||||
*
|
||||
*
|
||||
* Set ENABLE_UPDATES=false to disable them for your installation
|
||||
*/
|
||||
if (
|
||||
|
||||
91
server/mailer.js
Normal file
91
server/mailer.js
Normal file
@@ -0,0 +1,91 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import nodemailer from 'nodemailer';
|
||||
import Oy from 'oy-vey';
|
||||
import invariant from 'invariant';
|
||||
import { baseStyles } from './emails/components/EmailLayout';
|
||||
|
||||
import { WelcomeEmail, welcomeEmailText } from './emails/WelcomeEmail';
|
||||
|
||||
type SendMailType = {
|
||||
to: string,
|
||||
properties?: any,
|
||||
title: string,
|
||||
previewText?: string,
|
||||
text: string,
|
||||
html: React.Element<*>,
|
||||
headCSS?: string,
|
||||
};
|
||||
|
||||
/**
|
||||
* Mailer
|
||||
*
|
||||
* Mailer class to contruct and send emails.
|
||||
*
|
||||
* To preview emails, add a new preview to `emails/index.js` and visit following
|
||||
* URLs in development mode:
|
||||
*
|
||||
* HTML: http://localhost:3000/email/:email_type/html
|
||||
* TEXT: http://localhost:3000/email/:email_type/text
|
||||
*/
|
||||
class Mailer {
|
||||
transporter: ?any;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
sendMail = async (data: SendMailType): ?Promise<*> => {
|
||||
if (this.transporter) {
|
||||
const html = Oy.renderTemplate(data.html, {
|
||||
title: data.title,
|
||||
headCSS: [baseStyles, data.headCSS].join(' '),
|
||||
previewText: data.previewText,
|
||||
});
|
||||
|
||||
invariant(this.transporter, 'very sure this.transporter exists');
|
||||
try {
|
||||
await this.transporter.sendMail({
|
||||
from: process.env.SMTP_SENDER_EMAIL,
|
||||
to: data.to,
|
||||
subject: data.title,
|
||||
html: html,
|
||||
text: data.text,
|
||||
});
|
||||
} catch (e) {
|
||||
Bugsnag.notifyException(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
welcome = async (to: string) => {
|
||||
this.sendMail({
|
||||
to,
|
||||
title: 'Welcome to Outline',
|
||||
previewText:
|
||||
'Outline is a place for your team to build and share knowledge.',
|
||||
html: <WelcomeEmail />,
|
||||
text: welcomeEmailText,
|
||||
});
|
||||
};
|
||||
|
||||
constructor() {
|
||||
if (process.env.SMTP_HOST) {
|
||||
let smtpConfig = {
|
||||
host: process.env.SMTP_HOST,
|
||||
port: process.env.SMTP_PORT,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: process.env.SMTP_USERNAME,
|
||||
pass: process.env.SMTP_PASSWORD,
|
||||
},
|
||||
};
|
||||
|
||||
this.transporter = nodemailer.createTransport(smtpConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mailer = new Mailer();
|
||||
|
||||
export { Mailer };
|
||||
export default mailer;
|
||||
19
server/mailer.test.js
Normal file
19
server/mailer.test.js
Normal file
@@ -0,0 +1,19 @@
|
||||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
import { Mailer } from './mailer';
|
||||
|
||||
describe('Mailer', () => {
|
||||
let fakeMailer;
|
||||
let sendMailOutput;
|
||||
|
||||
beforeEach(() => {
|
||||
fakeMailer = new Mailer();
|
||||
fakeMailer.transporter = {
|
||||
sendMail: output => (sendMailOutput = output),
|
||||
};
|
||||
});
|
||||
|
||||
test('#welcome', () => {
|
||||
fakeMailer.welcome('user@example.com');
|
||||
expect(sendMailOutput).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import bcrypt from 'bcrypt';
|
||||
import uuid from 'uuid';
|
||||
import { DataTypes, sequelize, encryptedFields } from '../sequelize';
|
||||
import { uploadToS3FromUrl } from '../utils/s3';
|
||||
import mailer from '../mailer';
|
||||
|
||||
import JWT from 'jsonwebtoken';
|
||||
|
||||
@@ -99,5 +100,6 @@ const hashPassword = function hashPassword(model) {
|
||||
User.beforeCreate(hashPassword);
|
||||
User.beforeUpdate(hashPassword);
|
||||
User.beforeCreate(setRandomJwtSecret);
|
||||
User.afterCreate(user => mailer.welcome(user.email));
|
||||
|
||||
export default User;
|
||||
|
||||
Reference in New Issue
Block a user