跳转到内容
搜索文档

邮件附件

处理不同类型的邮件附件,包括 PDF、内联图像和经过验证与编码的文件上传。

最后更新 查看 MarkdownAgent 设置

本示例演示如何发送包含 PDF、内联图像和文件上传等各种类型附件的邮件。

在 Wrangler 配置文件中配置邮件绑定:

{
	"send_email": [{ "name": "EMAIL" }],
	"vars": {
		"DOMAIN": "yourdomain.com",
	},
}
[[send_email]]
name = "EMAIL"

[vars]
DOMAIN = "yourdomain.com"
interface Env {
	EMAIL: SendEmail;
	DOMAIN: string;
}

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const url = new URL(request.url);

		if (url.pathname === "/send-invoice" && request.method === "POST") {
			return sendInvoiceWithPDF(request, env);
		}

		if (url.pathname === "/send-report" && request.method === "POST") {
			return sendReportWithImages(request, env);
		}

		if (url.pathname === "/upload-attachment" && request.method === "POST") {
			return sendEmailWithUpload(request, env);
		}

		return new Response("Not Found", { status: 404 });
	},
};

PDF 附件

生成 PDF 文档并将其作为邮件附件发送:

async function sendInvoiceWithPDF(
	request: Request,
	env: Env,
): Promise<Response> {
	const { customerEmail, invoiceData } = await request.json();

	// Generate PDF content
	const pdfContent = generateInvoicePDF(invoiceData);

	await env.EMAIL.send({
		to: customerEmail,
		from: `billing@${env.DOMAIN}`,
		subject: `Invoice #${invoiceData.number}`,
		html: `
      <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
        <h1>Invoice #${invoiceData.number}</h1>
        <p>Dear ${invoiceData.customerName},</p>
        
        <div style="background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0;">
          <h3>Invoice Details</h3>
          <p><strong>Invoice Number:</strong> ${invoiceData.number}</p>
          <p><strong>Date:</strong> ${new Date(invoiceData.date).toLocaleDateString()}</p>
          <p><strong>Amount Due:</strong> $${invoiceData.total}</p>
          <p><strong>Due Date:</strong> ${new Date(invoiceData.dueDate).toLocaleDateString()}</p>
        </div>
        
        <p>Please find your invoice attached. Payment is due within 30 days.</p>
        <p>Thank you for your business!</p>
      </div>
    `,
		attachments: [
			{
				filename: `invoice-${invoiceData.number}.pdf`,
				content: new TextEncoder().encode(pdfContent).buffer,
				type: "application/pdf",
				disposition: "attachment",
			},
		],
	});

	return new Response(
		JSON.stringify({ success: true, message: "Invoice sent" }),
	);
}

function generateInvoicePDF(invoiceData: any): string {
	// Simple PDF generation (in practice, use a proper PDF library)
	const pdfContent = `%PDF-1.4
1 0 obj<< /Type /Catalog /Pages 2 0 R >>endobj
2 0 obj<< /Type /Pages /Kids [3 0 R] /Count 1 >>endobj
3 0 obj<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>endobj
4 0 obj<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>endobj
5 0 obj<< /Length 200 >>stream
BT
/F1 24 Tf
50 700 Td
(INVOICE #${invoiceData.number}) Tj
0 -50 Td
/F1 12 Tf
(Customer: ${invoiceData.customerName}) Tj
0 -20 Td
(Date: ${new Date(invoiceData.date).toLocaleDateString()}) Tj
0 -20 Td
(Amount: $${invoiceData.total}) Tj
ET
endstream
endobj
xref
0 6
0000000000 65535 f 
0000000010 00000 n 
0000000053 00000 n 
0000000100 00000 n 
0000000200 00000 n 
0000000300 00000 n 
trailer<< /Size 6 /Root 1 0 R >>
startxref
400
%%EOF`;

	return pdfContent;
}

内联图像

使用 Content-ID 引用将图像直接嵌入邮件内容:

