aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/activity/index.ts12
-rw-r--r--src/integrations/email.ts69
-rw-r--r--src/types/index.ts33
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;