feat: add configurable attachment limits for email service

Closes #358
This commit is contained in:
Dries Augustyns
2026-04-29 08:48:03 +02:00
parent 10946511b0
commit 2eb407993b
7 changed files with 27 additions and 9 deletions
+3
View File
@@ -154,6 +154,9 @@ Required for builds and deployment (see turbo.json and .env.example):
plus-addressing, domain existence, and MX records plus-addressing, domain existence, and MX records
- Security (optional): `AUTO_PROJECT_DISABLE` (default: true) - Controls whether projects are automatically disabled when - Security (optional): `AUTO_PROJECT_DISABLE` (default: true) - Controls whether projects are automatically disabled when
bounce/complaint rate thresholds are exceeded 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): - Phishing Detection (optional):
- `OPENROUTER_API_KEY` - API key for OpenRouter (enables phishing detection) - `OPENROUTER_API_KEY` - API key for OpenRouter (enables phishing detection)
- `OPENROUTER_MODEL` (default: anthropic/claude-3-haiku) - LLM model to use for content analysis - `OPENROUTER_MODEL` (default: anthropic/claude-3-haiku) - LLM model to use for content analysis
+6
View File
@@ -110,6 +110,12 @@ export const DISABLE_SIGNUPS = process.env.DISABLE_SIGNUPS === 'true';
// Controls whether email validation checks are performed on signup (default: false) // Controls whether email validation checks are performed on signup (default: false)
export const VERIFY_EMAIL_ON_SIGNUP = process.env.VERIFY_EMAIL_ON_SIGNUP === 'true'; 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 // Email Verification & Password Reset
export const TOKEN_EXPIRY_SECONDS = 3600; // 1 hour export const TOKEN_EXPIRY_SECONDS = 3600; // 1 hour
export const EMAIL_VERIFICATION_RATE_LIMIT = 3; // Max 3 emails per hour export const EMAIL_VERIFICATION_RATE_LIMIT = 3; // Max 3 emails per hour
+1 -1
View File
@@ -120,7 +120,7 @@ export class Actions {
* - data: object (optional) - Contact data and template variables * - data: object (optional) - Contact data and template variables
* - Simple values are saved to contact (persistent) * - Simple values are saved to contact (persistent)
* - {value: any, persistent: false} are only used for this email (non-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 * - filename: string (required) - Attachment filename
* - content: string (required) - Base64 encoded file content * - content: string (required) - Base64 encoded file content
* - contentType: string (required) - MIME type (e.g., "application/pdf") * - contentType: string (required) - MIME type (e.g., "application/pdf")
@@ -787,8 +787,8 @@ describe('EmailService', () => {
} }
}); });
it('should validate attachment size limit (10MB total)', () => { it('should validate attachment size limit (10MB total default)', () => {
// Exceeds ~13.3M base64 chars limit // Exceeds ~13.4M base64 chars for the default 10MB limit
const largeContent = 'A'.repeat(14000000); const largeContent = 'A'.repeat(14000000);
const result = ActionSchemas.send.safeParse({ const result = ActionSchemas.send.safeParse({
+2 -1
View File
@@ -19,6 +19,7 @@ import {prisma} from './database/prisma.js';
const API_URI = process.env.API_URI ?? 'http://localhost:3000'; const API_URI = process.env.API_URI ?? 'http://localhost:3000';
const SMTP_DOMAIN = process.env.SMTP_DOMAIN ?? ''; const SMTP_DOMAIN = process.env.SMTP_DOMAIN ?? '';
const MAX_RECIPIENTS = parseInt(process.env.MAX_RECIPIENTS ?? '5', 10); 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_SECURE = parseInt(process.env.PORT_SECURE ?? '465', 10);
const PORT_SUBMISSION = parseInt(process.env.PORT_SUBMISSION ?? '587', 10); const PORT_SUBMISSION = parseInt(process.env.PORT_SUBMISSION ?? '587', 10);
const CERT_PATH = process.env.CERT_PATH ?? '/certs'; const CERT_PATH = process.env.CERT_PATH ?? '/certs';
@@ -441,7 +442,7 @@ function createSMTPServer(options: {secure: boolean; port: number; key?: Buffer;
onRcptTo: handleRcptTo, onRcptTo: handleRcptTo,
onData: handleData, onData: handleData,
banner: 'Plunk SMTP Relay', banner: 'Plunk SMTP Relay',
size: 10 * 1024 * 1024, // 10MB max message size size: MAX_ATTACHMENT_SIZE_MB * 1024 * 1024,
authOptional: false, // Require authentication authOptional: false, // Require authentication
}; };
@@ -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. 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 ## 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 ## 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. 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.
+5 -4
View File
@@ -456,7 +456,7 @@ export const ActionSchemas = {
path: ['contentId'], path: ['contentId'],
}), }),
) )
.max(10) // Maximum 10 attachments per email .max(Number(process.env['MAX_ATTACHMENTS_COUNT'] ?? 10))
.optional(), .optional(),
}) })
.superRefine((data, ctx) => { .superRefine((data, ctx) => {
@@ -471,12 +471,13 @@ export const ActionSchemas = {
// Validate total attachment size // Validate total attachment size
if (data.attachments && data.attachments.length > 0) { 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); const totalBase64Length = data.attachments.reduce((sum, att) => sum + att.content.length, 0);
if (totalBase64Length > 13333333) { if (totalBase64Length > maxBase64Length) {
// ~10MB limit
ctx.addIssue({ ctx.addIssue({
code: z.ZodIssueCode.custom, code: z.ZodIssueCode.custom,
message: 'Total attachment size must not exceed 10MB', message: `Total attachment size must not exceed ${maxSizeMb}MB`,
path: ['attachments'], path: ['attachments'],
}); });
} }