async function sendReportWithImages(
	request: Request,
	env: Env,
): Promise<Response> {
	const { recipientEmail, reportData } = await request.json();

	// Generate chart and logo images as SVG
	const chartImage = generateChartImage(reportData);
	const logoImage = getCompanyLogo();

	await env.EMAIL.send({
		to: recipientEmail,
		from: `reports@${env.DOMAIN}`,
		subject: `${reportData.title} - ${reportData.period}`,
		html: `
      <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
        <!-- Company Logo (inline) -->
        <div style="text-align: center; margin-bottom: 30px;">
          <img src="cid:company-logo" alt="Company Logo" style="width: 200px; height: auto;">
        </div>
        
        <h1 style="color: #2563eb;">${reportData.title}</h1>
        <p style="color: #666;">Period: ${reportData.period}</p>
        
        <h2>Key Metrics</h2>
        <div style="background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0;">
          <p><strong>Revenue:</strong> $${reportData.metrics.revenue.toLocaleString()}</p>
          <p><strong>New Users:</strong> ${reportData.metrics.users.toLocaleString()}</p>
          <p><strong>Growth Rate:</strong> ${reportData.metrics.growth}%</p>
        </div>
        
        <h2>Performance Chart</h2>
        <!-- Inline chart image -->
        <div style="text-align: center; margin: 20px 0;">
          <img src="cid:performance-chart" alt="Performance Chart" 
               style="max-width: 100%; height: auto; border: 1px solid #ddd; border-radius: 4px;">
        </div>
        
        <p>For detailed analysis, please contact our team.</p>
      </div>
    `,
		attachments: [
			{
				filename: "company-logo.svg",
				content: logoImage,
				type: "image/svg+xml",
				disposition: "inline",
				contentId: "company-logo",
			},
			{
				filename: "performance-chart.svg",
				content: chartImage,
				type: "image/svg+xml",
				disposition: "inline",
				contentId: "performance-chart",
			},
		],
	});

	return new Response(
		JSON.stringify({ success: true, message: "Report sent" }),
	);
}

function generateChartImage(reportData: any): ArrayBuffer {
	// Generate simple SVG chart (in practice, use a chart library)
	const chartSVG = `
    <svg width="400" height="200" xmlns="http://www.w3.org/2000/svg">
      <rect width="400" height="200" fill="#f8f9fa" stroke="#dee2e6"/>
      <rect x="50" y="150" width="30" height="${Math.max(10, reportData.metrics.growth * 2)}" fill="#2563eb"/>
      <rect x="100" y="120" width="30" height="50" fill="#28a745"/>
      <rect x="150" y="100" width="30" height="70" fill="#ffc107"/>
      <rect x="200" y="80" width="30" height="90" fill="#dc3545"/>
      <text x="200" y="30" text-anchor="middle" font-family="Arial" font-size="16">Performance Chart</text>
    </svg>
  `;

	return new TextEncoder().encode(chartSVG).buffer;
}

function getCompanyLogo(): ArrayBuffer {
	// Simple SVG logo
	const logoSVG = `
    <svg width="200" height="60" xmlns="http://www.w3.org/2000/svg">
      <rect width="200" height="60" fill="#2563eb" rx="8"/>
      <text x="100" y="35" text-anchor="middle" font-family="Arial"
            font-size="24" font-weight="bold" fill="white">Company</text>
    </svg>
  `;

	return new TextEncoder().encode(logoSVG).buffer;
}

文件上传

处理文件上传,并在验证后将其作为邮件附件发送:

