Added email templating, and user welcome email
This commit is contained in:
@ -13,3 +13,9 @@ URL=http://localhost:3000
|
|||||||
DEPLOYMENT=hosted
|
DEPLOYMENT=hosted
|
||||||
ENABLE_UPDATES=true
|
ENABLE_UPDATES=true
|
||||||
GOOGLE_ANALYTICS_ID=
|
GOOGLE_ANALYTICS_ID=
|
||||||
|
|
||||||
|
SMTP_HOST=
|
||||||
|
SMTP_PORT=
|
||||||
|
SMTP_USERNAME=
|
||||||
|
SMTP_PASSWORD=
|
||||||
|
SMTP_SENDER_EMAIL=
|
39
package.json
39
package.json
@ -4,10 +4,8 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf dist",
|
"clean": "rimraf dist",
|
||||||
"build:webpack":
|
"build:webpack": "NODE_ENV=production webpack --config webpack.config.prod.js",
|
||||||
"NODE_ENV=production webpack --config webpack.config.prod.js",
|
"build:analyze": "NODE_ENV=production webpack --config webpack.config.prod.js --json | webpack-bundle-size-analyzer",
|
||||||
"build:analyze":
|
|
||||||
"NODE_ENV=production webpack --config webpack.config.prod.js --json | webpack-bundle-size-analyzer",
|
|
||||||
"build": "npm run clean && npm run build:webpack",
|
"build": "npm run clean && npm run build:webpack",
|
||||||
"start": "NODE_ENV=production node index.js",
|
"start": "NODE_ENV=production node index.js",
|
||||||
"dev": "NODE_ENV=development nodemon --inspect --watch server index.js",
|
"dev": "NODE_ENV=development nodemon --inspect --watch server index.js",
|
||||||
@ -20,24 +18,39 @@
|
|||||||
"sequelize:migrate": "sequelize db:migrate",
|
"sequelize:migrate": "sequelize db:migrate",
|
||||||
"test": "npm run test:app && npm run test:server",
|
"test": "npm run test:app && npm run test:server",
|
||||||
"test:app": "jest",
|
"test:app": "jest",
|
||||||
"test:server":
|
"test:server": "jest --config=server/.jestconfig.json --runInBand --forceExit",
|
||||||
"jest --config=server/.jestconfig.json --runInBand --forceExit",
|
|
||||||
"precommit": "lint-staged"
|
"precommit": "lint-staged"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.js": ["eslint --fix", "git add"]
|
"*.js": [
|
||||||
|
"eslint --fix",
|
||||||
|
"git add"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"verbose": false,
|
"verbose": false,
|
||||||
"roots": ["app"],
|
"roots": [
|
||||||
|
"app"
|
||||||
|
],
|
||||||
"moduleNameMapper": {
|
"moduleNameMapper": {
|
||||||
"^.*[.](s?css|css)$": "<rootDir>/__mocks__/styleMock.js",
|
"^.*[.](s?css|css)$": "<rootDir>/__mocks__/styleMock.js",
|
||||||
"^.*[.](gif|ttf|eot|svg)$": "<rootDir>/__test__/fileMock.js"
|
"^.*[.](gif|ttf|eot|svg)$": "<rootDir>/__test__/fileMock.js"
|
||||||
},
|
},
|
||||||
"moduleFileExtensions": ["js", "jsx", "json"],
|
"moduleFileExtensions": [
|
||||||
"moduleDirectories": ["node_modules"],
|
"js",
|
||||||
"modulePaths": ["app"],
|
"jsx",
|
||||||
"setupFiles": ["<rootDir>/setupJest.js", "<rootDir>/__mocks__/window.js"]
|
"json"
|
||||||
|
],
|
||||||
|
"moduleDirectories": [
|
||||||
|
"node_modules"
|
||||||
|
],
|
||||||
|
"modulePaths": [
|
||||||
|
"app"
|
||||||
|
],
|
||||||
|
"setupFiles": [
|
||||||
|
"<rootDir>/setupJest.js",
|
||||||
|
"<rootDir>/__mocks__/window.js"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 7.6"
|
"node": ">= 7.6"
|
||||||
@ -117,8 +130,10 @@
|
|||||||
"mobx-react-devtools": "^4.2.11",
|
"mobx-react-devtools": "^4.2.11",
|
||||||
"moment": "2.13.0",
|
"moment": "2.13.0",
|
||||||
"node-dev": "3.1.0",
|
"node-dev": "3.1.0",
|
||||||
|
"nodemailer": "^4.4.0",
|
||||||
"normalize.css": "^7.0.0",
|
"normalize.css": "^7.0.0",
|
||||||
"normalizr": "2.0.1",
|
"normalizr": "2.0.1",
|
||||||
|
"oy-vey": "^0.10.0",
|
||||||
"pg": "^6.1.5",
|
"pg": "^6.1.5",
|
||||||
"pg-hstore": "2.3.2",
|
"pg-hstore": "2.3.2",
|
||||||
"polished": "1.2.1",
|
"polished": "1.2.1",
|
||||||
|
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 () => {
|
describe.skip('#auth.signup', async () => {
|
||||||
it('should signup a new user', 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', {
|
const res = await server.post('/api/auth.signup', {
|
||||||
body: {
|
body: {
|
||||||
username: 'testuser',
|
username: 'testuser',
|
||||||
@ -23,6 +29,7 @@ describe.skip('#auth.signup', async () => {
|
|||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
expect(body.ok).toBe(true);
|
expect(body.ok).toBe(true);
|
||||||
expect(body.data.user).toBeTruthy();
|
expect(body.data.user).toBeTruthy();
|
||||||
|
expect(welcomeEmailMock).toBeCalledWith('new.user@example.com');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require params', async () => {
|
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 updates from './utils/updates';
|
||||||
|
|
||||||
import api from './api';
|
import api from './api';
|
||||||
|
import emails from './emails';
|
||||||
import routes from './routes';
|
import routes from './routes';
|
||||||
|
|
||||||
const app = new Koa();
|
const app = new Koa();
|
||||||
@ -71,6 +72,10 @@ if (process.env.NODE_ENV === 'production' && process.env.BUGSNAG_KEY) {
|
|||||||
app.on('error', bugsnag.koaHandler);
|
app.on('error', bugsnag.koaHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
app.use(mount('/emails', emails));
|
||||||
|
}
|
||||||
|
|
||||||
app.use(mount('/api', api));
|
app.use(mount('/api', api));
|
||||||
app.use(mount(routes));
|
app.use(mount(routes));
|
||||||
|
|
||||||
@ -85,7 +90,7 @@ app.use(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Production updates and anonymous analytics.
|
* Production updates and anonymous analytics.
|
||||||
*
|
*
|
||||||
* Set ENABLE_UPDATES=false to disable them for your installation
|
* Set ENABLE_UPDATES=false to disable them for your installation
|
||||||
*/
|
*/
|
||||||
if (
|
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 uuid from 'uuid';
|
||||||
import { DataTypes, sequelize, encryptedFields } from '../sequelize';
|
import { DataTypes, sequelize, encryptedFields } from '../sequelize';
|
||||||
import { uploadToS3FromUrl } from '../utils/s3';
|
import { uploadToS3FromUrl } from '../utils/s3';
|
||||||
|
import mailer from '../mailer';
|
||||||
|
|
||||||
import JWT from 'jsonwebtoken';
|
import JWT from 'jsonwebtoken';
|
||||||
|
|
||||||
@ -99,5 +100,6 @@ const hashPassword = function hashPassword(model) {
|
|||||||
User.beforeCreate(hashPassword);
|
User.beforeCreate(hashPassword);
|
||||||
User.beforeUpdate(hashPassword);
|
User.beforeUpdate(hashPassword);
|
||||||
User.beforeCreate(setRandomJwtSecret);
|
User.beforeCreate(setRandomJwtSecret);
|
||||||
|
User.afterCreate(user => mailer.welcome(user.email));
|
||||||
|
|
||||||
export default User;
|
export default User;
|
||||||
|
@ -37,6 +37,7 @@ productionWebpackConfig.plugins.push(
|
|||||||
productionWebpackConfig.plugins.push(
|
productionWebpackConfig.plugins.push(
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
'process.env': {
|
'process.env': {
|
||||||
|
URL: JSON.stringify(process.env.URL),
|
||||||
NODE_ENV: JSON.stringify('production'),
|
NODE_ENV: JSON.stringify('production'),
|
||||||
GOOGLE_ANALYTICS_ID: JSON.stringify(process.env.GOOGLE_ANALYTICS_ID),
|
GOOGLE_ANALYTICS_ID: JSON.stringify(process.env.GOOGLE_ANALYTICS_ID),
|
||||||
},
|
},
|
||||||
|
24
yarn.lock
24
yarn.lock
@ -1517,6 +1517,12 @@ clean-css@3.4.x:
|
|||||||
commander "2.8.x"
|
commander "2.8.x"
|
||||||
source-map "0.4.x"
|
source-map "0.4.x"
|
||||||
|
|
||||||
|
clean-css@^4.0.12:
|
||||||
|
version "4.1.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.1.9.tgz#35cee8ae7687a49b98034f70de00c4edd3826301"
|
||||||
|
dependencies:
|
||||||
|
source-map "0.5.x"
|
||||||
|
|
||||||
cli-color@~1.2.0:
|
cli-color@~1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/cli-color/-/cli-color-1.2.0.tgz#3a5ae74fd76b6267af666e69e2afbbd01def34d1"
|
resolved "https://registry.yarnpkg.com/cli-color/-/cli-color-1.2.0.tgz#3a5ae74fd76b6267af666e69e2afbbd01def34d1"
|
||||||
@ -6175,6 +6181,10 @@ node-pre-gyp@0.6.36, node-pre-gyp@^0.6.36:
|
|||||||
tar "^2.2.1"
|
tar "^2.2.1"
|
||||||
tar-pack "^3.4.0"
|
tar-pack "^3.4.0"
|
||||||
|
|
||||||
|
nodemailer@^4.4.0:
|
||||||
|
version "4.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-4.4.0.tgz#571989e524a906fb83b1518f3e4c0d3140db24af"
|
||||||
|
|
||||||
nodemon@1.11.0:
|
nodemon@1.11.0:
|
||||||
version "1.11.0"
|
version "1.11.0"
|
||||||
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.11.0.tgz#226c562bd2a7b13d3d7518b49ad4828a3623d06c"
|
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.11.0.tgz#226c562bd2a7b13d3d7518b49ad4828a3623d06c"
|
||||||
@ -6492,6 +6502,14 @@ osenv@^0.1.0, osenv@^0.1.4:
|
|||||||
version "0.0.5"
|
version "0.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/over/-/over-0.0.5.tgz#f29852e70fd7e25f360e013a8ec44c82aedb5708"
|
resolved "https://registry.yarnpkg.com/over/-/over-0.0.5.tgz#f29852e70fd7e25f360e013a8ec44c82aedb5708"
|
||||||
|
|
||||||
|
oy-vey@^0.10.0:
|
||||||
|
version "0.10.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/oy-vey/-/oy-vey-0.10.0.tgz#16160f837f0ea3d0340adfc2377ba93d1ed9ce76"
|
||||||
|
dependencies:
|
||||||
|
clean-css "^4.0.12"
|
||||||
|
object-assign "^4.1.1"
|
||||||
|
sanitizer "^0.1.3"
|
||||||
|
|
||||||
p-cancelable@^0.3.0:
|
p-cancelable@^0.3.0:
|
||||||
version "0.3.0"
|
version "0.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.3.0.tgz#b9e123800bcebb7ac13a479be195b507b98d30fa"
|
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.3.0.tgz#b9e123800bcebb7ac13a479be195b507b98d30fa"
|
||||||
@ -7827,6 +7845,10 @@ sane@~1.6.0:
|
|||||||
walker "~1.0.5"
|
walker "~1.0.5"
|
||||||
watch "~0.10.0"
|
watch "~0.10.0"
|
||||||
|
|
||||||
|
sanitizer@^0.1.3:
|
||||||
|
version "0.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/sanitizer/-/sanitizer-0.1.3.tgz#d4f0af7475d9a7baf2a9e5a611718baa178a39e1"
|
||||||
|
|
||||||
sax@1.2.1:
|
sax@1.2.1:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
|
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
|
||||||
@ -8177,7 +8199,7 @@ source-map@0.5.6:
|
|||||||
version "0.5.6"
|
version "0.5.6"
|
||||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
|
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
|
||||||
|
|
||||||
source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.0, source-map@~0.5.1, source-map@~0.5.3:
|
source-map@0.5.x, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.0, source-map@~0.5.1, source-map@~0.5.3:
|
||||||
version "0.5.7"
|
version "0.5.7"
|
||||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
|
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user