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

Refactor to allow for multiple transports #13

Closed
wants to merge 27 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions conf/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
"isPublic": true
}
},
"transport": {
"description": "Mail transport to use",
"type": "string"
},
"connectionUrl": {
"description": "Connection URL for the SMTP service",
"type": "string",
Expand Down
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
* Email sending utilities
* @namespace mailer
*/
export { default as AbstractMailTransport } from './lib/AbstractMailTransport.js'
export { default } from './lib/MailerModule.js'
30 changes: 30 additions & 0 deletions lib/AbstractMailTransport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { App } from 'adapt-authoring-core'
/**
* An abstract class which encompasses functions related to a single mail transport type
* @memberof mailer
*/
class AbstractMailTransport {
name;

/**
* Shortcut to retrieve mailer config values
* @param {string} key
* @returns {String} the config value
*/
getConfig (key) {
return App.instance.config.get(`adapt-authoring-mailer.${key}`)
}

/**
* Sends an email
* @param {MailData} data
*/
async send (data) {}

/**
* Performs any useful tests to check transport is working correctly
*/
async test () {}
}

export default AbstractMailTransport
65 changes: 47 additions & 18 deletions lib/MailerModule.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { AbstractModule } from 'adapt-authoring-core'
import nodemailer from 'nodemailer'
import AbstractMailTransport from './AbstractMailTransport.js'
import AzureTransport from './transports/AzureTransport.js'
import FilesystemTransport from './transports/FilesystemTransport.js'
import SmtpTransport from './transports/SmtpTransport.js'
/**
* Mailer Module
* @memberof mailer
Expand All @@ -19,15 +22,10 @@ class MailerModule extends AbstractModule {
*/
this.connectionUrl = this.getConfig('connectionUrl')
/**
* The Nodemailer SMTP transport instance
* @type {Nodemailer~Transport}
* Registered mail transports
* @type {Object}
*/
this.transporter = undefined

if (this.isEnabled) {
this.transporter = nodemailer.createTransport(this.connectionUrl)
await this.testConnection()
}
this.transports = {}
// note we still enable the API route if mailer is disabled to allow for testing
const [auth, server] = await this.app.waitForModule('auth', 'server')
const router = server.api.createChildRouter('mailer')
Expand All @@ -43,18 +41,47 @@ class MailerModule extends AbstractModule {
}
})
auth.unsecureRoute(`${router.path}/test`, 'post')

if (this.isEnabled) {
// add the standard transport
this.registerTransport(AzureTransport)
this.registerTransport(FilesystemTransport)
this.registerTransport(SmtpTransport)
this.app.onReady().then(() => this.initTransports())
}
}

/**
* Checks the provided SMTP settings using nodemailer.verify.
* @return {Promise}
*/
async testConnection () {
registerTransport (TransportClass) {
let t
try {
await this.transporter.verify()
this.log('info', 'SMTP connection verified successfully')
t = new TransportClass()
this.transports[t.name] = t
} catch (e) {
this.log('warn', `SMTP connection test failed, ${e}`)
this.log('error', `Failed to create transport, ${e}`)
}
if (!(t instanceof AbstractMailTransport)) {
this.log('error', 'Failed to create transport, not an instance of AbstractMailTransport')
}
if (!t.name) {
this.log('error', 'Failed to create transport, does not define a name')
}
}

getTransport() {
const transportName = this.getConfig('transport')
if(!this.transports[transportName]) {
throw new Error(`No transport with name ${transportName}`)
}
return this.transports[transportName]
}

async initTransports () {
const transport = this.getTransport()
try {
await transport.test()
this.log('info', `${transport.name} connection verified successfully`)
} catch (e) {
this.log('warn', `${transport.name} connection test failed, ${e}`)
}
}

Expand All @@ -79,7 +106,9 @@ class MailerModule extends AbstractModule {
const jsonschema = await this.app.waitForModule('jsonschema')
const schema = await jsonschema.getSchema('maildata')
await schema.validate(data)
await this.transporter.sendMail(data)

await this.getTransport().send(data)

this.log('info', 'email sent successfully')
} catch (e) {
throw this.app.errors.MAIL_SEND_FAILED.setData({ email: data.to, error: e })
Expand Down
48 changes: 48 additions & 0 deletions lib/transports/AzureTransport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import AbstractMailTransport from '../AbstractMailTransport.js'
import { EmailClient, KnownEmailSendStatus } from '@azure/communication-email'
/**
* Microsoft Azure Mail Transport
* @memberof mailer
* @extends {AbstractMailTransport}
*/
class AzureTransport extends AbstractMailTransport {
name = 'azure'

/** @override */
async send (data) {
const message = {
senderAddress: data.from,
content: {
subject: data.subject,
plainText: data.text,
html: data.html
},
recipients: {
to: [{ address: data.to }]
}
}
const client = new EmailClient(this.getConfig('connectionUrl'))
const poller = await client.beginSend(message)

if (!poller.getOperationState().isStarted) {
throw new Error('Poller was not started.')
}
let elapsedSecs = 0
const timeoutSecs = 10
while (!poller.isDone()) {
poller.poll()

await new Promise(resolve => setTimeout(resolve, 1000))
elapsedSecs++

if (elapsedSecs >= timeoutSecs) {
throw new Error('Polling timed out.')
}
}
if (poller.getResult().status !== KnownEmailSendStatus.Succeeded) {
throw poller.getResult().error
}
}
}

export default AzureTransport
23 changes: 23 additions & 0 deletions lib/transports/FilesystemTransport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import AbstractMailTransport from '../AbstractMailTransport.js'
import { App } from 'adapt-authoring-core'
import fs from 'fs/promises'
import path from 'path'
/**
* Local filesystem shim mail transport, will store mail data locally
* @memberof mailer
* @extends {AbstractMailTransport}
*/
class FilesystemTransport extends AbstractMailTransport {
name = 'filesystem'

/** @override */
async send (data) {
const dir = path.join(App.instance.getConfig('tempDir'), 'mailer')
try {
await fs.mkdir(dir)
} catch {}
return fs.writeFile(path.join(dir, `${Date.now()}.txt`), JSON.stringify(data, null, 2))
}
}

export default FilesystemTransport
26 changes: 26 additions & 0 deletions lib/transports/SmtpTransport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import AbstractMailTransport from '../AbstractMailTransport.js'
import nodemailer from 'nodemailer'
/**
* SMTP mail transport
* @memberof mailer
* @extends {AbstractMailTransport}
*/
class SmtpTransport extends AbstractMailTransport {
name = 'smtp'

createTransport () {
return nodemailer.createTransport(this.getConfig('connectionUrl'))
}

/** @override */
async send (data) {
return this.createTransport().sendMail(data)
}

/** @override */
async test () {
await this.createTransport().verify()
}
}

export default SmtpTransport
9 changes: 0 additions & 9 deletions lib/typedefs.js

This file was deleted.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"main": "index.js",
"repository": "github:adapt-security/adapt-authoring-mailer",
"dependencies": {
"@azure/communication-email": "^1.0.0",
"nodemailer": "^6.9.9"
},
"peerDependencies": {
Expand Down
Loading