async function sendEmailWithUpload(
	request: Request,
	env: Env,
): Promise<Response> {
	const formData = await request.formData();
	const file = formData.get("file") as File;
	const recipientEmail = formData.get("email") as string;
	const message = formData.get("message") as string;
	const senderName = (formData.get("senderName") as string) || "Someone";

	if (!file || !recipientEmail) {
		return new Response(
			JSON.stringify({
				error: "Missing required fields: file and email",
			}),
			{ status: 400 },
		);
	}

	// Validate file
	const validation = validateFile(file);
	if (!validation.valid) {
		return new Response(
			JSON.stringify({
				error: validation.error,
			}),
			{ status: 400 },
		);
	}

	await env.EMAIL.send({
		to: recipientEmail,
		from: `uploads@${env.DOMAIN}`,
		subject: `File shared: ${file.name}`,
		html: `
      <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
        <h1 style="color: #2563eb;">File Shared</h1>

        <div style="background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0;">
          <p><strong>${senderName}</strong> has shared a file with you.</p>
          ${message ? `<p><em>"${message}"</em></p>` : ""}
        </div>

        <div style="border: 1px solid #dee2e6; border-radius: 8px; padding: 20px; margin: 20px 0;">
          <h3 style="margin: 0 0 15px 0;">File Details</h3>
          <p><strong>Filename:</strong> ${file.name}</p>
          <p><strong>Size:</strong> ${formatFileSize(file.size)}</p>
          <p><strong>Type:</strong> ${getFileTypeDescription(file.type)}</p>
        </div>

        <p>The file is attached to this email for download.</p>

        <div style="background: #fff3cd; border: 1px solid #ffeaa7; padding: 15px; border-radius: 4px;">
          <p style="margin: 0; color: #856404;">
            <strong>Security notice:</strong> Always verify the source before opening attachments.
          </p>
        </div>
      </div>
    `,
		attachments: [
			{
				filename: file.name,
				content: await file.arrayBuffer(),
				type: file.type,
				disposition: "attachment",
			},
		],
	});

	return new Response(
		JSON.stringify({
			success: true,
			message: `File ${file.name} sent to ${recipientEmail}`,
		}),
	);
}

function validateFile(file: File): { valid: boolean; error?: string } {
	// 5 MiB matches the general outbound message size limit. Messages up to
	// 25 MiB are accepted only when sent to verified destination addresses.
	// Refer to /email-service/platform/limits/ for details.
	const maxSize = 5 * 1024 * 1024;
	const allowedTypes = [
		"image/jpeg",
		"image/png",
		"image/gif",
		"application/pdf",
		"text/plain",
		"text/csv",
		"application/msword",
		"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
		"application/vnd.ms-excel",
		"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
		"application/zip",
	];

	if (file.size === 0) {
		return { valid: false, error: "File is empty" };
	}

	if (file.size > maxSize) {
		return {
			valid: false,
			error: `File too large (max ${formatFileSize(maxSize)})`,
		};
	}

	if (!allowedTypes.includes(file.type)) {
		return { valid: false, error: "File type not allowed" };
	}

	// Check for dangerous extensions
	const dangerousExtensions = [".exe", ".bat", ".cmd", ".scr", ".vbs", ".js"];
	const fileName = file.name.toLowerCase();
	if (dangerousExtensions.some((ext) => fileName.endsWith(ext))) {
		return {
			valid: false,
			error: "File extension not allowed for security reasons",
		};
	}

	return { valid: true };
}

function formatFileSize(bytes: number): string {
	if (bytes === 0) return "0 Bytes";
	const k = 1024;
	const sizes = ["Bytes", "KB", "MB", "GB"];
	const i = Math.floor(Math.log(bytes) / Math.log(k));
	return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}

function getFileTypeDescription(mimeType: string): string {
	const typeMap: Record<string, string> = {
		"image/jpeg": "JPEG Image",
		"image/png": "PNG Image",
		"image/gif": "GIF Image",
		"application/pdf": "PDF Document",
		"text/plain": "Text Document",
		"application/msword": "Word Document",
		"application/vnd.ms-excel": "Excel Spreadsheet",
		"application/zip": "ZIP Archive",
	};

	return typeMap[mimeType] || "Unknown";
}

后续步骤

  • Send methodAttachment 接口和 send() 方法的完整参考。
  • Limits — 消息大小和附件限制。
  • Email headers — 在附件之外设置自定义标头。

这篇文档对您有帮助吗?