From f3136fed40982d6eabcdf1888f26b3e234f78a32 Mon Sep 17 00:00:00 2001 From: Patrick Connolly Date: Thu, 21 May 2020 02:23:05 -0300 Subject: [PATCH 01/23] Updated database schema to bugfix password reset process. [Fixes #273] --- server/postgres/db_setup_draft.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/postgres/db_setup_draft.sql b/server/postgres/db_setup_draft.sql index 6970b3122..960e5533b 100644 --- a/server/postgres/db_setup_draft.sql +++ b/server/postgres/db_setup_draft.sql @@ -335,11 +335,11 @@ CREATE TABLE zinvites ( CREATE INDEX zinvites_zid_idx ON zinvites USING btree (zid); -- TODO flush regularly -CREATE TABLE password_reset_tokens ( +CREATE TABLE pwreset_tokens ( uid INTEGER REFERENCES users(uid), created BIGINT DEFAULT now_as_millis(), - pwresettoken VARCHAR(250), - UNIQUE (pwresettoken) + token VARCHAR(250), + UNIQUE (token) ); CREATE TABLE beta( From 1226022afbae5f0ee891a6a3ad8a9cb9d0368644 Mon Sep 17 00:00:00 2001 From: Patrick Connolly Date: Thu, 21 May 2020 02:24:30 -0300 Subject: [PATCH 02/23] Added maildev docker container for inspecting emails during dev. --- docker-compose.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 4e47e6bdb..1e01ee724 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -106,7 +106,14 @@ services: build: context: ./client-report args: - GIT_HASH: "${GIT_HASH}" + GIT_HASH: null + + maildev: + image: maildev/maildev:1.1.0 + networks: + - 'polis-dev' + ports: + - 1080:80 networks: polis-dev: From e5af9a40d61f54c973c4fb6f81b66eff69f00edd Mon Sep 17 00:00:00 2001 From: Patrick Connolly Date: Thu, 21 May 2020 14:50:44 -0300 Subject: [PATCH 03/23] Ensured the proxied services are seeing the origin host. --- file-server/nginx.site.default.conf | 1 + server/email/sendEmailSesMailgun.js | 60 +++++++++++------------------ server/package-lock.json | 5 +++ server/package.json | 1 + 4 files changed, 29 insertions(+), 38 deletions(-) diff --git a/file-server/nginx.site.default.conf b/file-server/nginx.site.default.conf index 3b00ef7ec..f8efa9dc7 100644 --- a/file-server/nginx.site.default.conf +++ b/file-server/nginx.site.default.conf @@ -4,6 +4,7 @@ server { location / { proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $host; proxy_pass http://server:5000; } } diff --git a/server/email/sendEmailSesMailgun.js b/server/email/sendEmailSesMailgun.js index fec0fe996..e0209695d 100644 --- a/server/email/sendEmailSesMailgun.js +++ b/server/email/sendEmailSesMailgun.js @@ -1,46 +1,11 @@ // Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . "use strict" +const nodemailer = require('nodemailer'); const Mailgun = require('mailgun').Mailgun; const mailgun = new Mailgun(process.env.MAILGUN_API_KEY); function EmailSenders(AWS) { - const sesClient = new AWS.SES({apiVersion: '2010-12-01'}); // reads AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY from process.env - - function sendTextEmailWithSes(sender, recipient, subject, text) { - console.log("sending email with SES: " + [sender, recipient, subject, text].join(" ")); - - return new Promise(function(resolve, reject) { - sesClient.sendEmail({ - Destination: { - ToAddresses: [recipient], - }, - Source: sender, - Message: { - Subject: { - Data: subject, - }, - Body: { - Text: { - Data: text, - }, - }, - }, - - }, function(err, data) { - if (err) { - console.error("polis_err_ses_email_send_failed"); - console.error("Unable to send email via ses to " + recipient); - console.error(err); - reject(err); - } else { - console.log("sent email with ses to " + recipient); - resolve(); - } - }); - }); - } - function sendTextEmailWithBackup(sender, recipient, subject, text) { console.log("sending email with mailgun: " + [sender, recipient, subject, text].join(" ")); @@ -61,12 +26,31 @@ function EmailSenders(AWS) { } function sendTextEmail(sender, recipient, subject, text) { - let promise = sendTextEmailWithSes(sender, recipient, subject, text).catch(function(err) { + let mailOptions; + + if (process.env.DEV_MODE) { + mailOptions = { + host: 'maildev', + port: 25, + ignoreTLS: true, + }; + } else { + mailOptions = { + // reads AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY from process.env + SES: new AWS.SES({ apiVersion: '2010-12-01' }) + }; + } + + const transporter = nodemailer.createTransport(mailOptions); + + let promise = transporter.sendMail({from: sender, to: recipient, subject: subject, text: text}).catch(function(err) { console.error("polis_err_primary_email_sender_failed"); console.error(err); return sendTextEmailWithBackup(sender, recipient, subject, text); }); promise.catch(function(err) { + console.error("polis_err_ses_email_send_failed"); + console.error("Unable to send email via ses to " + recipient); console.error(err); }); return promise; @@ -81,4 +65,4 @@ function EmailSenders(AWS) { module.exports = { EmailSenders: EmailSenders, -}; \ No newline at end of file +}; diff --git a/server/package-lock.json b/server/package-lock.json index 5b9c76782..1248664c2 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -3500,6 +3500,11 @@ "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, + "nodemailer": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.4.6.tgz", + "integrity": "sha512-/kJ+FYVEm2HuUlw87hjSqTss+GU35D4giOpdSfGp7DO+5h6RlJj7R94YaYHOkoxu1CSaM0d3WRBtCzwXrY6MKA==" + }, "oauth": { "version": "0.9.14", "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.14.tgz", diff --git a/server/package.json b/server/package.json index 6c3142d81..77e34095b 100644 --- a/server/package.json +++ b/server/package.json @@ -37,6 +37,7 @@ "lru-cache": "3.0.0", "mailgun": "0.4.3", "mimelib": "0.2.19", + "nodemailer": "^6.4.6", "oauth": "0.9.14", "optimist": "0.3.7", "p3p": "0.0.2", From 4d9ac302615051b34561bdac6f798bb3db27a50e Mon Sep 17 00:00:00 2001 From: Patrick Connolly Date: Fri, 5 Jun 2020 22:12:38 -0300 Subject: [PATCH 04/23] Small fixup from rebase. --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 1e01ee724..aeafb7d25 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -106,7 +106,7 @@ services: build: context: ./client-report args: - GIT_HASH: null + GIT_HASH: "${GIT_HASH}" maildev: image: maildev/maildev:1.1.0 From be4d49f36899c24c78c420a40a4191967c417615 Mon Sep 17 00:00:00 2001 From: Patrick Connolly Date: Sat, 18 Jul 2020 02:07:21 -0300 Subject: [PATCH 05/23] Added SMTP port exposure to maildev container. --- docker-compose.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index aeafb7d25..3e5a1396a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -111,9 +111,12 @@ services: maildev: image: maildev/maildev:1.1.0 networks: - - 'polis-dev' + - "polis-dev" ports: - - 1080:80 + # User interface + - "1080:80" + # SMTP port + - "25:25" networks: polis-dev: From d17211593274f4a6f2d09f38fd366e8a0821e9de Mon Sep 17 00:00:00 2001 From: Patrick Connolly Date: Sat, 18 Jul 2020 02:10:10 -0300 Subject: [PATCH 06/23] Migrated AWS_REGION config into envvar. --- server/docker-dev.env | 2 ++ server/server.js | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/server/docker-dev.env b/server/docker-dev.env index c17b234c2..1e50117f1 100644 --- a/server/docker-dev.env +++ b/server/docker-dev.env @@ -23,3 +23,5 @@ SHOULD_USE_TRANSLATION_API=false STATIC_FILES_ADMINDASH_PORT=8080 STATIC_FILES_HOST=file-server STATIC_FILES_PORT=8080 + +AWS_REGION=us-east-1 diff --git a/server/server.js b/server/server.js index ac941b417..f5703f2f8 100644 --- a/server/server.js +++ b/server/server.js @@ -4,7 +4,7 @@ const akismetLib = require('akismet'); const AWS = require('aws-sdk'); -AWS.config.set('region', 'us-east-1'); +AWS.config.set('region', process.env.AWS_REGION); const badwords = require('badwords/object'); const Promise = require('bluebird'); const http = require('http'); From 6dce6ce72e855563ebede0928ee8c7a7713a778e Mon Sep 17 00:00:00 2001 From: Patrick Connolly Date: Sat, 18 Jul 2020 02:13:02 -0300 Subject: [PATCH 07/23] Added mailgun nodemailer transport. Added fallback through multiple transport mechanisms. --- server/docker-dev.env | 6 + server/email/sendEmailSesMailgun.js | 68 ---- server/email/senders.js | 75 ++++ server/package-lock.json | 581 +++++++++++++++++++++++++++- server/package.json | 2 +- server/server.js | 2 +- 6 files changed, 655 insertions(+), 79 deletions(-) delete mode 100644 server/email/sendEmailSesMailgun.js create mode 100644 server/email/senders.js diff --git a/server/docker-dev.env b/server/docker-dev.env index 1e50117f1..6ef30af7c 100644 --- a/server/docker-dev.env +++ b/server/docker-dev.env @@ -25,3 +25,9 @@ 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. +EMAIL_TRANSPORT_TYPES=maildev +POLIS_FROM_ADDRESS="Example " diff --git a/server/email/sendEmailSesMailgun.js b/server/email/sendEmailSesMailgun.js deleted file mode 100644 index e0209695d..000000000 --- a/server/email/sendEmailSesMailgun.js +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . -"use strict" - -const nodemailer = require('nodemailer'); -const Mailgun = require('mailgun').Mailgun; -const mailgun = new Mailgun(process.env.MAILGUN_API_KEY); - -function EmailSenders(AWS) { - - function sendTextEmailWithBackup(sender, recipient, subject, text) { - console.log("sending email with mailgun: " + [sender, recipient, subject, text].join(" ")); - let servername = ""; - let options = {}; - return new Promise(function(resolve, reject) { - mailgun.sendText(sender, [recipient], subject, text, servername, options, function(err) { - if (err) { - console.error("Unable to send email via mailgun to " + recipient + " " + err); - console.error("polis_err_mailgun_email_send_failed"); - reject(err); - } else { - console.log("sent email with mailgun to " + recipient); - resolve(); - } - }); - }); - } - - function sendTextEmail(sender, recipient, subject, text) { - let mailOptions; - - if (process.env.DEV_MODE) { - mailOptions = { - host: 'maildev', - port: 25, - ignoreTLS: true, - }; - } else { - mailOptions = { - // reads AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY from process.env - SES: new AWS.SES({ apiVersion: '2010-12-01' }) - }; - } - - const transporter = nodemailer.createTransport(mailOptions); - - let promise = transporter.sendMail({from: sender, to: recipient, subject: subject, text: text}).catch(function(err) { - console.error("polis_err_primary_email_sender_failed"); - console.error(err); - return sendTextEmailWithBackup(sender, recipient, subject, text); - }); - promise.catch(function(err) { - console.error("polis_err_ses_email_send_failed"); - console.error("Unable to send email via ses to " + recipient); - console.error(err); - }); - return promise; - } - - return { - sendTextEmail: sendTextEmail, - sendTextEmailWithBackupOnly: sendTextEmailWithBackup, - }; -} - - -module.exports = { - EmailSenders: EmailSenders, -}; diff --git a/server/email/senders.js b/server/email/senders.js new file mode 100644 index 000000000..b28c9df14 --- /dev/null +++ b/server/email/senders.js @@ -0,0 +1,75 @@ +// Copyright (C) 2012-present, The Authors. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +"use strict" + +const fs = require('fs'); +const nodemailer = require('nodemailer'); +const AWS = require('aws-sdk'); +AWS.config.set('region', process.env.AWS_REGION); + +function sendTextEmailWithBackup(sender, recipient, subject, text) { + const transportTypes = process.env.EMAIL_TRANSPORT_TYPES.split(',') + if (transportTypes.length < 2) { + new Error('No backup email transport available.'); + } + const backupTransport = transportTypes[1]; + sendTextEmail(sender, recipient, subject, text, backupTransport); +} + +function isDocker() { + // See: https://stackoverflow.com/a/25518345/504018 + return fs.existsSync('/.dockerenv'); +} + +function getMailOptions(transportType) { + switch (transportType) { + case 'maildev': + return { + // Allows running outside docker, connecting to exposed port of maildev container. + host: isDocker() ? 'maildev' : 'localhost', + port: 25, + ignoreTLS: true, + } + case 'mailgun': + const mg = require('nodemailer-mailgun-transport'); + const mailgunAuth = { + auth: { + api_key: process.env.MAILGUN_API_KEY, + domain: process.env.MAILGUN_DOMAIN + } + } + return mg(mailgunAuth) + case 'aws-ses': + return { + // reads AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY from process.env + SES: new AWS.SES({ apiVersion: '2010-12-01' }) + } + default: + return {}; + } +} + +function sendTextEmail(sender, recipient, subject, text, transportTypes = process.env.EMAIL_TRANSPORT_TYPES, priority = 1) { + // Exit if empty string passed. + if (!transportTypes) { return } + + transportTypes = transportTypes.split(','); + // Shift first index and clone to rename. + const thisTransportType = transportTypes.shift(); + const nextTransportTypes = [...transportTypes]; + const mailOptions = getMailOptions(thisTransportType); + const transporter = nodemailer.createTransport(mailOptions); + + let promise = transporter.sendMail({from: sender, to: recipient, subject: subject, text: text}) + .catch(function(err) { + console.error("polis_err_email_sender_failed_transport_priority_" + priority.toString()); + console.error(`Unable to send email via priority ${priority.toString()} transport '${thisTransportType}' to: ${recipient}`); + console.error(err); + return sendTextEmail(sender, recipient, subject, text, nextTransportTypes.join(','), priority + 1); + }); + return promise; +} + +module.exports = { + sendTextEmail: sendTextEmail, + sendTextEmailWithBackupOnly: sendTextEmailWithBackup, +}; diff --git a/server/package-lock.json b/server/package-lock.json index 1248664c2..7852e7cb2 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -298,6 +298,11 @@ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, + "ast-types": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.3.tgz", + "integrity": "sha512-XTZ7xGML849LkQP86sWdQzfhwbt3YwIO6MqbX9mUNYY98VKaaVZP7YNNm70IpwecbkkxmfC5IYAzOQ/2p29zRA==" + }, "async": { "version": "0.1.22", "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", @@ -635,6 +640,11 @@ "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-1.0.1.tgz", "integrity": "sha1-Iqk2kB4wKa/NdUfrRIfOtpejvwg=" }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, "capture-stack-trace": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz", @@ -739,6 +749,14 @@ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, + "consolidate": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.15.1.tgz", + "integrity": "sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==", + "requires": { + "bluebird": "^3.1.1" + } + }, "core-util-is": { "version": "1.0.2", "resolved": false, @@ -783,6 +801,11 @@ "assert-plus": "^1.0.0" } }, + "data-uri-to-buffer": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz", + "integrity": "sha512-vKQ9DTQPN1FLYiiEEOQ6IBGFqvjCa5rSK3cWMy/Nespm5d/x3dGFT9UBZnkLxCwua/IXBi2TYnwTEpsOvhC4UQ==" + }, "debug": { "version": "2.6.9", "resolved": false, @@ -801,6 +824,21 @@ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" + }, + "degenerator": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-1.0.4.tgz", + "integrity": "sha1-/PSQo37OJmRk2cxDGrmMWBnO0JU=", + "requires": { + "ast-types": "0.x.x", + "escodegen": "1.x.x", + "esprima": "3.x.x" + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -811,6 +849,11 @@ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, "detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", @@ -868,11 +911,43 @@ "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=" }, + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "requires": { + "es6-promise": "^4.0.3" + } + }, "escape-string-regexp": { "version": "1.0.5", "resolved": false, "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + } + } + }, "eslint": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/eslint/-/eslint-2.13.1.tgz", @@ -1834,6 +1909,21 @@ } } }, + "esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, "eventemitter3": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz", @@ -2593,6 +2683,11 @@ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" + }, "fb": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/fb/-/fb-1.0.2.tgz", @@ -2650,6 +2745,38 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, + "ftp": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz", + "integrity": "sha1-kZfYYa2BQvPmPVqDv+TFn3MwiF0=", + "requires": { + "readable-stream": "1.1.x", + "xregexp": "2.0.0" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, "gauge": { "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", @@ -2687,6 +2814,26 @@ "is-property": "^1.0.0" } }, + "get-uri": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-2.0.4.tgz", + "integrity": "sha512-v7LT/s8kVjs+Tx0ykk1I+H/rbpzkHvuIq87LmeXptcf5sNWm9uQiwjNAt94SJPA1zOlCntmnOlJvVWKmzsxG8Q==", + "requires": { + "data-uri-to-buffer": "1", + "debug": "2", + "extend": "~3.0.2", + "file-uri-to-path": "1", + "ftp": "~0.3.10", + "readable-stream": "2" + }, + "dependencies": { + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + } + } + }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -2817,6 +2964,35 @@ "resolved": "https://registry.npmjs.org/htmlencode/-/htmlencode-0.0.4.tgz", "integrity": "sha1-9+LWr74YqHp45jujMI51N2Z0Dj8=" }, + "http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + } + } + }, "http-proxy": { "version": "0.10.4", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-0.10.4.tgz", @@ -2987,6 +3163,33 @@ } } }, + "http-proxy-agent": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", + "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", + "requires": { + "agent-base": "4", + "debug": "3.1.0" + }, + "dependencies": { + "agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "requires": { + "es6-promisify": "^5.0.0" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -3028,6 +3231,11 @@ "minimatch": "^3.0.4" } }, + "inflection": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz", + "integrity": "sha1-ogCTVlbW9fa8TcdQLhrstwMihBY=" + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -3070,6 +3278,11 @@ "request": "^2.74.0" } }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" + }, "is": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/is/-/is-3.2.1.tgz", @@ -3112,6 +3325,11 @@ "resolved": false, "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=" }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, "is-stream-ended": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", @@ -3200,6 +3418,15 @@ "safe-buffer": "^5.0.1" } }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, "libpq": { "version": "1.8.9", "resolved": "https://registry.npmjs.org/libpq/-/libpq-1.8.9.tgz", @@ -3256,10 +3483,59 @@ } } }, - "mailgun": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/mailgun/-/mailgun-0.4.3.tgz", - "integrity": "sha1-+Xlv3TCnEWhUQevr0A+SAxtp+Ak=" + "mailgun-js": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/mailgun-js/-/mailgun-js-0.22.0.tgz", + "integrity": "sha512-a2alg5nuTZA9Psa1pSEIEsbxr1Zrmqx4VkgGCQ30xVh0kIH7Bu57AYILo+0v8QLSdXtCyLaS+KVmdCrQo0uWFA==", + "requires": { + "async": "^2.6.1", + "debug": "^4.1.0", + "form-data": "^2.3.3", + "inflection": "~1.12.0", + "is-stream": "^1.1.0", + "path-proxy": "~1.0.0", + "promisify-call": "^2.0.2", + "proxy-agent": "^3.0.3", + "tsscmp": "^1.0.6" + }, + "dependencies": { + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "requires": { + "lodash": "^4.17.14" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "lodash": { + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } }, "methmeth": { "version": "1.1.0", @@ -3406,6 +3682,11 @@ } } }, + "netmask": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-1.0.6.tgz", + "integrity": "sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU=" + }, "next-tick": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", @@ -3452,6 +3733,20 @@ } } }, + "nodemailer": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.4.6.tgz", + "integrity": "sha512-/kJ+FYVEm2HuUlw87hjSqTss+GU35D4giOpdSfGp7DO+5h6RlJj7R94YaYHOkoxu1CSaM0d3WRBtCzwXrY6MKA==" + }, + "nodemailer-mailgun-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nodemailer-mailgun-transport/-/nodemailer-mailgun-transport-2.0.0.tgz", + "integrity": "sha512-TPGi2anyS0w/4jv7TJNS3wX5DbNQ2+j+ssnwY4IYxL4QaYXAXewcw6YUtBgnOsEvQVJvmxQLDuW4f4JaMPgfaA==", + "requires": { + "consolidate": "^0.15.1", + "mailgun-js": "^0.22.0" + } + }, "nopt": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", @@ -3500,11 +3795,6 @@ "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, - "nodemailer": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.4.6.tgz", - "integrity": "sha512-/kJ+FYVEm2HuUlw87hjSqTss+GU35D4giOpdSfGp7DO+5h6RlJj7R94YaYHOkoxu1CSaM0d3WRBtCzwXrY6MKA==" - }, "oauth": { "version": "0.9.14", "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.14.tgz", @@ -3556,6 +3846,19 @@ } } }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, "options": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz", @@ -3585,11 +3888,95 @@ "resolved": "https://registry.npmjs.org/p3p/-/p3p-0.0.2.tgz", "integrity": "sha1-apQSgKau6l7xc7OrlZOyApmhhxA=" }, + "pac-proxy-agent": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-3.0.1.tgz", + "integrity": "sha512-44DUg21G/liUZ48dJpUSjZnFfZro/0K5JTyFYLBcmh9+T6Ooi4/i4efwUiEy0+4oQusCBqWdhv16XohIj1GqnQ==", + "requires": { + "agent-base": "^4.2.0", + "debug": "^4.1.1", + "get-uri": "^2.0.0", + "http-proxy-agent": "^2.1.0", + "https-proxy-agent": "^3.0.0", + "pac-resolver": "^3.0.0", + "raw-body": "^2.2.0", + "socks-proxy-agent": "^4.0.1" + }, + "dependencies": { + "agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "requires": { + "es6-promisify": "^5.0.0" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "https-proxy-agent": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz", + "integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==", + "requires": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "pac-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-3.0.0.tgz", + "integrity": "sha512-tcc38bsjuE3XZ5+4vP96OfhOugrX+JcnpUbhfuc4LuXBLQhoTthOstZeoQJBDnQUDYzYmdImKsbz0xSl1/9qeA==", + "requires": { + "co": "^4.6.0", + "degenerator": "^1.0.4", + "ip": "^1.1.5", + "netmask": "^1.0.6", + "thunkify": "^2.1.2" + } + }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, + "path-proxy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/path-proxy/-/path-proxy-1.0.0.tgz", + "integrity": "sha1-GOijaFn8nS8aU7SN7hOFQ8Ag3l4=", + "requires": { + "inflection": "~1.3.0" + }, + "dependencies": { + "inflection": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.3.8.tgz", + "integrity": "sha1-y9Fg2p91sUw8xjV41POWeEvzAU4=" + } + } + }, "path-to-regexp": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", @@ -3781,16 +4168,99 @@ "xtend": "^4.0.0" } }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" + }, "process-nextick-args": { "version": "1.0.7", "resolved": false, "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" }, + "promisify-call": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/promisify-call/-/promisify-call-2.0.4.tgz", + "integrity": "sha1-1IwtRWUszM1SgB3ey9UzptS9X7o=", + "requires": { + "with-callback": "^1.0.2" + } + }, "propprop": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/propprop/-/propprop-0.3.1.tgz", "integrity": "sha1-oEmjVouJZEAGfRXY7J8zc15XAXg=" }, + "proxy-agent": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-3.1.1.tgz", + "integrity": "sha512-WudaR0eTsDx33O3EJE16PjBRZWcX8GqCEeERw1W3hZJgH/F2a46g7jty6UGty6NeJ4CKQy8ds2CJPMiyeqaTvw==", + "requires": { + "agent-base": "^4.2.0", + "debug": "4", + "http-proxy-agent": "^2.1.0", + "https-proxy-agent": "^3.0.0", + "lru-cache": "^5.1.1", + "pac-proxy-agent": "^3.0.1", + "proxy-from-env": "^1.0.0", + "socks-proxy-agent": "^4.0.1" + }, + "dependencies": { + "agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "requires": { + "es6-promisify": "^5.0.0" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "https-proxy-agent": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz", + "integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==", + "requires": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "requires": { + "yallist": "^3.0.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", @@ -3816,6 +4286,17 @@ "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" }, + "raw-body": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz", + "integrity": "sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.3", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, "rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -4133,6 +4614,11 @@ } } }, + "smart-buffer": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.1.0.tgz", + "integrity": "sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw==" + }, "sntp": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", @@ -4141,6 +4627,40 @@ "hoek": "4.x.x" } }, + "socks": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.3.3.tgz", + "integrity": "sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA==", + "requires": { + "ip": "1.1.5", + "smart-buffer": "^4.1.0" + } + }, + "socks-proxy-agent": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz", + "integrity": "sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg==", + "requires": { + "agent-base": "~4.2.1", + "socks": "~2.3.2" + }, + "dependencies": { + "agent-base": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", + "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", + "requires": { + "es6-promisify": "^5.0.0" + } + } + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true + }, "split": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", @@ -4333,6 +4853,16 @@ "xtend": "~4.0.1" } }, + "thunkify": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/thunkify/-/thunkify-2.1.2.tgz", + "integrity": "sha1-+qDp0jDFGsyVyhOjYawFyn4EVT0=" + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, "tough-cookie": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", @@ -4341,6 +4871,11 @@ "punycode": "^1.4.1" } }, + "tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==" + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -4355,6 +4890,14 @@ "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", "optional": true }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "requires": { + "prelude-ls": "~1.1.2" + } + }, "type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -4375,6 +4918,11 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, "url": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", @@ -4480,6 +5028,16 @@ } } }, + "with-callback": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/with-callback/-/with-callback-1.0.2.tgz", + "integrity": "sha1-oJYpuakgAo1yFAT7Q1vc/1yRvCE=" + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" + }, "wrappy": { "version": "1.0.2", "resolved": false, @@ -4511,6 +5069,11 @@ "lodash": "^4.0.0" } }, + "xregexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", + "integrity": "sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM=" + }, "xtend": { "version": "4.0.1", "resolved": false, diff --git a/server/package.json b/server/package.json index 77e34095b..0118fefaf 100644 --- a/server/package.json +++ b/server/package.json @@ -35,9 +35,9 @@ "intercom-client": "2.9.4", "intercom.io": "1.5.0", "lru-cache": "3.0.0", - "mailgun": "0.4.3", "mimelib": "0.2.19", "nodemailer": "^6.4.6", + "nodemailer-mailgun-transport": "^2.0.0", "oauth": "0.9.14", "optimist": "0.3.7", "p3p": "0.0.2", diff --git a/server/server.js b/server/server.js index f5703f2f8..376d5dba1 100644 --- a/server/server.js +++ b/server/server.js @@ -213,7 +213,7 @@ var web = new WebClient(process.env.SLACK_API_TOKEN); // # notifications const winston = console; -const emailSenders = require('./email/sendEmailSesMailgun').EmailSenders(AWS); +const emailSenders = require('./email/senders'); const sendTextEmail = emailSenders.sendTextEmail; const sendTextEmailWithBackupOnly = emailSenders.sendTextEmailWithBackupOnly; From 494c4fbecfaab1a933dd6eeb71c322f2373a3eb7 Mon Sep 17 00:00:00 2001 From: Patrick Connolly Date: Sun, 19 Jul 2020 01:23:59 -0300 Subject: [PATCH 08/23] Added ability for cypress to check maildev inbox on another port. --- e2e/cypress.json | 1 + e2e/cypress/support/commands.js | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/e2e/cypress.json b/e2e/cypress.json index fcae5acdb..83259cb70 100644 --- a/e2e/cypress.json +++ b/e2e/cypress.json @@ -1,4 +1,5 @@ { + "chromeWebSecurity": false, "apiPath": "/api/v3", "baseUrl": "https://preprod.pol.is" } \ No newline at end of file diff --git a/e2e/cypress/support/commands.js b/e2e/cypress/support/commands.js index 2f34eec98..25dae7213 100644 --- a/e2e/cypress/support/commands.js +++ b/e2e/cypress/support/commands.js @@ -75,3 +75,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); } + } +); From 529669bdcacfade04685ae3cabc37dc5001e3daf Mon Sep 17 00:00:00 2001 From: Patrick Connolly Date: Sun, 19 Jul 2020 01:24:37 -0300 Subject: [PATCH 09/23] e2e: Fixed create_user test. --- .../integration/polis/client-admin/create_user.spec.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/e2e/cypress/integration/polis/client-admin/create_user.spec.js b/e2e/cypress/integration/polis/client-admin/create_user.spec.js index 424d0ffb1..0d1d6f307 100644 --- a/e2e/cypress/integration/polis/client-admin/create_user.spec.js +++ b/e2e/cypress/integration/polis/client-admin/create_user.spec.js @@ -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') }) From 8037607fa0a31c5cd3fce08ee0514d280680cad0 Mon Sep 17 00:00:00 2001 From: Patrick Connolly Date: Sun, 19 Jul 2020 01:25:54 -0300 Subject: [PATCH 10/23] e2e: Added checks of password reset flow. --- e2e/cypress/integration/polis/emails.spec.js | 42 ++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 e2e/cypress/integration/polis/emails.spec.js diff --git a/e2e/cypress/integration/polis/emails.spec.js b/e2e/cypress/integration/polis/emails.spec.js new file mode 100644 index 000000000..5f6181790 --- /dev/null +++ b/e2e/cypress/integration/polis/emails.spec.js @@ -0,0 +1,42 @@ +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 () { + const existingEmail = this.user.email + cy.visit('/pwresetinit') + cy.get('input[placeholder="email"]').type(existingEmail) + 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', existingEmail) + cy.root().click() + }) + // Has password reset link with proper hostname. + cy.get('.email-content').should('contain', `${Cypress.config().baseUrl}/pwreset/`) + }) +}) From 3d05dd0986457d24e5424b20177144a3f0718467 Mon Sep 17 00:00:00 2001 From: Patrick Connolly Date: Sun, 19 Jul 2020 01:32:46 -0300 Subject: [PATCH 11/23] e2e: Added test stubs for types of emails sent. --- e2e/cypress/integration/polis/emails.spec.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/e2e/cypress/integration/polis/emails.spec.js b/e2e/cypress/integration/polis/emails.spec.js index 5f6181790..640fc7bcf 100644 --- a/e2e/cypress/integration/polis/emails.spec.js +++ b/e2e/cypress/integration/polis/emails.spec.js @@ -39,4 +39,23 @@ describe('Emails', () => { // Has password reset link with proper hostname. cy.get('.email-content').should('contain', `${Cypress.config().baseUrl}/pwreset/`) }) + + // 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 () { + }) }) From dfc31f84f902308f474f92d95527154828a75a2d Mon Sep 17 00:00:00 2001 From: Patrick Connolly Date: Sun, 19 Jul 2020 02:42:08 -0300 Subject: [PATCH 12/23] e2e: Run through whole password reset flow, and confirm new password. --- e2e/cypress/integration/polis/emails.spec.js | 63 +++++++++++++++++++- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/e2e/cypress/integration/polis/emails.spec.js b/e2e/cypress/integration/polis/emails.spec.js index 640fc7bcf..ae6682583 100644 --- a/e2e/cypress/integration/polis/emails.spec.js +++ b/e2e/cypress/integration/polis/emails.spec.js @@ -25,19 +25,76 @@ describe('Emails', () => { }) it('sends for successful password reset', function () { - const existingEmail = this.user.email + // 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(existingEmail) + 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', existingEmail) + 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() + // Had to remove one single-quote from regex so as not to confuse IDE. + // See: https://www.regextester.com/94502 + const urlRegex = new RegExp(/(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&\(\)\*\+,;=.]+/g) + const match = emailContent.match(urlRegex) + // First "url" is email domain. Second url is the one we want. + const passwordResetUrl = match[1] + + // Submit password reset form with new password. + cy.visit(passwordResetUrl) + + 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. From 1f23ba5df9be3172bd026d787774db7687c02e8a Mon Sep 17 00:00:00 2001 From: Patrick Connolly Date: Sun, 19 Jul 2020 22:58:25 -0300 Subject: [PATCH 13/23] e2e: Added plugin to output more details to stdout. --- e2e/cypress/plugins/index.js | 1 + e2e/cypress/support/index.js | 3 ++ e2e/package-lock.json | 53 ++++++++++++++++++++++++++++++++++++ e2e/package.json | 1 + 4 files changed, 58 insertions(+) diff --git a/e2e/cypress/plugins/index.js b/e2e/cypress/plugins/index.js index aa9918d21..6cfeb167c 100644 --- a/e2e/cypress/plugins/index.js +++ b/e2e/cypress/plugins/index.js @@ -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) } diff --git a/e2e/cypress/support/index.js b/e2e/cypress/support/index.js index 52b362bbc..7bb8cb979 100644 --- a/e2e/cypress/support/index.js +++ b/e2e/cypress/support/index.js @@ -27,3 +27,6 @@ before(() => { cy.signup(user.name, user.email, user.password) }) }) + +// Register the log collector for logging activity to terminal. +require('cypress-terminal-report/src/installLogsCollector')() diff --git a/e2e/package-lock.json b/e2e/package-lock.json index cf53ae069..497f28e58 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -573,6 +573,53 @@ "yauzl": "2.10.0" } }, + "cypress-terminal-report": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/cypress-terminal-report/-/cypress-terminal-report-1.4.1.tgz", + "integrity": "sha512-Oyx7VQCSh4Nh1q0ROo9Wq6KrxxbP8adhUQRxVpNlXeyyfLj2X8AXxqbIh6Ct3V8oENKh5iHHhuOfRfQ1tmE4Tw==", + "dev": true, + "requires": { + "chalk": "^3.0.0", + "methods": "^1.1.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + } + } + }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -2131,6 +2178,12 @@ } } }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, "mime-db": { "version": "1.44.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", diff --git a/e2e/package.json b/e2e/package.json index 31aa6ae84..0f028b9c6 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -12,6 +12,7 @@ "author": "Benjamin Rosas ", "devDependencies": { "cypress": "^4.7.0", + "cypress-terminal-report": "^1.4.1", "eslint": "^7.1.0", "eslint-config-prettier": "^6.11.0", "eslint-config-prettier-standard": "^3.0.1", From 778cb8fbf25c321c6791b7affc84adec00b7dd23 Mon Sep 17 00:00:00 2001 From: Patrick Connolly Date: Sun, 19 Jul 2020 23:06:23 -0300 Subject: [PATCH 14/23] e2e: Make more clear when reporter prints to terminal. --- e2e/cypress/support/index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/e2e/cypress/support/index.js b/e2e/cypress/support/index.js index 7bb8cb979..2679fb87e 100644 --- a/e2e/cypress/support/index.js +++ b/e2e/cypress/support/index.js @@ -29,4 +29,9 @@ before(() => { }) // Register the log collector for logging activity to terminal. -require('cypress-terminal-report/src/installLogsCollector')() +const reporterOptions = { + // When to print terminal logs for tests. + // Options: onFail, always + printLogs: 'onFail', +} +require('cypress-terminal-report/src/installLogsCollector')(reporterOptions) From 5434026f1ec26a331e77347322691965e8c8a6aa Mon Sep 17 00:00:00 2001 From: Patrick Connolly Date: Sun, 19 Jul 2020 23:07:48 -0300 Subject: [PATCH 15/23] Added log command for troubleshooting GitHub Actions issue. --- e2e/cypress/integration/polis/emails.spec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/e2e/cypress/integration/polis/emails.spec.js b/e2e/cypress/integration/polis/emails.spec.js index ae6682583..cf362e573 100644 --- a/e2e/cypress/integration/polis/emails.spec.js +++ b/e2e/cypress/integration/polis/emails.spec.js @@ -67,6 +67,7 @@ describe('Emails', () => { // See: https://www.regextester.com/94502 const urlRegex = new RegExp(/(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&\(\)\*\+,;=.]+/g) const match = emailContent.match(urlRegex) + cy.log(match) // First "url" is email domain. Second url is the one we want. const passwordResetUrl = match[1] From e2da854aa63311e4d8d9d175499fe735edfbcefd Mon Sep 17 00:00:00 2001 From: Patrick Connolly Date: Sun, 19 Jul 2020 23:44:14 -0300 Subject: [PATCH 16/23] e2e: Fixed issue with matching password reset token. --- e2e/cypress/integration/polis/emails.spec.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/e2e/cypress/integration/polis/emails.spec.js b/e2e/cypress/integration/polis/emails.spec.js index cf362e573..a23f4cb88 100644 --- a/e2e/cypress/integration/polis/emails.spec.js +++ b/e2e/cypress/integration/polis/emails.spec.js @@ -63,16 +63,14 @@ describe('Emails', () => { cy.get('.email-content').should('contain', `${Cypress.config().baseUrl}/pwreset/`) cy.get('.email-content').then(($elem) => { const emailContent = $elem.text() - // Had to remove one single-quote from regex so as not to confuse IDE. - // See: https://www.regextester.com/94502 - const urlRegex = new RegExp(/(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&\(\)\*\+,;=.]+/g) - const match = emailContent.match(urlRegex) - cy.log(match) + 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. - const passwordResetUrl = match[1] + cy.log(JSON.stringify(match)) + const passwordResetToken = match[1] // Submit password reset form with new password. - cy.visit(passwordResetUrl) + cy.visit(`/pwreset/${passwordResetToken}`) cy.route({ method: 'POST', From 45017e75c82855c606f35935e0ee76e73385db90 Mon Sep 17 00:00:00 2001 From: Patrick Connolly Date: Wed, 12 Aug 2020 15:54:22 -0400 Subject: [PATCH 17/23] Added testing of email transport failover. --- .github/workflows/cypress-tests.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/cypress-tests.yml b/.github/workflows/cypress-tests.yml index af3b4ef67..04f2e2e8f 100644 --- a/.github/workflows/cypress-tests.yml +++ b/.github/workflows/cypress-tests.yml @@ -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 + - name: Serve app via docker-compose run: docker-compose up --detach From a131a0bda03118fd83ef1ce2e0d11fbe7b9bb443 Mon Sep 17 00:00:00 2001 From: Patrick Connolly Date: Wed, 12 Aug 2020 23:45:36 -0400 Subject: [PATCH 18/23] Adding docs for email transport configuration. [skip ci] --- docs/deployment.md | 53 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/docs/deployment.md b/docs/deployment.md index 938dda3f6..4a5041237 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -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! From 3e06e53a1914d117109c5e81726d33da3564cde9 Mon Sep 17 00:00:00 2001 From: Patrick Connolly Date: Fri, 21 Aug 2020 13:42:46 -0400 Subject: [PATCH 19/23] Check maildev via API instead of UI. --- e2e/cypress.json | 1 - e2e/cypress/integration/polis/emails.spec.js | 112 +++++++++---------- e2e/cypress/support/commands.js | 4 +- 3 files changed, 55 insertions(+), 62 deletions(-) diff --git a/e2e/cypress.json b/e2e/cypress.json index a2f103a8d..d0d27c5ac 100644 --- a/e2e/cypress.json +++ b/e2e/cypress.json @@ -1,5 +1,4 @@ { - "chromeWebSecurity": false, "apiPath": "/api/v3", "ignoreTestFiles": "**/examples/*.spec.js", "baseUrl": "http://localhost" diff --git a/e2e/cypress/integration/polis/emails.spec.js b/e2e/cypress/integration/polis/emails.spec.js index a23f4cb88..e16a033ba 100644 --- a/e2e/cypress/integration/polis/emails.spec.js +++ b/e2e/cypress/integration/polis/emails.spec.js @@ -1,14 +1,13 @@ describe('Emails', () => { - const EMAIL_PORT = '1080' + const MAILDEV_HTTP_PORT = '1080' + // See: https://github.com/maildev/maildev/blob/master/docs/rest.md + const MAILDEV_API_BASE = `${Cypress.config().baseUrl}:${MAILDEV_HTTP_PORT}` beforeEach(() => { - cy.visit(`${Cypress.config().baseUrl}:${EMAIL_PORT}`) - cy.contains('Clear Inbox').click() - cy.contains('Now receiving all emails') + cy.server() + cy.route('POST', Cypress.config().apiPath + '/auth/pwresettoken').as('resetPassword') - cy.fixture('users.json').then((users) => { - cy.wrap(users[0]).as('user') - }) + cy.request('DELETE', MAILDEV_API_BASE + '/email/all') }) it('sends for failed password reset', function () { @@ -16,16 +15,20 @@ describe('Emails', () => { 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) - }) + cy.wait('@resetPassword').its('status').should('eq', 200) + cy.location('pathname').should('eq', '/pwresetinit/done') + + cy.request('GET', MAILDEV_API_BASE + '/email') + .then(resp => { + const email = resp.body.shift() + console.log(email) + cy.wrap(email).its('subject').should('contain', 'Password Reset Failed') + cy.wrap(email).its('to').its(0).its('address').should('contain', nonExistingEmail) + }) }) it('sends for successful password reset', function () { - // Create a new user account + // Create a new user account, so we can actually change password. const randomInt = Math.floor(Math.random() * 10000) const newUser = { email: `user${randomInt}@polis.test`, @@ -34,17 +37,8 @@ describe('Emails', () => { 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) - }) + const strictFail = true + cy.signup(newUser.name, newUser.email, newUser.password, strictFail) cy.logout() @@ -52,41 +46,41 @@ describe('Emails', () => { 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.wait('@resetPassword').its('status').should('eq', 200) + cy.location('pathname').should('eq', '/pwresetinit/done') + + cy.request('GET', MAILDEV_API_BASE + '/email') + .then(resp => { + const email = resp.body.shift() + cy.wrap(email).its('subject').should('contain', 'Polis Password Reset') + cy.wrap(email).its('to').its(0).its('address').should('contain', newUser.email) + + // Has password reset link with proper hostname. + cy.wrap(email).its('text').should('contain', `${Cypress.config().baseUrl}/pwreset/`) + + const emailContent = email.text + console.log(email) + 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('POST', Cypress.config().apiPath + '/auth/password').as('newPassword') + + 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('@newPassword').then((xhr) => { + expect(xhr.status).to.equal(200) + }) }) - }) cy.logout() diff --git a/e2e/cypress/support/commands.js b/e2e/cypress/support/commands.js index c57c8cd49..cc847d34d 100644 --- a/e2e/cypress/support/commands.js +++ b/e2e/cypress/support/commands.js @@ -34,7 +34,7 @@ Cypress.Commands.add("logout", () => { }) }) -Cypress.Commands.add("signup", (name, email, password) => { +Cypress.Commands.add("signup", (name, email, password, strictFail=false) => { cy.request({ method: 'POST', url: Cypress.config().apiPath + '/auth/new', @@ -44,7 +44,7 @@ Cypress.Commands.add("signup", (name, email, password) => { gatekeeperTosPrivacy: true, password: password }, - failOnStatusCode: false + failOnStatusCode: strictFail }).then(resp => { // Expand success criteria to allow user already existing. // TODO: Be smarter with seeding users so we only create once. From 233b8305be090823710e68f618a3a50e9aecdd1f Mon Sep 17 00:00:00 2001 From: Patrick Connolly Date: Fri, 21 Aug 2020 14:55:43 -0400 Subject: [PATCH 20/23] Improved documentation of cypress workflow for email transports. --- .github/workflows/cypress-tests.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cypress-tests.yml b/.github/workflows/cypress-tests.yml index 04f2e2e8f..802e31655 100644 --- a/.github/workflows/cypress-tests.yml +++ b/.github/workflows/cypress-tests.yml @@ -53,7 +53,9 @@ jobs: - name: Set server configuration run: | # Test email transport failovers - echo EMAIL_TRANSPORT_TYPES=mailgun,noop,maildev >> server/docker-dev.env + # mailgun: unconfigured transport (will fail) + # nonexistent: nonexistent transport (will fail) + echo EMAIL_TRANSPORT_TYPES=mailgun,nonexistent,maildev >> server/docker-dev.env - name: Serve app via docker-compose run: docker-compose up --detach From 6b141200cb55c20244b9ce090040b9ef73a05fa5 Mon Sep 17 00:00:00 2001 From: Patrick Connolly Date: Fri, 21 Aug 2020 15:04:51 -0400 Subject: [PATCH 21/23] e2e: Added note about cypress-terminal-report in README. --- e2e/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/e2e/README.md b/e2e/README.md index d82d8a74a..ef9d080ab 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -17,6 +17,16 @@ To run these tests: 4. Click on tests to run - Alternatively, you can run all tests automatically: `npm test` +## Debugging + +- We use [`cypress-terminal-report`][] to ensure that logs display not only in + Cypress's [Test Runner][test-runner] browser UI, but also in the console. + - These only print when a test has failed, to reduce noise. + - Logs of failed tests can be seen on CI server (GitHub Actions). + + [`cypress-terminal-report`]: https://github.com/archfz/cypress-terminal-report#readme + [test-runner]: https://docs.cypress.io/guides/core-concepts/test-runner.html + ## Notes - We keep some helper scripts in `package.json`. From a363939e746fb6465dbd171b724fcbad2672cd0e Mon Sep 17 00:00:00 2001 From: Patrick Connolly Date: Fri, 21 Aug 2020 15:11:55 -0400 Subject: [PATCH 22/23] Removed straggling TODO. --- server/docker-dev.env | 1 - 1 file changed, 1 deletion(-) diff --git a/server/docker-dev.env b/server/docker-dev.env index 9c407afb1..8ac085d5a 100644 --- a/server/docker-dev.env +++ b/server/docker-dev.env @@ -31,6 +31,5 @@ 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. EMAIL_TRANSPORT_TYPES=maildev POLIS_FROM_ADDRESS="Example " From 0f499eb3fccde86137a94b1c765b6a7ebd4b3c84 Mon Sep 17 00:00:00 2001 From: Patrick Connolly Date: Sat, 22 Aug 2020 15:59:18 -0400 Subject: [PATCH 23/23] Set email transport defaults to match current production. --- server/email/senders.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/email/senders.js b/server/email/senders.js index b28c9df14..7c9f99d6e 100644 --- a/server/email/senders.js +++ b/server/email/senders.js @@ -7,7 +7,9 @@ const AWS = require('aws-sdk'); AWS.config.set('region', process.env.AWS_REGION); function sendTextEmailWithBackup(sender, recipient, subject, text) { - const transportTypes = process.env.EMAIL_TRANSPORT_TYPES.split(',') + const transportTypes = process.env.EMAIL_TRANSPORT_TYPES + ? process.env.EMAIL_TRANSPORT_TYPES.split(',') + : ['aws-ses', 'mailgun'] if (transportTypes.length < 2) { new Error('No backup email transport available.'); }