mirror of
https://github.com/papra-hq/papra.git
synced 2026-05-02 19:39:34 -05:00
feat(server): added smtp client support for emailing (#306)
This commit is contained in:
committed by
GitHub
parent
cb38d66485
commit
f0876fdc63
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@papra/app-server": minor
|
||||
---
|
||||
|
||||
Added support for classic SMTP client for email sending
|
||||
@@ -57,6 +57,7 @@
|
||||
"mime-types": "^3.0.1",
|
||||
"nanoid": "^5.1.5",
|
||||
"node-cron": "^3.0.3",
|
||||
"nodemailer": "^7.0.3",
|
||||
"p-limit": "^6.2.0",
|
||||
"p-queue": "^8.1.0",
|
||||
"picomatch": "^4.0.2",
|
||||
@@ -76,6 +77,7 @@
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@types/picomatch": "^4.0.0",
|
||||
"@types/sanitize-html": "^2.16.0",
|
||||
"@vitest/coverage-v8": "catalog:",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { LOGGER_EMAIL_DRIVER_NAME, loggerEmailDriverFactory } from './logger/logger.email-driver';
|
||||
import { RESEND_EMAIL_DRIVER_NAME, resendEmailDriverFactory } from './resend/resend.email-driver';
|
||||
import { SMTP_EMAIL_DRIVER_NAME, smtpEmailDriverFactory } from './smtp/smtp.email-driver';
|
||||
|
||||
export const emailDrivers = {
|
||||
[RESEND_EMAIL_DRIVER_NAME]: resendEmailDriverFactory,
|
||||
[LOGGER_EMAIL_DRIVER_NAME]: loggerEmailDriverFactory,
|
||||
[SMTP_EMAIL_DRIVER_NAME]: smtpEmailDriverFactory,
|
||||
} as const;
|
||||
|
||||
export const emailDriverFactoryNames = Object.keys(emailDrivers);
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { ConfigDefinition } from 'figue';
|
||||
import { z } from 'zod';
|
||||
import { booleanishSchema } from '../../../config/config.schemas';
|
||||
import { parseJson } from '../../../intake-emails/intake-emails.schemas';
|
||||
|
||||
export const smtpEmailDriverConfig = {
|
||||
host: {
|
||||
doc: 'The host of the SMTP server',
|
||||
schema: z.string().optional(),
|
||||
default: '',
|
||||
env: 'SMTP_HOST',
|
||||
},
|
||||
port: {
|
||||
doc: 'The port of the SMTP server',
|
||||
schema: z.coerce.number(),
|
||||
default: 587,
|
||||
env: 'SMTP_PORT',
|
||||
},
|
||||
user: {
|
||||
doc: 'The user of the SMTP server',
|
||||
schema: z.string().optional(),
|
||||
default: undefined,
|
||||
env: 'SMTP_USER',
|
||||
},
|
||||
password: {
|
||||
doc: 'The password of the SMTP server',
|
||||
schema: z.string().optional(),
|
||||
default: undefined,
|
||||
env: 'SMTP_PASSWORD',
|
||||
},
|
||||
secure: {
|
||||
doc: 'Whether to use a secure connection to the SMTP server',
|
||||
schema: booleanishSchema,
|
||||
default: false,
|
||||
env: 'SMTP_SECURE',
|
||||
},
|
||||
rawConfig: {
|
||||
doc: 'The raw configuration for the nodemailer SMTP client in JSON format for advanced use cases. If set, this will override all other config options. See https://nodemailer.com/smtp/ for more details.',
|
||||
schema: z.string().transform(parseJson).optional(),
|
||||
default: undefined,
|
||||
env: 'SMTP_JSON_CONFIG',
|
||||
},
|
||||
} as const satisfies ConfigDefinition;
|
||||
@@ -0,0 +1,45 @@
|
||||
import nodemailer from 'nodemailer';
|
||||
import { createError } from '../../../shared/errors/errors';
|
||||
import { defineEmailDriverFactory } from '../email-driver.models';
|
||||
|
||||
export const SMTP_EMAIL_DRIVER_NAME = 'smtp';
|
||||
|
||||
export const smtpEmailDriverFactory = defineEmailDriverFactory(({ config, logger }) => {
|
||||
const { fromEmail } = config.emails;
|
||||
const { host, port, secure, user, password, rawConfig } = config.emails.drivers.smtp;
|
||||
|
||||
const transporter = nodemailer.createTransport(rawConfig ?? {
|
||||
host,
|
||||
port,
|
||||
secure,
|
||||
auth: {
|
||||
user,
|
||||
pass: password,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
name: SMTP_EMAIL_DRIVER_NAME,
|
||||
sendEmail: async ({ to, subject, html, from }) => {
|
||||
try {
|
||||
const { messageId } = await transporter.sendMail({
|
||||
from: from ?? fromEmail,
|
||||
to,
|
||||
subject,
|
||||
html,
|
||||
});
|
||||
|
||||
logger.info({ messageId }, 'Email sent');
|
||||
} catch (error) {
|
||||
logger.error({ error }, 'Failed to send email');
|
||||
|
||||
throw createError({
|
||||
code: 'email.send_failed',
|
||||
message: 'Failed to send email',
|
||||
statusCode: 500,
|
||||
isInternal: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import { emailDriverFactoryNames } from './drivers/email-driver';
|
||||
import { LOGGER_EMAIL_DRIVER_NAME } from './drivers/logger/logger.email-driver';
|
||||
import { loggerEmailDriverConfig } from './drivers/logger/logger.email-driver.config';
|
||||
import { resendEmailDriverConfig } from './drivers/resend/resend.email-driver.config';
|
||||
import { smtpEmailDriverConfig } from './drivers/smtp/smtp.email-driver.config';
|
||||
|
||||
export const emailsConfig = {
|
||||
fromEmail: {
|
||||
@@ -21,5 +22,6 @@ export const emailsConfig = {
|
||||
drivers: {
|
||||
resend: resendEmailDriverConfig,
|
||||
logger: loggerEmailDriverConfig,
|
||||
smtp: smtpEmailDriverConfig,
|
||||
},
|
||||
} as const satisfies ConfigDefinition;
|
||||
|
||||
Generated
+21
-1
@@ -319,6 +319,9 @@ importers:
|
||||
node-cron:
|
||||
specifier: ^3.0.3
|
||||
version: 3.0.3
|
||||
nodemailer:
|
||||
specifier: ^7.0.3
|
||||
version: 7.0.3
|
||||
p-limit:
|
||||
specifier: ^6.2.0
|
||||
version: 6.2.0
|
||||
@@ -371,6 +374,9 @@ importers:
|
||||
'@types/node-cron':
|
||||
specifier: ^3.0.11
|
||||
version: 3.0.11
|
||||
'@types/nodemailer':
|
||||
specifier: ^6.4.17
|
||||
version: 6.4.17
|
||||
'@types/picomatch':
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
@@ -3341,6 +3347,9 @@ packages:
|
||||
'@types/node@22.15.18':
|
||||
resolution: {integrity: sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==}
|
||||
|
||||
'@types/nodemailer@6.4.17':
|
||||
resolution: {integrity: sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==}
|
||||
|
||||
'@types/normalize-package-data@2.4.4':
|
||||
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
|
||||
|
||||
@@ -5806,6 +5815,7 @@ packages:
|
||||
|
||||
libsql@0.4.7:
|
||||
resolution: {integrity: sha512-T9eIRCs6b0J1SHKYIvD8+KCJMcWZ900iZyxdnSCdqxN12Z1ijzT+jY5nrk72Jw4B0HGzms2NgpryArlJqvc3Lw==}
|
||||
cpu: [x64, arm64, wasm32]
|
||||
os: [darwin, linux, win32]
|
||||
|
||||
lilconfig@3.1.3:
|
||||
@@ -6292,6 +6302,10 @@ packages:
|
||||
node-releases@2.0.19:
|
||||
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
|
||||
|
||||
nodemailer@7.0.3:
|
||||
resolution: {integrity: sha512-Ajq6Sz1x7cIK3pN6KesGTah+1gnwMnx5gKl3piQlQQE/PwyJ4Mbc8is2psWYxK3RJTVeqsDaCv8ZzXLCDHMTZw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
nopt@5.0.0:
|
||||
resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -11420,6 +11434,10 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/nodemailer@6.4.17':
|
||||
dependencies:
|
||||
'@types/node': 22.15.18
|
||||
|
||||
'@types/normalize-package-data@2.4.4': {}
|
||||
|
||||
'@types/picomatch@3.0.2': {}
|
||||
@@ -11434,7 +11452,7 @@ snapshots:
|
||||
|
||||
'@types/sax@1.2.7':
|
||||
dependencies:
|
||||
'@types/node': 17.0.45
|
||||
'@types/node': 22.15.18
|
||||
|
||||
'@types/unist@2.0.11': {}
|
||||
|
||||
@@ -15340,6 +15358,8 @@ snapshots:
|
||||
|
||||
node-releases@2.0.19: {}
|
||||
|
||||
nodemailer@7.0.3: {}
|
||||
|
||||
nopt@5.0.0:
|
||||
dependencies:
|
||||
abbrev: 1.1.1
|
||||
|
||||
Reference in New Issue
Block a user