@nixxie-cms/email 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,101 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import type pug from 'pug'
4
+ import type { EmailTemplatesConfig } from '../types'
5
+
6
+ type PugModule = typeof pug
7
+
8
+ let pugModule: PugModule | undefined
9
+
10
+ function getPug(): PugModule {
11
+ if (!pugModule) {
12
+ try {
13
+ pugModule = require('pug') as PugModule
14
+ } catch {
15
+ throw new Error('Pug is not installed. Run: npm install pug')
16
+ }
17
+ }
18
+ return pugModule
19
+ }
20
+
21
+ export class PugEngine {
22
+ private cache = new Map<string, pug.compileTemplate>()
23
+ private config: EmailTemplatesConfig
24
+
25
+ constructor(config: EmailTemplatesConfig) {
26
+ this.config = config
27
+ }
28
+
29
+ async render(templateName: string, data: Record<string, unknown>): Promise<string> {
30
+ const p = getPug()
31
+ const compiled = this.getCompiled(templateName, p)
32
+ const layout = this.config.layouts ? this.resolveLayout(templateName) : null
33
+
34
+ const renderData: Record<string, unknown> = { ...data }
35
+ if (layout) {
36
+ renderData['layout'] = layout
37
+ }
38
+
39
+ return compiled(renderData)
40
+ }
41
+
42
+ private getCompiled(templateName: string, p: PugModule): pug.compileTemplate {
43
+ const cacheKey = `template:${templateName}`
44
+ if (this.config.cache !== false && this.cache.has(cacheKey)) {
45
+ return this.cache.get(cacheKey)!
46
+ }
47
+
48
+ const filePath = this.resolveTemplatePath(templateName)
49
+ const engineOptions = this.config.engineOptions?.pug ?? {}
50
+
51
+ const compiled = p.compileFile(filePath, {
52
+ ...engineOptions,
53
+ filename: filePath,
54
+ cache: false,
55
+ })
56
+
57
+ if (this.config.cache !== false) {
58
+ this.cache.set(cacheKey, compiled)
59
+ }
60
+
61
+ return compiled
62
+ }
63
+
64
+ private resolveTemplatePath(templateName: string): string {
65
+ if (!this.config.dir) {
66
+ throw new Error('templates.dir must be set to use template rendering')
67
+ }
68
+
69
+ const candidates = [
70
+ path.join(this.config.dir, `${templateName}.pug`),
71
+ path.join(this.config.dir, `${templateName}.jade`),
72
+ path.join(this.config.dir, templateName, 'html.pug'),
73
+ path.join(this.config.dir, templateName, 'index.pug'),
74
+ ]
75
+
76
+ for (const candidate of candidates) {
77
+ if (fs.existsSync(candidate)) return candidate
78
+ }
79
+
80
+ throw new Error(`Pug template not found: ${templateName} (searched in ${this.config.dir})`)
81
+ }
82
+
83
+ private resolveLayout(templateName: string): string | null {
84
+ if (!this.config.layouts) return null
85
+
86
+ if (this.config.dir) {
87
+ const perTemplate = path.join(this.config.dir, templateName, 'layout.pug')
88
+ if (fs.existsSync(perTemplate)) return perTemplate
89
+ }
90
+
91
+ // Default layout: templates/layouts/default.pug
92
+ const defaultLayout = path.join(this.config.dir, 'layouts', 'default.pug')
93
+ if (fs.existsSync(defaultLayout)) return defaultLayout
94
+
95
+ return null
96
+ }
97
+
98
+ clearCache() {
99
+ this.cache.clear()
100
+ }
101
+ }
@@ -0,0 +1,180 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import type { EmailTemplatesConfig } from '../types'
4
+ import { HandlebarsEngine } from './HandlebarsEngine'
5
+ import { PugEngine } from './PugEngine'
6
+
7
+ export type RenderedEmail = {
8
+ html: string
9
+ text: string | undefined
10
+ subject: string | undefined
11
+ }
12
+
13
+ export class TemplateManager {
14
+ private handlebars: HandlebarsEngine | undefined
15
+ private pug: PugEngine | undefined
16
+ private config: EmailTemplatesConfig
17
+
18
+ constructor(config: EmailTemplatesConfig) {
19
+ this.config = config
20
+ const engine = config.engine ?? 'handlebars'
21
+ if (engine === 'handlebars' || engine === 'auto') {
22
+ this.handlebars = new HandlebarsEngine(config)
23
+ }
24
+ if (engine === 'pug' || engine === 'auto') {
25
+ this.pug = new PugEngine(config)
26
+ }
27
+ }
28
+
29
+ async render(templateName: string, data: Record<string, unknown>): Promise<RenderedEmail> {
30
+ const engine = this.detectEngine(templateName)
31
+
32
+ switch (engine) {
33
+ case 'handlebars':
34
+ return this.renderWithHandlebars(templateName, data)
35
+ case 'pug':
36
+ return this.renderWithPug(templateName, data)
37
+ default:
38
+ throw new Error(`No template engine configured for template: ${templateName}`)
39
+ }
40
+ }
41
+
42
+ private detectEngine(templateName: string): 'handlebars' | 'pug' | null {
43
+ const configuredEngine = this.config.engine ?? 'handlebars'
44
+
45
+ if (configuredEngine === 'auto' || configuredEngine === 'handlebars') {
46
+ if (this.config.dir) {
47
+ const hbsCandidates = [
48
+ path.join(this.config.dir, `${templateName}.hbs`),
49
+ path.join(this.config.dir, `${templateName}.handlebars`),
50
+ path.join(this.config.dir, templateName, 'html.hbs'),
51
+ path.join(this.config.dir, templateName, 'html.handlebars'),
52
+ ]
53
+ if (hbsCandidates.some(p => fs.existsSync(p))) return 'handlebars'
54
+ }
55
+ }
56
+
57
+ if (configuredEngine === 'auto' || configuredEngine === 'pug') {
58
+ if (this.config.dir) {
59
+ const pugCandidates = [
60
+ path.join(this.config.dir, `${templateName}.pug`),
61
+ path.join(this.config.dir, `${templateName}.jade`),
62
+ path.join(this.config.dir, templateName, 'html.pug'),
63
+ path.join(this.config.dir, templateName, 'index.pug'),
64
+ ]
65
+ if (pugCandidates.some(p => fs.existsSync(p))) return 'pug'
66
+ }
67
+ }
68
+
69
+ if (configuredEngine !== 'auto') return configuredEngine
70
+
71
+ return null
72
+ }
73
+
74
+ private async renderWithHandlebars(
75
+ templateName: string,
76
+ data: Record<string, unknown>
77
+ ): Promise<RenderedEmail> {
78
+ if (!this.handlebars) {
79
+ this.handlebars = new HandlebarsEngine(this.config)
80
+ }
81
+
82
+ const html = await this.handlebars.render(templateName, data)
83
+ const text = await this.renderTextVersion(templateName, data, 'handlebars')
84
+ const subject = await this.renderSubject(templateName, data, 'handlebars')
85
+
86
+ return { html, text, subject }
87
+ }
88
+
89
+ private async renderWithPug(
90
+ templateName: string,
91
+ data: Record<string, unknown>
92
+ ): Promise<RenderedEmail> {
93
+ if (!this.pug) {
94
+ this.pug = new PugEngine(this.config)
95
+ }
96
+
97
+ const html = await this.pug.render(templateName, data)
98
+ const text = await this.renderTextVersion(templateName, data, 'pug')
99
+ const subject = await this.renderSubject(templateName, data, 'pug')
100
+
101
+ return { html, text, subject }
102
+ }
103
+
104
+ private async renderTextVersion(
105
+ templateName: string,
106
+ data: Record<string, unknown>,
107
+ engine: 'handlebars' | 'pug'
108
+ ): Promise<string | undefined> {
109
+ if (!this.config.dir) return undefined
110
+
111
+ const textPaths = engine === 'handlebars'
112
+ ? [
113
+ path.join(this.config.dir, templateName, 'text.hbs'),
114
+ path.join(this.config.dir, templateName, 'text.handlebars'),
115
+ path.join(this.config.dir, `${templateName}.text.hbs`),
116
+ ]
117
+ : [
118
+ path.join(this.config.dir, templateName, 'text.pug'),
119
+ path.join(this.config.dir, `${templateName}.text.pug`),
120
+ ]
121
+
122
+ const textFile = textPaths.find(p => fs.existsSync(p))
123
+ if (!textFile) return undefined
124
+
125
+ // Render a plain-text variant using the same engine but without layout
126
+ const textConfig = { ...this.config, layouts: undefined }
127
+ try {
128
+ if (engine === 'handlebars') {
129
+ const eng = new HandlebarsEngine(textConfig)
130
+ return await eng.render(path.relative(this.config.dir!, textFile).replace(/\\/g, '/').replace(/\.(hbs|handlebars)$/, ''), data)
131
+ } else {
132
+ const eng = new PugEngine(textConfig)
133
+ return await eng.render(path.relative(this.config.dir!, textFile).replace(/\\/g, '/').replace(/\.(pug|jade)$/, ''), data)
134
+ }
135
+ } catch {
136
+ // Fall back to reading the file as plain template
137
+ return fs.readFileSync(textFile, 'utf-8')
138
+ }
139
+ }
140
+
141
+ private async renderSubject(
142
+ templateName: string,
143
+ data: Record<string, unknown>,
144
+ engine: 'handlebars' | 'pug'
145
+ ): Promise<string | undefined> {
146
+ if (!this.config.dir) return undefined
147
+
148
+ const subjectPaths = engine === 'handlebars'
149
+ ? [
150
+ path.join(this.config.dir, templateName, 'subject.hbs'),
151
+ path.join(this.config.dir, templateName, 'subject.handlebars'),
152
+ ]
153
+ : [
154
+ path.join(this.config.dir, templateName, 'subject.pug'),
155
+ ]
156
+
157
+ const subjectFile = subjectPaths.find(p => fs.existsSync(p))
158
+ if (!subjectFile) return undefined
159
+
160
+ const subjectConfig = { ...this.config, layouts: undefined }
161
+ try {
162
+ if (engine === 'handlebars') {
163
+ const eng = new HandlebarsEngine(subjectConfig)
164
+ const relative = path.relative(this.config.dir!, subjectFile).replace(/\\/g, '/').replace(/\.(hbs|handlebars)$/, '')
165
+ return (await eng.render(relative, data)).trim()
166
+ } else {
167
+ const eng = new PugEngine(subjectConfig)
168
+ const relative = path.relative(this.config.dir!, subjectFile).replace(/\\/g, '/').replace(/\.(pug|jade)$/, '')
169
+ return (await eng.render(relative, data)).trim()
170
+ }
171
+ } catch {
172
+ return undefined
173
+ }
174
+ }
175
+
176
+ clearCache() {
177
+ this.handlebars?.clearCache()
178
+ this.pug?.clearCache()
179
+ }
180
+ }
@@ -0,0 +1,4 @@
1
+ export { HandlebarsEngine } from './HandlebarsEngine'
2
+ export { PugEngine } from './PugEngine'
3
+ export { TemplateManager } from './TemplateManager'
4
+ export type { RenderedEmail } from './TemplateManager'
@@ -0,0 +1,50 @@
1
+ import type { EmailTransporter, TransportMessage, TransportResult } from '../types'
2
+
3
+ let messageCounter = 0
4
+
5
+ export class ConsoleTransport implements EmailTransporter {
6
+ async send(message: TransportMessage): Promise<TransportResult> {
7
+ const id = `console-${Date.now()}-${++messageCounter}`
8
+
9
+ const separator = '─'.repeat(60)
10
+ console.log(`\n${separator}`)
11
+ console.log('📧 [Nixxie Email — Development Mode]')
12
+ console.log(separator)
13
+ console.log(` From: ${message.from}`)
14
+ console.log(` To: ${message.to.join(', ')}`)
15
+ if (message.cc?.length) console.log(` CC: ${message.cc.join(', ')}`)
16
+ if (message.bcc?.length) console.log(` BCC: ${message.bcc.join(', ')}`)
17
+ if (message.replyTo?.length) console.log(` Reply-To: ${message.replyTo.join(', ')}`)
18
+ console.log(` Subject: ${message.subject}`)
19
+ console.log(` ID: ${id}`)
20
+ if (message.attachments?.length) {
21
+ console.log(` Attachments:`)
22
+ for (const a of message.attachments) {
23
+ console.log(` • ${a.filename ?? a.path ?? 'unnamed'}`)
24
+ }
25
+ }
26
+ if (message.text) {
27
+ console.log(`\n Text body:\n ${message.text.slice(0, 300).replace(/\n/g, '\n ')}`)
28
+ }
29
+ if (message.html) {
30
+ const truncated = message.html.replace(/<[^>]+>/g, '').trim().slice(0, 200)
31
+ console.log(`\n HTML preview:\n ${truncated}…`)
32
+ }
33
+ console.log(`${separator}\n`)
34
+
35
+ return {
36
+ messageId: id,
37
+ accepted: message.to,
38
+ rejected: [],
39
+ response: 'logged',
40
+ }
41
+ }
42
+
43
+ async verify(): Promise<boolean> {
44
+ return true
45
+ }
46
+
47
+ async close(): Promise<void> {
48
+ // nothing to close
49
+ }
50
+ }
@@ -0,0 +1,58 @@
1
+ import nodemailer from 'nodemailer'
2
+ import type { EmailTransporter, MailgunTransportConfig, TransportMessage, TransportResult } from '../types'
3
+
4
+ export class MailgunTransport implements EmailTransporter {
5
+ private transporter: nodemailer.Transporter
6
+
7
+ constructor(config: MailgunTransportConfig) {
8
+ const host =
9
+ config.region === 'eu'
10
+ ? 'smtp.eu.mailgun.org'
11
+ : 'smtp.mailgun.org'
12
+
13
+ this.transporter = nodemailer.createTransport({
14
+ host,
15
+ port: 587,
16
+ secure: false,
17
+ auth: {
18
+ user: `postmaster@${config.domain}`,
19
+ pass: config.apiKey,
20
+ },
21
+ })
22
+ }
23
+
24
+ async send(message: TransportMessage): Promise<TransportResult> {
25
+ const info = await this.transporter.sendMail({
26
+ from: message.from,
27
+ to: message.to.join(', '),
28
+ cc: message.cc?.join(', '),
29
+ bcc: message.bcc?.join(', '),
30
+ replyTo: message.replyTo?.join(', '),
31
+ subject: message.subject,
32
+ html: message.html,
33
+ text: message.text,
34
+ attachments: message.attachments,
35
+ headers: message.headers,
36
+ })
37
+
38
+ return {
39
+ messageId: info.messageId,
40
+ accepted: (info.accepted as string[]) ?? message.to,
41
+ rejected: (info.rejected as string[]) ?? [],
42
+ response: info.response,
43
+ }
44
+ }
45
+
46
+ async verify(): Promise<boolean> {
47
+ try {
48
+ await this.transporter.verify()
49
+ return true
50
+ } catch {
51
+ return false
52
+ }
53
+ }
54
+
55
+ async close(): Promise<void> {
56
+ this.transporter.close()
57
+ }
58
+ }
@@ -0,0 +1,83 @@
1
+ import type { EmailTransporter, ResendTransportConfig, TransportMessage, TransportResult } from '../types'
2
+
3
+ type ResendSendResponse = {
4
+ id: string
5
+ }
6
+
7
+ type ResendErrorResponse = {
8
+ name: string
9
+ message: string
10
+ statusCode: number
11
+ }
12
+
13
+ export class ResendTransport implements EmailTransporter {
14
+ private readonly apiKey: string
15
+
16
+ constructor(config: ResendTransportConfig) {
17
+ this.apiKey = config.apiKey
18
+ }
19
+
20
+ async send(message: TransportMessage): Promise<TransportResult> {
21
+ const body = {
22
+ from: message.from,
23
+ to: message.to,
24
+ cc: message.cc,
25
+ bcc: message.bcc,
26
+ reply_to: message.replyTo,
27
+ subject: message.subject,
28
+ html: message.html,
29
+ text: message.text,
30
+ headers: message.headers,
31
+ attachments: message.attachments?.map(a => ({
32
+ filename: a.filename,
33
+ // Resend expects attachment `content` as base64; encode raw string/Buffer content.
34
+ content:
35
+ a.content == null
36
+ ? undefined
37
+ : Buffer.isBuffer(a.content)
38
+ ? a.content.toString('base64')
39
+ : Buffer.from(a.content, 'utf-8').toString('base64'),
40
+ path: a.path,
41
+ })),
42
+ }
43
+
44
+ const response = await fetch(`${this.baseUrl}/emails`, {
45
+ method: 'POST',
46
+ headers: {
47
+ Authorization: `Bearer ${this.apiKey}`,
48
+ 'Content-Type': 'application/json',
49
+ },
50
+ body: JSON.stringify(body),
51
+ })
52
+
53
+ const json = await response.json() as ResendSendResponse | ResendErrorResponse
54
+
55
+ if (!response.ok) {
56
+ const err = json as ResendErrorResponse
57
+ throw new Error(`Resend API error ${response.status}: ${err.message}`)
58
+ }
59
+
60
+ const data = json as ResendSendResponse
61
+ return {
62
+ messageId: data.id,
63
+ accepted: message.to,
64
+ rejected: [],
65
+ response: `${response.status} ${response.statusText}`,
66
+ }
67
+ }
68
+
69
+ async verify(): Promise<boolean> {
70
+ try {
71
+ const response = await fetch(`${this.baseUrl}/domains`, {
72
+ headers: { Authorization: `Bearer ${this.apiKey}` },
73
+ })
74
+ return response.ok
75
+ } catch {
76
+ return false
77
+ }
78
+ }
79
+
80
+ async close(): Promise<void> {
81
+ // No persistent connections to close
82
+ }
83
+ }
@@ -0,0 +1,54 @@
1
+ import nodemailer from 'nodemailer'
2
+ import type { EmailTransporter, SendGridTransportConfig, TransportMessage, TransportResult } from '../types'
3
+
4
+ export class SendGridTransport implements EmailTransporter {
5
+ private transporter: nodemailer.Transporter
6
+
7
+ constructor(config: SendGridTransportConfig) {
8
+ // SendGrid supports nodemailer via their SMTP relay
9
+ this.transporter = nodemailer.createTransport({
10
+ host: 'smtp.sendgrid.net',
11
+ port: 587,
12
+ secure: false,
13
+ auth: {
14
+ user: 'apikey',
15
+ pass: config.apiKey,
16
+ },
17
+ })
18
+ }
19
+
20
+ async send(message: TransportMessage): Promise<TransportResult> {
21
+ const info = await this.transporter.sendMail({
22
+ from: message.from,
23
+ to: message.to.join(', '),
24
+ cc: message.cc?.join(', '),
25
+ bcc: message.bcc?.join(', '),
26
+ replyTo: message.replyTo?.join(', '),
27
+ subject: message.subject,
28
+ html: message.html,
29
+ text: message.text,
30
+ attachments: message.attachments,
31
+ headers: message.headers,
32
+ })
33
+
34
+ return {
35
+ messageId: info.messageId,
36
+ accepted: (info.accepted as string[]) ?? message.to,
37
+ rejected: (info.rejected as string[]) ?? [],
38
+ response: info.response,
39
+ }
40
+ }
41
+
42
+ async verify(): Promise<boolean> {
43
+ try {
44
+ await this.transporter.verify()
45
+ return true
46
+ } catch {
47
+ return false
48
+ }
49
+ }
50
+
51
+ async close(): Promise<void> {
52
+ this.transporter.close()
53
+ }
54
+ }
@@ -0,0 +1,58 @@
1
+ import nodemailer from 'nodemailer'
2
+ import type { EmailTransporter, SesTransportConfig, TransportMessage, TransportResult } from '../types'
3
+
4
+ export class SesTransport implements EmailTransporter {
5
+ private transporter: nodemailer.Transporter
6
+
7
+ constructor(config: SesTransportConfig) {
8
+ // Uses SES SMTP interface — credentials are SES SMTP credentials (different from IAM keys)
9
+ // See: https://docs.aws.amazon.com/ses/latest/dg/smtp-credentials.html
10
+ this.transporter = nodemailer.createTransport({
11
+ host: `email-smtp.${config.region}.amazonaws.com`,
12
+ port: 587,
13
+ secure: false,
14
+ auth: config.credentials
15
+ ? {
16
+ user: config.credentials.accessKeyId,
17
+ pass: config.credentials.secretAccessKey,
18
+ }
19
+ : undefined,
20
+ tls: { ciphers: 'SSLv3' },
21
+ })
22
+ }
23
+
24
+ async send(message: TransportMessage): Promise<TransportResult> {
25
+ const info = await this.transporter.sendMail({
26
+ from: message.from,
27
+ to: message.to.join(', '),
28
+ cc: message.cc?.join(', '),
29
+ bcc: message.bcc?.join(', '),
30
+ replyTo: message.replyTo?.join(', '),
31
+ subject: message.subject,
32
+ html: message.html,
33
+ text: message.text,
34
+ attachments: message.attachments,
35
+ headers: message.headers,
36
+ })
37
+
38
+ return {
39
+ messageId: info.messageId,
40
+ accepted: (info.accepted as string[]) ?? message.to,
41
+ rejected: (info.rejected as string[]) ?? [],
42
+ response: info.response,
43
+ }
44
+ }
45
+
46
+ async verify(): Promise<boolean> {
47
+ try {
48
+ await this.transporter.verify()
49
+ return true
50
+ } catch {
51
+ return false
52
+ }
53
+ }
54
+
55
+ async close(): Promise<void> {
56
+ this.transporter.close()
57
+ }
58
+ }
@@ -0,0 +1,66 @@
1
+ import nodemailer from 'nodemailer'
2
+ import type { EmailTransporter, SmtpTransportConfig, TransportMessage, TransportResult } from '../types'
3
+
4
+ export class SmtpTransport implements EmailTransporter {
5
+ private transporter: nodemailer.Transporter
6
+
7
+ constructor(config: SmtpTransportConfig) {
8
+ this.transporter = nodemailer.createTransport({
9
+ host: config.host,
10
+ port: config.port ?? 587,
11
+ secure: config.secure ?? (config.port === 465),
12
+ auth: config.auth,
13
+ tls: config.tls,
14
+ connectionTimeout: config.connectionTimeout ?? 5_000,
15
+ greetingTimeout: config.greetingTimeout ?? 5_000,
16
+ socketTimeout: config.socketTimeout ?? 30_000,
17
+ pool: config.pool ?? false,
18
+ maxConnections: config.maxConnections ?? 5,
19
+ maxMessages: config.maxMessages ?? 100,
20
+ })
21
+ }
22
+
23
+ async send(message: TransportMessage): Promise<TransportResult> {
24
+ const info = await this.transporter.sendMail({
25
+ from: message.from,
26
+ to: message.to.join(', '),
27
+ cc: message.cc?.join(', '),
28
+ bcc: message.bcc?.join(', '),
29
+ replyTo: message.replyTo?.join(', '),
30
+ subject: message.subject,
31
+ html: message.html,
32
+ text: message.text,
33
+ attachments: message.attachments?.map(a => ({
34
+ filename: a.filename,
35
+ content: a.content,
36
+ path: a.path,
37
+ href: a.href,
38
+ contentType: a.contentType,
39
+ encoding: a.encoding,
40
+ contentDisposition: a.contentDisposition,
41
+ cid: a.cid,
42
+ })),
43
+ headers: message.headers,
44
+ })
45
+
46
+ return {
47
+ messageId: info.messageId,
48
+ accepted: (info.accepted as string[]) ?? [],
49
+ rejected: (info.rejected as string[]) ?? [],
50
+ response: info.response,
51
+ }
52
+ }
53
+
54
+ async verify(): Promise<boolean> {
55
+ try {
56
+ await this.transporter.verify()
57
+ return true
58
+ } catch {
59
+ return false
60
+ }
61
+ }
62
+
63
+ async close(): Promise<void> {
64
+ this.transporter.close()
65
+ }
66
+ }
@@ -0,0 +1,30 @@
1
+ import type { EmailTransportConfig, EmailTransporter } from '../types'
2
+ import { ConsoleTransport } from './ConsoleTransport'
3
+ import { MailgunTransport } from './MailgunTransport'
4
+ import { ResendTransport } from './ResendTransport'
5
+ import { SendGridTransport } from './SendGridTransport'
6
+ import { SesTransport } from './SesTransport'
7
+ import { SmtpTransport } from './SmtpTransport'
8
+
9
+ export function createTransport(config: EmailTransportConfig): EmailTransporter {
10
+ switch (config.type) {
11
+ case 'smtp':
12
+ return new SmtpTransport(config)
13
+ case 'sendgrid':
14
+ return new SendGridTransport(config)
15
+ case 'mailgun':
16
+ return new MailgunTransport(config)
17
+ case 'ses':
18
+ return new SesTransport(config)
19
+ case 'resend':
20
+ return new ResendTransport(config)
21
+ case 'console':
22
+ return new ConsoleTransport()
23
+ default: {
24
+ const exhaustiveCheck: never = config
25
+ throw new Error(`Unknown email transport type: ${(exhaustiveCheck as any).type}`)
26
+ }
27
+ }
28
+ }
29
+
30
+ export { SmtpTransport, SendGridTransport, MailgunTransport, SesTransport, ResendTransport, ConsoleTransport }