diff --git a/CLAUDE.md b/CLAUDE.md index 1bef6f3..44acce3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -154,6 +154,9 @@ Required for builds and deployment (see turbo.json and .env.example): plus-addressing, domain existence, and MX records - Security (optional): `AUTO_PROJECT_DISABLE` (default: true) - Controls whether projects are automatically disabled when bounce/complaint rate thresholds are exceeded +- Attachment Limits (optional): + - `MAX_ATTACHMENT_SIZE_MB` (default: 10) - Maximum total attachment size in megabytes per email. AWS SES supports up to 40 MB. + - `MAX_ATTACHMENTS_COUNT` (default: 10) - Maximum number of attachments per email - Phishing Detection (optional): - `OPENROUTER_API_KEY` - API key for OpenRouter (enables phishing detection) - `OPENROUTER_MODEL` (default: anthropic/claude-3-haiku) - LLM model to use for content analysis diff --git a/apps/api/src/app/constants.ts b/apps/api/src/app/constants.ts index 7c924cf..9e28bd2 100644 --- a/apps/api/src/app/constants.ts +++ b/apps/api/src/app/constants.ts @@ -110,6 +110,12 @@ export const DISABLE_SIGNUPS = process.env.DISABLE_SIGNUPS === 'true'; // Controls whether email validation checks are performed on signup (default: false) export const VERIFY_EMAIL_ON_SIGNUP = process.env.VERIFY_EMAIL_ON_SIGNUP === 'true'; +// Attachment Limits (optional) +// Maximum total attachment size in MB (default: 10). AWS SES supports up to 40 MB. +export const MAX_ATTACHMENT_SIZE_MB = Number(validateEnv('MAX_ATTACHMENT_SIZE_MB', '10')); +// Maximum number of attachments per email (default: 10) +export const MAX_ATTACHMENTS_COUNT = Number(validateEnv('MAX_ATTACHMENTS_COUNT', '10')); + // Email Verification & Password Reset export const TOKEN_EXPIRY_SECONDS = 3600; // 1 hour export const EMAIL_VERIFICATION_RATE_LIMIT = 3; // Max 3 emails per hour diff --git a/apps/api/src/controllers/Actions.ts b/apps/api/src/controllers/Actions.ts index 3e74349..388e229 100644 --- a/apps/api/src/controllers/Actions.ts +++ b/apps/api/src/controllers/Actions.ts @@ -120,7 +120,7 @@ export class Actions { * - data: object (optional) - Contact data and template variables * - Simple values are saved to contact (persistent) * - {value: any, persistent: false} are only used for this email (non-persistent) - * - attachments: array (optional) - Email attachments (max 10, 10MB total) + * - attachments: array (optional) - Email attachments (configurable via MAX_ATTACHMENTS_COUNT / MAX_ATTACHMENT_SIZE_MB, defaults: 10 / 10MB) * - filename: string (required) - Attachment filename * - content: string (required) - Base64 encoded file content * - contentType: string (required) - MIME type (e.g., "application/pdf") diff --git a/apps/api/src/services/__tests__/EmailService.test.ts b/apps/api/src/services/__tests__/EmailService.test.ts index 7e4626e..3ccc3bd 100644 --- a/apps/api/src/services/__tests__/EmailService.test.ts +++ b/apps/api/src/services/__tests__/EmailService.test.ts @@ -787,8 +787,8 @@ describe('EmailService', () => { } }); - it('should validate attachment size limit (10MB total)', () => { - // Exceeds ~13.3M base64 chars limit + it('should validate attachment size limit (10MB total default)', () => { + // Exceeds ~13.4M base64 chars for the default 10MB limit const largeContent = 'A'.repeat(14000000); const result = ActionSchemas.send.safeParse({ diff --git a/apps/smtp/src/server.ts b/apps/smtp/src/server.ts index 04889e7..fefc149 100644 --- a/apps/smtp/src/server.ts +++ b/apps/smtp/src/server.ts @@ -19,6 +19,7 @@ import {prisma} from './database/prisma.js'; const API_URI = process.env.API_URI ?? 'http://localhost:3000'; const SMTP_DOMAIN = process.env.SMTP_DOMAIN ?? ''; const MAX_RECIPIENTS = parseInt(process.env.MAX_RECIPIENTS ?? '5', 10); +const MAX_ATTACHMENT_SIZE_MB = parseInt(process.env.MAX_ATTACHMENT_SIZE_MB ?? '10', 10); const PORT_SECURE = parseInt(process.env.PORT_SECURE ?? '465', 10); const PORT_SUBMISSION = parseInt(process.env.PORT_SUBMISSION ?? '587', 10); const CERT_PATH = process.env.CERT_PATH ?? '/certs'; @@ -441,7 +442,7 @@ function createSMTPServer(options: {secure: boolean; port: number; key?: Buffer; onRcptTo: handleRcptTo, onData: handleData, banner: 'Plunk SMTP Relay', - size: 10 * 1024 * 1024, // 10MB max message size + size: MAX_ATTACHMENT_SIZE_MB * 1024 * 1024, authOptional: false, // Require authentication }; diff --git a/apps/wiki/content/docs/concepts/transactional-emails.mdx b/apps/wiki/content/docs/concepts/transactional-emails.mdx index 145e80c..f1bd82b 100644 --- a/apps/wiki/content/docs/concepts/transactional-emails.mdx +++ b/apps/wiki/content/docs/concepts/transactional-emails.mdx @@ -7,7 +7,14 @@ icon: Send Transactional emails are emails sent directly through the API. They are typically used for one-to-one communication, such as password resets, order confirmations, and notifications. ## Sending with attachments -Plunk supports sending attachments with transactional emails. You can include up to 10 attachments per email, with a maximum total size of 10MB. Attachments should be base64 encoded and included in the `attachments` array when sending the email via the [/v1/email/send](/api-reference/public-api/sendEmail) endpoint. +Plunk supports sending attachments with transactional emails. By default, you can include up to 10 attachments per email with a maximum total size of 10MB. Attachments should be base64 encoded and included in the `attachments` array when sending the email via the [/v1/email/send](/api-reference/public-api/sendEmail) endpoint. + +Self-hosters can raise these limits via environment variables to match the capacity of the underlying email provider (AWS SES supports up to 40MB per message by default): + +| Variable | Default | Description | +|---|---|---| +| `MAX_ATTACHMENT_SIZE_MB` | `10` | Maximum total attachment size in megabytes | +| `MAX_ATTACHMENTS_COUNT` | `10` | Maximum number of attachments per email | ## Sending from a template You can also send transactional emails using a [template](/concepts/templates) you have created in the dashboard. This allows you to reuse the same design and content for multiple emails, while still personalizing them with contact data. \ No newline at end of file diff --git a/packages/shared/src/schemas/index.ts b/packages/shared/src/schemas/index.ts index a4364dc..dd03753 100644 --- a/packages/shared/src/schemas/index.ts +++ b/packages/shared/src/schemas/index.ts @@ -456,7 +456,7 @@ export const ActionSchemas = { path: ['contentId'], }), ) - .max(10) // Maximum 10 attachments per email + .max(Number(process.env['MAX_ATTACHMENTS_COUNT'] ?? 10)) .optional(), }) .superRefine((data, ctx) => { @@ -471,12 +471,13 @@ export const ActionSchemas = { // Validate total attachment size if (data.attachments && data.attachments.length > 0) { + const maxSizeMb = Number(process.env['MAX_ATTACHMENT_SIZE_MB'] ?? 10); + const maxBase64Length = Math.floor((maxSizeMb * 1024 * 1024 * 4) / 3); const totalBase64Length = data.attachments.reduce((sum, att) => sum + att.content.length, 0); - if (totalBase64Length > 13333333) { - // ~10MB limit + if (totalBase64Length > maxBase64Length) { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: 'Total attachment size must not exceed 10MB', + message: `Total attachment size must not exceed ${maxSizeMb}MB`, path: ['attachments'], }); }