Added email templating, and user welcome email

This commit is contained in:
Jori Lallo
2017-11-12 15:02:23 -08:00
parent 272cc158ea
commit 348e5f0b20
17 changed files with 463 additions and 14 deletions

View 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\\">&nbsp;</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\\">&nbsp;</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\\">&nbsp;</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",
}
`;

View File

@@ -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 () => {

View 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>
);
};

View 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>
);
};

View 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} />;
};

View 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;
}
`;

View 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: '&nbsp;' }}
/>
</TR>
</TBody>
</Table>
);
};
export default EmptySpace;

View 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
View 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;

View File

@@ -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
View 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
View 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();
});
});

View File

@@ -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;