Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use nodemailer for sending email (with local email dev solution) #433

Merged
merged 26 commits into from
Aug 22, 2020
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f3136fe
Updated database schema to bugfix password reset process. [Fixes #273]
patcon May 21, 2020
1226022
Added maildev docker container for inspecting emails during dev.
patcon May 21, 2020
e5af9a4
Ensured the proxied services are seeing the origin host.
patcon May 21, 2020
4d9ac30
Small fixup from rebase.
patcon Jun 6, 2020
be4d49f
Added SMTP port exposure to maildev container.
patcon Jul 18, 2020
d172115
Migrated AWS_REGION config into envvar.
patcon Jul 18, 2020
6dce6ce
Added mailgun nodemailer transport. Added fallback through multiple t…
patcon Jul 18, 2020
494c4fb
Added ability for cypress to check maildev inbox on another port.
patcon Jul 19, 2020
529669b
e2e: Fixed create_user test.
patcon Jul 19, 2020
8037607
e2e: Added checks of password reset flow.
patcon Jul 19, 2020
3d05dd0
e2e: Added test stubs for types of emails sent.
patcon Jul 19, 2020
dfc31f8
e2e: Run through whole password reset flow, and confirm new password.
patcon Jul 19, 2020
1f23ba5
e2e: Added plugin to output more details to stdout.
patcon Jul 20, 2020
778cb8f
e2e: Make more clear when reporter prints to terminal.
patcon Jul 20, 2020
5434026
Added log command for troubleshooting GitHub Actions issue.
patcon Jul 20, 2020
e2da854
e2e: Fixed issue with matching password reset token.
patcon Jul 20, 2020
cca71ac
Merge branch 'dev' into feature/231-dev-email
patcon Aug 12, 2020
45017e7
Added testing of email transport failover.
patcon Aug 12, 2020
a131a0b
Adding docs for email transport configuration. [skip ci]
patcon Aug 13, 2020
082f399
Merge branch 'dev' into feature/231-dev-email
patcon Aug 21, 2020
3e06e53
Check maildev via API instead of UI.
patcon Aug 21, 2020
b51ba07
Merge branch 'dev' into feature/231-dev-email
patcon Aug 21, 2020
233b830
Improved documentation of cypress workflow for email transports.
patcon Aug 21, 2020
6b14120
e2e: Added note about cypress-terminal-report in README.
patcon Aug 21, 2020
a363939
Removed straggling TODO.
patcon Aug 21, 2020
0f499eb
Set email transport defaults to match current production.
patcon Aug 22, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/cypress-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ jobs:
echo GOOGLE_CREDENTIALS_BASE64=${{ secrets.GOOGLE_CREDENTIALS_BASE64 }} >> server/docker-dev.env
echo SHOULD_USE_TRANSLATION_API=true >> server/docker-dev.env

- name: Set server configuration
run: |
# Test email transport failovers
echo EMAIL_TRANSPORT_TYPES=mailgun,noop,maildev >> server/docker-dev.env
patcon marked this conversation as resolved.
Show resolved Hide resolved

- name: Serve app via docker-compose
run: docker-compose up --detach

Expand Down
10 changes: 10 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,16 @@ services:
args:
GIT_HASH: "${GIT_HASH}"

maildev:
image: maildev/maildev:1.1.0
networks:
- "polis-dev"
ports:
# User interface
- "1080:80"
# SMTP port
- "25:25"

networks:
polis-dev:

Expand Down
53 changes: 53 additions & 0 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,59 @@ We use Google to automatically translate submitted comments into the language of
[base64-encoder]: https://codepen.io/bsngr/pen/awuDh


## Email Transports

We use [Nodemailer][] to send email. Nodemailer uses various built-in and
packaged _email transports_ to send email via SMTP or API, either directly or
via third-party platforms.

Each transport needs a bit of hardcoded scaffold configuration to make it work,
which we welcome via code contribution. But after this, others can easily use
the same email transport by setting some configuration values via environment
variable or otherwise.

We use `EMAIL_TRANSPORT_TYPES` to set email transports and their fallback
order. Each transport has a keyword (e.g., `maildev`). You may set one or more
transports, separated by commas. If you set more than one, then each transport
will "fallback" to the next on failure.

For example, if you set `aws-ses,mailgun`, then we'll try to send via
`aws-ses`, but on failure, we'll try to send via `mailgun`. If Mailgun fails,
the email will not be sent.

[Nodemailer]: https://nodemailer.com/about/

### Configuring transport: `maildev`

Note: The [MailDev][] email transport is for **development purposes only**. Ensure it's disabled in production!

1. Add `maildev` into the `EMAIL_TRANSPORT_TYPES` configuration.

This transport will work automatically when running via Docker Compose, accessible on port 1080.

[MailDev]: https://github.com/maildev/maildev

### Configuring transport: `aws-ses`

1. Add `aws-ses` into the `EMAIL_TRANSPORT_TYPES` configuration.
2. Set the `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` configuration.

### Configuring transport: `mailgun`

1. Add `mailgun` into the `EMAIL_TRANSPORT_TYPES` configuration.
2. Set the `MAILGUN_API_KEY` and `MAILGUN_DOMAIN` configuration.

### Adding a new transport

1. [Find a transport for the service you require][transports] (or write your
own!)
2. Add any new transport configuration to `getMailOptions(...)` in
[`server/email/senders.js`][mail-senders].
3. Submit a pull request.

[transports]: https://github.com/search?q=nodemailer+transport
[mail-senders]: /server/email/senders.js

## Contribution notes

Please help us out as you go in setting things up by improving the deployment code and documentation!
Expand Down
1 change: 1 addition & 0 deletions e2e/cypress.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"chromeWebSecurity": false,
"apiPath": "/api/v3",
"ignoreTestFiles": "**/examples/*.spec.js",
"baseUrl": "http://localhost"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
describe('Create User', () => {
beforeEach(() => {
// Cypress doesn't believe in cleanup.
// See: https://docs.cypress.io/guides/references/best-practices.html#State-reset-should-go-before-each-test
cy.logout()
})

before(() => {
cy.fixture('users.json').as('users')
})

Expand Down
117 changes: 117 additions & 0 deletions e2e/cypress/integration/polis/emails.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
describe('Emails', () => {
const EMAIL_PORT = '1080'

beforeEach(() => {
cy.visit(`${Cypress.config().baseUrl}:${EMAIL_PORT}`)
cy.contains('Clear Inbox').click()
cy.contains('Now receiving all emails')

cy.fixture('users.json').then((users) => {
cy.wrap(users[0]).as('user')
})
})

it('sends for failed password reset', function () {
const nonExistingEmail = 'nonexistent@polis.test'
cy.visit('/pwresetinit')
cy.get('input[placeholder="email"]').type(nonExistingEmail)
cy.contains('button', 'Send password reset email').click()

cy.visit(`${Cypress.config().baseUrl}:${EMAIL_PORT}/`)
cy.get('a.email-item').first().within(() => {
cy.get('.title').should('contain', 'Password Reset Failed')
cy.get('.subline').should('contain', nonExistingEmail)
})
})

it('sends for successful password reset', function () {
// Create a new user account
const randomInt = Math.floor(Math.random() * 10000)
const newUser = {
email: `user${randomInt}@polis.test`,
name: `Test User ${randomInt}`,
password: 'testpassword',
newPassword: 'newpassword',
}

cy.server()
cy.route({
method: 'POST',
url: Cypress.config().apiPath + '/auth/new'
}).as('authNew')

cy.signup(newUser.name, newUser.email, newUser.password)

cy.wait('@authNew').then((xhr) => {
expect(xhr.status).to.equal(200)
})

cy.logout()

// Request password reset on new account
cy.visit('/pwresetinit')
cy.get('input[placeholder="email"]').type(newUser.email)
cy.contains('button', 'Send password reset email').click()

cy.visit(`${Cypress.config().baseUrl}:${EMAIL_PORT}/`)
cy.get('a.email-item').first().within(() => {
cy.get('.title').should('contain', 'Polis Password Reset')
cy.get('.subline').should('contain', newUser.email)
cy.root().click()
})
// Has password reset link with proper hostname.
cy.get('.email-content').should('contain', `${Cypress.config().baseUrl}/pwreset/`)
cy.get('.email-content').then(($elem) => {
const emailContent = $elem.text()
const tokenRegex = new RegExp('/pwreset/([a-zA-Z0-9]+)\n', 'g')
const match = tokenRegex.exec(emailContent)
// First "url" is email domain. Second url is the one we want.
cy.log(JSON.stringify(match))
const passwordResetToken = match[1]

// Submit password reset form with new password.
cy.visit(`/pwreset/${passwordResetToken}`)

cy.route({
method: 'POST',
url: Cypress.config().apiPath + '/auth/password'
}).as('authPassword')

cy.get('form').within(() => {
cy.get('input[placeholder="new password"]').type(newUser.newPassword)
cy.get('input[placeholder="repeat new password"]').type(newUser.newPassword)
cy.get('button').click()
})

cy.wait('@authPassword').then((xhr) => {
expect(xhr.status).to.equal(200)
})
})

cy.logout()

// Login with new password.
cy.login(newUser.email, newUser.newPassword)

cy.url().should('eq', Cypress.config().baseUrl + '/')
})

// TODO: Re-enabled account verification.
it.skip('sends when new account requires verification', function () {
})

// TODO: Allow batch interval to be skipped or reduced for tests.
it.skip('sends when new statements arrive', function () {
})

// TODO: Fix data export.
it.skip('sends when data export is run', function () {
})

// TODO: Find way to test embedded iframe.
it.skip('sends when new conversation is auto-created', function () {
})

it.skip('sends when new statement available for moderation', function () {
})
})
1 change: 1 addition & 0 deletions e2e/cypress/plugins/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
require('cypress-terminal-report/src/installLogsPrinter')(on)
}
14 changes: 14 additions & 0 deletions e2e/cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,17 @@ Cypress.Commands.add("createConvo", (adminEmail, adminPassword) => {
// Wait for header of convo admin page to be available.
cy.contains('h3', 'Configure')
})

// Allow visiting maildev inbox urls, to test sending of emails.
// See: https://github.com/cypress-io/cypress/issues/944#issuecomment-651503805
Cypress.Commands.overwrite(
'visit',
(originalFn, url, options) => {
if (url.includes(':1080')) {
cy.window().then(win => {
return win.open(url, '_self');
});
}
else { return originalFn(url, options); }
}
);
8 changes: 8 additions & 0 deletions e2e/cypress/support/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,11 @@ before(() => {
}
})
})

