diff --git a/package-lock.json b/package-lock.json index 5a4f58522..2354e09b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7906,15 +7906,6 @@ } } }, - "@types/nodemailer": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.0.tgz", - "integrity": "sha512-KY7bFWB0MahRZvVW4CuW83qcCDny59pJJ0MQ5ifvfcjNwPlIT0vW4uARO4u1gtkYnWdhSvURegecY/tzcukJcA==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, "@types/normalize-package-data": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", @@ -18037,6 +18028,11 @@ "minimalistic-crypto-utils": "^1.0.0" } }, + "emailjs": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/emailjs/-/emailjs-3.2.0.tgz", + "integrity": "sha512-eZMcUgkhNkHlr9J8ifhNyvuMEoSlpZzZj10kgCaa3b2DQo9GKkYYoQ4dB0iOQ6WERrT7VKW7XOTEbt7ZbTtyyQ==" + }, "emoji-regex": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.1.1.tgz", @@ -36242,11 +36238,6 @@ "semver": "^5.3.0" } }, - "nodemailer": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.4.6.tgz", - "integrity": "sha512-/kJ+FYVEm2HuUlw87hjSqTss+GU35D4giOpdSfGp7DO+5h6RlJj7R94YaYHOkoxu1CSaM0d3WRBtCzwXrY6MKA==" - }, "noms": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz", diff --git a/package.json b/package.json index 9913c9060..ea638c52e 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "dataloader": "1.4.0", "dompurify": "^2.0.8", "dotenv": "^8.2.0", + "emailjs": "^3.2.0", "env-rewrite": "^1.0.2", "express": "^4.17.1", "express-enforces-ssl": "^1.1.0", @@ -123,7 +124,6 @@ "mongodb-core": "^3.2.7", "ms": "^2.1.2", "node-fetch": "^2.6.0", - "nodemailer": "^6.4.6", "nunjucks": "^3.2.1", "on-finished": "^2.3.0", "passport": "^0.4.1", @@ -204,7 +204,6 @@ "@types/ms": "^0.7.31", "@types/node": "^12.12.34", "@types/node-fetch": "^2.5.5", - "@types/nodemailer": "^6.4.0", "@types/nunjucks": "^3.1.3", "@types/on-finished": "^2.3.1", "@types/passport": "^1.0.3", diff --git a/src/core/server/queue/tasks/mailer/processor.ts b/src/core/server/queue/tasks/mailer/processor.ts index bb822cf4f..8bbbe663d 100644 --- a/src/core/server/queue/tasks/mailer/processor.ts +++ b/src/core/server/queue/tasks/mailer/processor.ts @@ -3,14 +3,18 @@ import { DOMLocalization } from "@fluent/dom/compat"; import Joi from "@hapi/joi"; import { Job } from "bull"; import createDOMPurify from "dompurify"; +import { + Message, + MessageAttachment, + SMTPClient, + SMTPConnectionOptions, +} from "emailjs"; import { minify } from "html-minifier"; import htmlToText from "html-to-text"; import { JSDOM } from "jsdom"; import { juiceResources } from "juice"; import { camelCase, isNil } from "lodash"; import { Db } from "mongodb"; -import { createTransport } from "nodemailer"; -import { Options } from "nodemailer/lib/smtp-connection"; import { LanguageCode } from "coral-common/helpers"; import { Config } from "coral-server/config"; @@ -51,12 +55,16 @@ const MailerDataSchema = Joi.object().keys({ tenantID: Joi.string(), }); -interface Message { - from: string; - to: string; - html: string; - text: string; - subject: string; +function send(client: SMTPClient, message: Message): Promise { + return new Promise((resolve, reject) => { + client.send(message, (err, msg) => { + if (err) { + return reject(err); + } + + return resolve(msg); + }); + }); } /** @@ -150,6 +158,18 @@ function createMessageTranslator(i18n: I18n) { // Juice the HTML to inline resources. const html = await juiceHTML(dom.serialize()); + // Wrap the html in an email attachment as an alternative representation of + // the email message. This is casted "as MessageAttachment" following the + // test example here: + // + // https://github.com/eleith/emailjs/blob/0781d02cbf7de0d55b417d00ae7f5ab1283bf527/test/message.ts#L166-L169 + // + // TODO: (wyattjoh) tracking issue here https://github.com/eleith/emailjs/issues/254 + const attachment = { + data: html, + alternative: true, + } as MessageAttachment; + // Get the translated subject. const subject = translate( bundle, @@ -162,13 +182,13 @@ function createMessageTranslator(i18n: I18n) { const text = htmlToText.fromString(html); // Prepare the message payload. - return { + return new Message({ from: fromAddress, to: data.message.to, - html, text, subject, - }; + attachment, + }); }; } @@ -177,9 +197,7 @@ export const createJobProcessor = (options: MailProcessorOptions) => { // Create the cache adapter that will handle invalidating the email transport // when the tenant experiences a change. - const cache = new TenantCacheAdapter>( - tenantCache - ); + const cache = new TenantCacheAdapter(tenantCache); // Create the message translator function. const translateMessage = createMessageTranslator(i18n); @@ -268,23 +286,21 @@ export const createJobProcessor = (options: MailProcessorOptions) => { if (!transport) { try { // Create the new transport options. - const opts: Options = { + const opts: Partial = { host: smtp.host, port: smtp.port, - secure: smtp.secure, + tls: smtp.secure, }; if (smtp.authentication && smtp.username && smtp.password) { // If authentication details are provided, add them to the transport // configuration. - opts.auth = { - type: "login", - user: smtp.username, - pass: smtp.password, - }; + opts.user = smtp.username; + opts.password = smtp.password; + opts.authentication = ["LOGIN"]; } // Create the transport based on the smtp uri. - transport = createTransport(opts); + transport = new SMTPClient(opts); } catch (e) { throw new WrappedInternalError(e, "could not create email transport"); } @@ -303,7 +319,7 @@ export const createJobProcessor = (options: MailProcessorOptions) => { try { // Send the mail message. - await transport.sendMail(message); + await send(transport, message); } catch (e) { throw new WrappedInternalError(e, "could not send email"); }