diff options
| author | Elizabeth Hunt <me@liz.coffee> | 2025-12-15 20:17:22 -0800 |
|---|---|---|
| committer | Elizabeth Hunt <me@liz.coffee> | 2025-12-15 20:19:43 -0800 |
| commit | 2814d5520623efe5f48c26f639d3ed6cc5f0d8d2 (patch) | |
| tree | 3fc1af65dac5ed55aceaab7574b22fea32cad86a /src | |
| parent | 2e41f030f02a336c2e9866d3d56b0494da5a622e (diff) | |
| download | posthook-2814d5520623efe5f48c26f639d3ed6cc5f0d8d2.tar.gz posthook-2814d5520623efe5f48c26f639d3ed6cc5f0d8d2.zip | |
Add email integration
Diffstat (limited to 'src')
| -rw-r--r-- | src/activity/index.ts | 12 | ||||
| -rw-r--r-- | src/integrations/email.ts | 69 | ||||
| -rw-r--r-- | src/types/index.ts | 33 |
3 files changed, 114 insertions, 0 deletions
diff --git a/src/activity/index.ts b/src/activity/index.ts index d3537b4..ea2ae09 100644 --- a/src/activity/index.ts +++ b/src/activity/index.ts @@ -18,6 +18,7 @@ import type { Storage } from '../storage/index.js'; import { ContentType } from '../types/index.js'; import { verifyHCaptcha } from '../integrations/hcaptcha.js'; import { sendNtfyNotification } from '../integrations/ntfy.js'; +import { sendEmailNotification } from '../integrations/email.js'; import { TokenSigner } from '../token/index.js'; const webhookRequestMetric = Metric.fromName('Webhook.Process').asResult(); @@ -310,6 +311,17 @@ export class WebhookActivityImpl implements IWebhookActivity { } } + // Send email notification if configured + if (route.email?.enabled) { + const emailResult = await sendEmailNotification(route.email, storedRequest); + if (emailResult.left().present()) { + const err = emailResult.left().get(); + tReq.trace.traceScope(LogLevel.WARN).trace(`email notification failed: ${err.message}`); + } else { + tReq.trace.trace('email notification sent'); + } + } + const baseName = `${storedRequest.timestamp}_${storedRequest.uuid}`; return tReq.move( diff --git a/src/integrations/email.ts b/src/integrations/email.ts new file mode 100644 index 0000000..aa4c36c --- /dev/null +++ b/src/integrations/email.ts @@ -0,0 +1,69 @@ +import { Either, type IEither } from '@emprespresso/pengueno'; +import type { EmailConfig, StoredRequest } from '../types/index.js'; +import nodemailer from 'nodemailer'; + +export async function sendEmailNotification(config: EmailConfig, request: StoredRequest): Promise<IEither<Error, void>> { + if (!config.enabled || !config.to || !config.from) { + return Either.right(<void>undefined); + } + + return Either.fromFailableAsync(async () => { + // Create transporter based on configuration + const transporter = nodemailer.createTransport({ + host: config.host || 'localhost', + port: config.port || 25, + secure: config.secure ?? false, + auth: config.username && config.password + ? { + user: config.username, + pass: config.password, + } + : undefined, + }); + + const subject = config.subject || `Webhook received: ${request.routeName}`; + + // Build email body + let htmlBody = ` + <h2>Webhook Notification</h2> + <p><strong>Route:</strong> ${request.routeName}</p> + <p><strong>Method:</strong> ${request.method}</p> + <p><strong>Timestamp:</strong> ${new Date(request.timestamp).toISOString()}</p> + <p><strong>UUID:</strong> ${request.uuid}</p> + `; + + if (config.includeBody && request.body !== undefined) { + htmlBody += ` + <h3>Request Body:</h3> + <pre>${JSON.stringify(request.body, null, 2)}</pre> + `; + } + + if (config.includeHeaders && request.headers) { + htmlBody += ` + <h3>Headers:</h3> + <pre>${JSON.stringify(request.headers, null, 2)}</pre> + `; + } + + if (request.files && request.files.length > 0) { + htmlBody += ` + <h3>Uploaded Files:</h3> + <ul> + ${request.files.map(f => `<li>${f.originalFilename} (${f.contentType}, ${f.size} bytes)</li>`).join('')} + </ul> + `; + } + + const mailOptions = { + from: config.from, + to: config.to, + subject: subject, + html: htmlBody, + }; + + await transporter.sendMail(mailOptions); + + return <void>undefined; + }); +} diff --git a/src/types/index.ts b/src/types/index.ts index 5e0b2d4..89a2a6c 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -12,12 +12,27 @@ export interface NtfyConfig { topic?: string; } +export interface EmailConfig { + enabled: boolean; + to?: string; + from?: string; + host?: string; + port?: number; + secure?: boolean; + username?: string; + password?: string; + subject?: string; + includeBody?: boolean; + includeHeaders?: boolean; +} + export interface RouteConfig { name: string; contentType: ContentType; hcaptchaProtected: boolean; hcaptchaSecret?: string; ntfy?: NtfyConfig; + email?: EmailConfig; requireToken?: boolean; } @@ -71,6 +86,24 @@ export function isRouteConfig(obj: unknown): obj is RouteConfig { } } + // Validate email config if present + if (r.email !== undefined) { + if (typeof r.email !== 'object' || r.email === null) return false; + const email = r.email as Record<string, unknown>; + if (typeof email.enabled !== 'boolean') return false; + if (email.enabled && (typeof email.to !== 'string' || typeof email.from !== 'string')) { + return false; + } + if (email.host !== undefined && typeof email.host !== 'string') return false; + if (email.port !== undefined && typeof email.port !== 'number') return false; + if (email.secure !== undefined && typeof email.secure !== 'boolean') return false; + if (email.username !== undefined && typeof email.username !== 'string') return false; + if (email.password !== undefined && typeof email.password !== 'string') return false; + if (email.subject !== undefined && typeof email.subject !== 'string') return false; + if (email.includeBody !== undefined && typeof email.includeBody !== 'boolean') return false; + if (email.includeHeaders !== undefined && typeof email.includeHeaders !== 'boolean') return false; + } + // Validate requireToken if present if (r.requireToken !== undefined && typeof r.requireToken !== 'boolean') { return false; |