// Register the log collector for logging activity to terminal.
const reporterOptions = {
// When to print terminal logs for tests.
// Options: onFail, always
printLogs: 'onFail',
}
require('cypress-terminal-report/src/installLogsCollector')(reporterOptions)
53 changes: 53 additions & 0 deletions e2e/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"author": "Benjamin Rosas <ben@aliencyb.org>",
"devDependencies": {
"cypress": "^4.9.0",
"cypress-terminal-report": "^1.4.1",
patcon marked this conversation as resolved.
Show resolved Hide resolved
"eslint": "^7.1.0",
"eslint-config-prettier": "^6.11.0",
"eslint-config-prettier-standard": "^3.0.1",
Expand Down
1 change: 1 addition & 0 deletions file-server/nginx.site.default.conf
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ server {

location / {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
proxy_pass http://server:5000;
}
}
8 changes: 8 additions & 0 deletions server/docker-dev.env
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,11 @@ SHOULD_USE_TRANSLATION_API=false
STATIC_FILES_ADMINDASH_PORT=8080
STATIC_FILES_HOST=file-server
STATIC_FILES_PORT=8080

AWS_REGION=us-east-1

# Options: maildev, aws-ses, mailgun
# Example: `aws-ses,mailgun` would try sending via AWS SES first, and fallback to Mailgun on error.
# TODO: Write docs on adding new email transports.
patcon marked this conversation as resolved.
Show resolved Hide resolved
EMAIL_TRANSPORT_TYPES=maildev
POLIS_FROM_ADDRESS="Example <team@example.com>"