Skip to content

Commit

Permalink
feat: add email support with MJML and nunjucks engine (or just plain …
Browse files Browse the repository at this point in the history
…text)
  • Loading branch information
sahachide committed Mar 15, 2021
1 parent cb59d67 commit 35544a3
Show file tree
Hide file tree
Showing 10 changed files with 161 additions and 42 deletions.
8 changes: 8 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,11 @@ services:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: test

mailcatcher:
container_name: mailcatcher
build:
context: docker/services/mailcatcher
dockerfile: Dockerfile
ports:
- 1080:1080
9 changes: 9 additions & 0 deletions docker/services/mailcatcher/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM ruby:2.7.2-alpine3.12

RUN set -xe \
&& apk add --no-cache libstdc++ sqlite-libs \
&& apk add --no-cache --virtual .build-deps build-base sqlite-dev \
&& gem install mailcatcher -v 0.7.1 -N \
&& apk del .build-deps

ENTRYPOINT ["mailcatcher", "--no-quit", "--smtp-ip=0.0.0.0", "--http-ip=0.0.0.0", "--foreground"]
71 changes: 68 additions & 3 deletions package-lock.json

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

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"dev:clean": "yalc installations clean zents",
"dev:dependency-update": "npm-check -u",
"test": "jest --config jest.config.json",
"test:docker": "docker-compose up -d --build"
"test:start-docker": "docker-compose up -d --build"
},
"files": [
"lib/**/*",
Expand All @@ -56,7 +56,9 @@
],
"dependencies": {
"@types/formidable": "1.0.31",
"@types/html-to-text": "^6.0.0",
"@types/ioredis": "^4.17.5",
"@types/nodemailer": "6.4.0",
"@types/nunjucks": "3.1.3",
"app-root-path": "3.0.0",
"bcrypt": "^5.0.0",
Expand All @@ -66,6 +68,7 @@
"dayjs": "^1.9.3",
"find-my-way": "^3.0.4",
"formidable": "2.0.0-canary.20200504.1",
"html-to-text": "^7.0.0",
"ioredis": "~4.17.3",
"jsonwebtoken": "^8.5.1",
"lodash.merge": "~4.6.2",
Expand Down Expand Up @@ -98,7 +101,6 @@
"@types/mjml": "^4.7.0",
"@types/ms": "^0.7.31",
"@types/node": "^14.11.10",
"@types/nodemailer": "^6.4.0",
"@types/parseurl": "^1.3.1",
"@types/qs": "^6.9.5",
"@types/secure-password": "^3.1.0",
Expand Down
4 changes: 4 additions & 0 deletions src/config/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ export const defaultConfig: ZenConfig = {
lstripBlocks: false,
},
email: {
enable: false,
engine: 'mjml',
htmlToText: {
enable: true,
},
},
log: {
level: 'info',
Expand Down
2 changes: 2 additions & 0 deletions src/dependencies/Injector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { BodyContextAction } from './InjectorAction/BodyContextAction'
import type { Class } from 'type-fest'
import type { Context } from '../http/Context'
import { CookieContextAction } from './InjectorAction/CookieContextAction'
import { EmailAction } from './InjectorAction/EmailAction'
import { ErrorContextAction } from './InjectorAction/ErrorContextAction'
import type { ModuleContext } from './ModuleContext'
import { ParamsContextAction } from './InjectorAction/ParamsContextAction'
Expand Down Expand Up @@ -63,6 +64,7 @@ export class Injector {
new CookieContextAction(this, context),
new RequestContextAction(this, context),
new ResponseContextAction(this, context),
new EmailAction(this),
new ErrorContextAction(this, context),
new AllContextAction(this, context),
]
Expand Down
63 changes: 33 additions & 30 deletions src/email/EmailFactory.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,36 @@
import type { EmailTemplates } from '../types/types'
import type { Transport } from 'nodemailer'
import type { EmailTemplates, MailOptions } from '../types/types'

import type { MailResponse } from '../types/interfaces'
import type { Transporter } from 'nodemailer'
import { config } from '../config/config'
import { createTransport } from 'nodemailer'
import { htmlToText } from 'html-to-text'
import { log } from '../log/logger'
import mjml2html from 'mjml'
import { renderString } from 'nunjucks'

export class EmailFactory {
protected transporter: Transport | null
protected transporter: Transporter

constructor(protected emailTemplates: EmailTemplates) {
if (typeof config.email?.host === 'string') {
this.transporter = (createTransport(config.email) as unknown) as Transport
if (config.email?.enable) {
this.transporter = createTransport(config.email)
} else {
this.transporter = null
}
}
public send({
to = config.email.defaults.to,
cc = config.email.defaults.cc,
bcc = config.email.defaults.bcc,
from = config.email.defaults.from,
topic = config.email.defaults.topic,
template,
payload = {},
engine = config.email.engine,
}: {
to: string
from?: string
cc?: string
bcc?: string
topic: string
template: string
payload?: Record<string, unknown>
engine?: string
}): void {

public async send(options: MailOptions): Promise<MailResponse> {
if (this.transporter === null) {
log.warn(
'Trying to send an E-Mail without proper email configuration. Please configure at least a email host',
throw new Error(
'Trying to send an E-Mail without proper email configuration. Please enable email in your ZenTS configuration before sending emails',
)

return
} else if (!this.emailTemplates.has(template)) {
throw new Error(`Email template "${template}" not found!`)
} else if (!this.emailTemplates.has(options.template)) {
throw new Error(`Email template "${options.template}" not found!`)
}

const engine = options.engine ?? config.email.engine
const { template, payload } = options
let content = this.emailTemplates.get(template)

if (engine !== 'plain') {
Expand All @@ -62,5 +48,22 @@ export class EmailFactory {

content = result.html
}

const data: MailOptions =
typeof config.email.mailOptions === 'undefined'
? options
: Object.assign({}, config.email.mailOptions, options)

if (engine !== 'plain') {
data.html = content

if (config.email?.htmlToText?.enable) {
data.text = htmlToText(content, config.email?.htmlToText)
}
} else if (!data.keepText) {
data.text = content
}

return (await this.transporter.sendMail(data)) as MailResponse
}
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ export { log } from './log/'
export { SecurityProviderOptions, SecurityProvider } from './security/'
export {
Context,
Email,
TemplateFilter,
QueryString,
MailOptions,
InjectedConnection,
InjectedEntityManager,
InjectedRepository,
Expand Down
23 changes: 16 additions & 7 deletions src/types/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
HTTPMethod,
HeaderValue,
LogLevel,
MailOptions,
TemplateFileExtension,
TemplateFiltersMap,
} from './types'
Expand All @@ -14,6 +15,7 @@ import type { ConnectionOptions } from 'typeorm'
import type { Context as ContextClass } from '../http/Context'
import type { ControllerFactory } from '../controller/ControllerFactory'
import type { EmailFactory } from '../email/EmailFactory'
import type { HtmlToTextOptions } from 'html-to-text'
import type { RedisOptions } from 'ioredis'
import type { Request } from '../http/Request'
import type { RequestFactory } from '../http/RequestFactory'
Expand Down Expand Up @@ -169,6 +171,15 @@ export interface LoadModuleResult<T> {

// ---- M

export interface MailResponse {
messageId?: string
envelope: Record<string, unknown>
accepted: string[]
rejected: string[]
pending: string[]
response: string
}

export interface ModuleDependency {
propertyKey: string
dependency: Class
Expand Down Expand Up @@ -470,12 +481,11 @@ export interface ZenConfig {
| 'hex'
}
email?: {
defaults?: {
to?: string
cc?: string
bcc?: string
from?: string
topic?: string
enable?: boolean
engine?: 'mjml' | 'nunjucks' | 'plain'
mailOptions?: Partial<MailOptions>
htmlToText?: HtmlToTextOptions & {
enable?: boolean
}
host?: string
port?: number
Expand All @@ -500,7 +510,6 @@ export interface ZenConfig {
maxMessages?: number
rateDelta?: number
rateLimit?: number
engine?: 'mjml' | 'nunjucks' | 'plain'
mjml?: {
fonts?: {
[key: string]: string
Expand Down
15 changes: 15 additions & 0 deletions src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ import type {
import type { IncomingMessage, ServerResponse } from 'http'

import type { DB_TYPE } from './enums'
import type { EmailFactory } from '../email/EmailFactory'
import type { HtmlToTextOptions } from 'html-to-text'
import type { Redis } from 'ioredis'
import type { SecurityProvider } from '../security/SecurityProvider'
import type { SendMailOptions } from 'nodemailer'
import type { Stream } from 'stream'
import type { TemplateResponse } from '../template/TemplateResponse'
import type findMyWay from 'find-my-way'
Expand All @@ -40,6 +43,8 @@ export type DatabaseObjectType<T> = T extends DB_TYPE.ORM ? Connection : Redis

export type Entities = Map<string, Class>

export type Email = EmailFactory

export type EmailTemplates = Map<string, string>

export type ErrorResponseData = JsonObject | JsonArray
Expand Down Expand Up @@ -103,6 +108,16 @@ export type LoaderTemplates = Map<string, LoaderTemplateItem>
export type LogLevel = 'fatal' | 'error' | 'warn' | 'log' | 'info' | 'success' | 'debug' | 'trace'

// ---- M

export type MailOptions = Partial<SendMailOptions> &
Partial<HtmlToTextOptions> & {
template: string
payload?: Record<string, unknown>
engine?: string
keepHtml?: boolean
keepText?: boolean
}

// ---- N

export type NunjucksFilterCallback = (err: any, result: any) => void
Expand Down

0 comments on commit 35544a3

Please sign in to comment.