經過 Day 21-23 的 CI/CD 與部署策略建立,我們已經有了完整的自動化部署體系。今天我們開始其他實作:AWS SES (Simple Email Service) 整合。作為專業的郵件發送服務,SES 提供高送達率、低成本、以及強大的監控能力。我們將實作 SES 的設定、域名驗證、DKIM/SPF 配置、以及送達率優化策略。
/**
* AWS SES 核心概念
*
* 1. 沙盒模式 (Sandbox Mode)
* - 新帳號預設在沙盒模式
* - 限制:只能發送給已驗證的信箱
* - 限制:每日 200 封,每秒 1 封
* - 需申請移出沙盒才能用於生產
*
* 2. 身份驗證
* - Email Address:驗證單一信箱
* - Domain:驗證整個域名(推薦)
* - 需在 DNS 新增驗證記錄
*
* 3. 發送配額
* - 每日發送配額:從 200 開始
* - 發送速率:從 1 封/秒開始
* - 會根據信譽自動調整
*
* 4. 信譽管理
* - Bounce Rate(退信率)< 5%
* - Complaint Rate(投訴率)< 0.1%
* - 違反會導致停權
*
* 5. 定價 (ap-northeast-1 Tokyo)
* - 前 62,000 封/月:免費(AWS Free Tier)
* - 之後:$0.10 / 1,000 封
* - 附件:$0.12 / GB
* - Dedicated IP:$24.95/月
*/
// infrastructure/lib/ses-iam-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';
export class SESIAMStack extends cdk.Stack {
public readonly sesUser: iam.User;
public readonly sesAccessKey: iam.AccessKey;
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// 建立 SES 專用 IAM User
this.sesUser = new iam.User(this, 'SESUser', {
userName: 'kyo-ses-user',
});
// 建立 SES 發送政策
const sesPolicy = new iam.Policy(this, 'SESPolicy', {
policyName: 'KyoSESPolicy',
statements: [
// 發送郵件權限
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
'ses:SendEmail',
'ses:SendRawEmail',
'ses:SendTemplatedEmail',
'ses:SendBulkTemplatedEmail',
],
resources: ['*'],
}),
// 取得發送配額
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
'ses:GetSendQuota',
'ses:GetSendStatistics',
'ses:GetAccount',
],
resources: ['*'],
}),
// 管理模板
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
'ses:CreateTemplate',
'ses:UpdateTemplate',
'ses:GetTemplate',
'ses:ListTemplates',
'ses:DeleteTemplate',
],
resources: ['*'],
}),
// 管理身份驗證
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
'ses:VerifyEmailIdentity',
'ses:VerifyDomainIdentity',
'ses:GetIdentityVerificationAttributes',
'ses:DeleteIdentity',
],
resources: ['*'],
}),
// 管理組態集 (Configuration Sets)
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
'ses:CreateConfigurationSet',
'ses:UpdateConfigurationSet',
'ses:DeleteConfigurationSet',
'ses:DescribeConfigurationSet',
'ses:CreateConfigurationSetEventDestination',
],
resources: ['*'],
}),
],
});
sesPolicy.attachToUser(this.sesUser);
// 建立 Access Key
this.sesAccessKey = new iam.AccessKey(this, 'SESAccessKey', {
user: this.sesUser,
});
// 輸出憑證(僅用於開發,生產環境應使用 Secrets Manager)
new cdk.CfnOutput(this, 'AccessKeyId', {
value: this.sesAccessKey.accessKeyId,
description: 'SES Access Key ID',
});
new cdk.CfnOutput(this, 'SecretAccessKey', {
value: this.sesAccessKey.secretAccessKey.unsafeUnwrap(),
description: 'SES Secret Access Key (Store securely!)',
});
}
}
// infrastructure/lib/ses-domain-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as ses from 'aws-cdk-lib/aws-ses';
import * as route53 from 'aws-cdk-lib/aws-route53';
import { Construct } from 'constructs';
export class SESDomainStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const domain = 'kyong.com';
// 取得 Route53 Hosted Zone
const hostedZone = route53.HostedZone.fromLookup(this, 'HostedZone', {
domainName: domain,
});
// 建立 SES 域名身份
const emailIdentity = new ses.EmailIdentity(this, 'EmailIdentity', {
identity: ses.Identity.publicHostedZone(hostedZone),
// 啟用 DKIM 簽章
dkimSigning: ses.DkimSigning.enabled(),
// 設定郵件來源
mailFromDomain: `mail.${domain}`,
// 設定 MX 失敗時的行為
behaviorOnMxFailure: ses.BehaviorOnMxFailure.REJECT_MESSAGE,
});
// 輸出 DKIM 記錄(如果需要手動設定)
new cdk.CfnOutput(this, 'DomainIdentityArn', {
value: emailIdentity.emailIdentityArn,
description: 'SES Domain Identity ARN',
});
// 建立組態集用於追蹤
const configurationSet = new ses.ConfigurationSet(this, 'ConfigSet', {
configurationSetName: 'kyo-production',
// 啟用聲譽指標
reputationMetrics: true,
// 發送功能
sendingEnabled: true,
// 自訂重定向域名
suppressionReasons: ses.SuppressionReasons.BOUNCES_AND_COMPLAINTS,
});
// SNS Topic 用於接收事件
const snsTopic = new sns.Topic(this, 'SESEventsTopic', {
topicName: 'ses-events',
displayName: 'SES Events Topic',
});
// 建立事件目的地
configurationSet.addEventDestination('SNSEventDestination', {
destination: ses.EventDestination.snsTopic(snsTopic),
events: [
ses.EmailSendingEvent.SEND,
ses.EmailSendingEvent.DELIVERY,
ses.EmailSendingEvent.BOUNCE,
ses.EmailSendingEvent.COMPLAINT,
ses.EmailSendingEvent.REJECT,
ses.EmailSendingEvent.OPEN,
ses.EmailSendingEvent.CLICK,
],
enabled: true,
});
// 輸出組態集名稱
new cdk.CfnOutput(this, 'ConfigurationSetName', {
value: configurationSet.configurationSetName,
description: 'SES Configuration Set Name',
});
new cdk.CfnOutput(this, 'SNSTopicArn', {
value: snsTopic.topicArn,
description: 'SNS Topic ARN for SES events',
});
}
}
#!/bin/bash
# scripts/setup-ses-dns.sh
# 這個腳本會輸出需要在 DNS 設定的記錄
DOMAIN="kyong.com"
REGION="ap-northeast-1"
echo "=== SES DNS 設定指南 for ${DOMAIN} ==="
echo ""
# 1. 域名驗證記錄
echo "1. 域名驗證 TXT 記錄"
echo " 類型: TXT"
echo " 名稱: _amazonses.${DOMAIN}"
echo " 值: [從 SES Console 取得]"
echo ""
# 2. DKIM 記錄
echo "2. DKIM CNAME 記錄 (3 筆)"
echo " SES 會提供 3 個 DKIM token,需分別建立 CNAME:"
echo ""
echo " 類型: CNAME"
echo " 名稱: <token1>._domainkey.${DOMAIN}"
echo " 值: <token1>.dkim.amazonses.com"
echo ""
echo " 類型: CNAME"
echo " 名稱: <token2>._domainkey.${DOMAIN}"
echo " 值: <token2>.dkim.amazonses.com"
echo ""
echo " 類型: CNAME"
echo " 名稱: <token3>._domainkey.${DOMAIN}"
echo " 值: <token3>.dkim.amazonses.com"
echo ""
# 3. SPF 記錄
echo "3. SPF TXT 記錄"
echo " 類型: TXT"
echo " 名稱: ${DOMAIN}"
echo " 值: \"v=spf1 include:amazonses.com ~all\""
echo ""
# 4. DMARC 記錄
echo "4. DMARC TXT 記錄 (建議)"
echo " 類型: TXT"
echo " 名稱: _dmarc.${DOMAIN}"
echo " 值: \"v=DMARC1; p=quarantine; rua=mailto:dmarc@${DOMAIN}\""
echo ""
# 5. 自訂 MAIL FROM 域名
echo "5. 自訂 MAIL FROM (MX 和 SPF)"
echo " MX 記錄:"
echo " 類型: MX"
echo " 名稱: mail.${DOMAIN}"
echo " 值: 10 feedback-smtp.${REGION}.amazonses.com"
echo ""
echo " SPF 記錄:"
echo " 類型: TXT"
echo " 名稱: mail.${DOMAIN}"
echo " 值: \"v=spf1 include:amazonses.com ~all\""
echo ""
# 驗證指令
echo "=== 驗證 DNS 設定 ==="
echo ""
echo "# 驗證域名驗證記錄"
echo "dig TXT _amazonses.${DOMAIN} +short"
echo ""
echo "# 驗證 SPF"
echo "dig TXT ${DOMAIN} +short | grep spf"
echo ""
echo "# 驗證 DKIM"
echo "dig CNAME <token1>._domainkey.${DOMAIN} +short"
echo ""
echo "# 驗證 DMARC"
echo "dig TXT _dmarc.${DOMAIN} +short"
echo ""
echo "# 驗證 MX"
echo "dig MX mail.${DOMAIN} +short"
// scripts/request-production-access.ts
/**
* AWS SES 移出沙盒申請指南
*
* 步驟 1: 前往 AWS Support Center
* https://console.aws.amazon.com/support/home#/case/create
*
* 步驟 2: 建立案例
* - Service: SES Sending Limits Increase
* - Category: Service Limit Increase
*
* 步驟 3: 填寫申請資訊
*/
interface ProductionAccessRequest {
// 1. 基本資訊
region: string; // 例如: ap-northeast-1
mailType: 'Transactional' | 'Marketing' | 'Mixed';
website: string;
// 2. 用途說明
useCase: string;
// 範例:
// "We provide an OTP (One-Time Password) service for enterprise customers.
// Our service sends transactional emails including:
// - OTP verification codes
// - Account activation emails
// - Password reset notifications
// We use a double opt-in process and maintain strict bounce/complaint management."
// 3. 退信處理流程
bounceHandling: string;
// 範例:
// "We monitor bounce notifications via SNS and:
// 1. Hard bounces: Immediately remove from list
// 2. Soft bounces: Retry up to 3 times, then remove
// 3. We maintain bounce rate < 5%"
// 4. 投訴處理流程
complaintHandling: string;
// 範例:
// "We monitor complaint notifications via SNS and:
// 1. Immediately unsubscribe complainants
// 2. Investigate root cause
// 3. We maintain complaint rate < 0.1%"
// 5. 郵件列表來源
listAcquisition: string;
// 範例:
// "All email addresses are:
// - Directly provided by users during registration
// - Verified via double opt-in
// - Never purchased or scraped"
// 6. 請求配額
dailyQuota: number; // 例如: 50000
sendingRate: number; // 例如: 14 (每秒)
// 7. 合規聲明
compliance: {
canSpamAct: boolean; // 美國 CAN-SPAM Act
gdpr: boolean; // 歐盟 GDPR
optOutMechanism: boolean; // 有退訂機制
};
}
const exampleRequest: ProductionAccessRequest = {
region: 'ap-northeast-1',
mailType: 'Transactional',
website: 'https://kyong.com',
useCase: `
We are building an enterprise OTP (One-Time Password) service platform.
Our service sends transactional emails to verified users, including:
- OTP verification codes for 2FA
- Account activation confirmations
- Password reset links
- Service notifications
We implement best practices:
- Double opt-in verification
- Automated bounce/complaint monitoring via SNS
- Strict list hygiene (bounce < 5%, complaint < 0.1%)
- Unsubscribe links in all emails
- SPF, DKIM, and DMARC authentication
We expect to send approximately 30,000 transactional emails per day initially,
growing to 50,000 within 3 months as we onboard more enterprise customers.
`.trim(),
bounceHandling: `
We have implemented automated bounce handling:
1. SNS topic subscribed to bounce notifications
2. Hard bounces: Immediately flagged and excluded from future sends
3. Soft bounces: Retry with exponential backoff (max 3 attempts)
4. Automated alerts when bounce rate exceeds 3%
5. Weekly bounce report review
6. Target bounce rate: < 2%
`.trim(),
complaintHandling: `
We take complaints very seriously:
1. SNS topic subscribed to complaint notifications
2. Immediate automatic unsubscribe upon complaint
3. Root cause investigation within 24 hours
4. Sender training if complaint is due to sending practices
5. Monthly complaint rate review
6. Target complaint rate: < 0.05%
`.trim(),
listAcquisition: `
All recipient email addresses are obtained through:
1. Direct user registration on our platform
2. Double opt-in verification process
3. Explicit consent for transactional emails
4. We NEVER purchase, rent, or scrape email lists
5. Users can manage preferences and opt-out anytime
6. Regular list cleaning to remove inactive addresses
`.trim(),
dailyQuota: 50000,
sendingRate: 14,
compliance: {
canSpamAct: true,
gdpr: true,
optOutMechanism: true,
},
};
// 輸出申請文本
console.log('=== AWS SES Production Access Request ===\n');
console.log('Region:', exampleRequest.region);
console.log('Mail Type:', exampleRequest.mailType);
console.log('Website:', exampleRequest.website);
console.log('\n=== Use Case ===');
console.log(exampleRequest.useCase);
console.log('\n=== Bounce Handling ===');
console.log(exampleRequest.bounceHandling);
console.log('\n=== Complaint Handling ===');
console.log(exampleRequest.complaintHandling);
console.log('\n=== List Acquisition ===');
console.log(exampleRequest.listAcquisition);
console.log('\n=== Requested Limits ===');
console.log(`Daily Quota: ${exampleRequest.dailyQuota} emails/day`);
console.log(`Sending Rate: ${exampleRequest.sendingRate} emails/second`);
/**
* 郵件送達率優化完全指南
*
* 1. 技術層面 (Technical)
* ✅ SPF 記錄設定
* ✅ DKIM 簽章啟用
* ✅ DMARC 政策設定
* ✅ 自訂 MAIL FROM 域名
* ✅ 使用 Dedicated IP (大量發送)
*
* 2. 內容層面 (Content)
* ✅ 避免垃圾郵件觸發詞
* ❌ "FREE", "!!!", "CLICK HERE NOW"
* ✅ 專業、清晰、有價值的內容
* ✅ 平衡文字與圖片比例 (60:40)
* ✅ 提供純文字版本
* ✅ 清晰的退訂連結
* ✅ 實體地址 (法規要求)
*
* 3. 列表管理 (List Hygiene)
* ✅ Double Opt-in 驗證
* ✅ 定期清理無效地址
* ✅ 移除退信地址 (Hard Bounce)
* ✅ 尊重投訴與退訂
* ✅ 區隔活躍/不活躍用戶
*
* 4. 發送策略 (Sending Strategy)
* ✅ IP 預熱 (Warming)
* ✅ 避免流量突增
* ✅ 一致的發送模式
* ✅ 監控關鍵指標
*
* 5. 監控指標 (Metrics)
* 📊 Open Rate: 15-25% (正常)
* 📊 Click Rate: 2-5% (正常)
* 📊 Bounce Rate: < 2% (健康)
* 📊 Complaint Rate: < 0.1% (健康)
* 📊 Unsubscribe Rate: < 0.5% (健康)
*/
interface EmailHealthMetrics {
bounceRate: number; // 退信率
complaintRate: number; // 投訴率
openRate: number; // 開信率
clickRate: number; // 點擊率
unsubscribeRate: number; // 退訂率
}
function evaluateEmailHealth(metrics: EmailHealthMetrics): {
status: 'healthy' | 'warning' | 'critical';
issues: string[];
recommendations: string[];
} {
const issues: string[] = [];
const recommendations: string[] = [];
// 檢查退信率
if (metrics.bounceRate > 5) {
issues.push(`High bounce rate: ${metrics.bounceRate.toFixed(2)}%`);
recommendations.push('Implement stricter email validation');
recommendations.push('Remove hard bounces immediately');
} else if (metrics.bounceRate > 2) {
issues.push(`Elevated bounce rate: ${metrics.bounceRate.toFixed(2)}%`);
recommendations.push('Review email list quality');
}
// 檢查投訴率
if (metrics.complaintRate > 0.1) {
issues.push(`High complaint rate: ${metrics.complaintRate.toFixed(3)}%`);
recommendations.push('Review email content and frequency');
recommendations.push('Ensure clear unsubscribe option');
}
// 檢查開信率
if (metrics.openRate < 10) {
issues.push(`Low open rate: ${metrics.openRate.toFixed(2)}%`);
recommendations.push('Improve subject lines');
recommendations.push('Optimize send time');
recommendations.push('Review sender reputation');
}
// 判斷整體健康狀態
let status: 'healthy' | 'warning' | 'critical';
if (metrics.bounceRate > 5 || metrics.complaintRate > 0.1) {
status = 'critical';
} else if (
metrics.bounceRate > 2 ||
metrics.complaintRate > 0.05 ||
metrics.openRate < 10
) {
status = 'warning';
} else {
status = 'healthy';
}
return { status, issues, recommendations };
}
// 使用範例
const currentMetrics: EmailHealthMetrics = {
bounceRate: 1.5,
complaintRate: 0.03,
openRate: 22.5,
clickRate: 3.2,
unsubscribeRate: 0.2,
};
const health = evaluateEmailHealth(currentMetrics);
console.log('Email Health Status:', health.status);
if (health.issues.length > 0) {
console.log('Issues:', health.issues);
console.log('Recommendations:', health.recommendations);
}
// scripts/ip-warming-plan.ts
/**
* Dedicated IP 預熱計劃
*
* 為什麼需要預熱?
* - 新 IP 沒有發送歷史
* - ISP 會觀察發送模式
* - 突然大量發送會被視為垃圾郵件
*
* 預熱目標:
* - 建立良好的發送信譽
* - 逐步增加發送量
* - 保持低退信/投訴率
*/
interface WarmupSchedule {
day: number;
dailyVolume: number;
notes: string;
}
const dedicatedIPWarmupPlan: WarmupSchedule[] = [
{ day: 1, dailyVolume: 50, notes: '第一天,發送給最活躍用戶' },
{ day: 2, dailyVolume: 100, notes: '雙倍發送量' },
{ day: 3, dailyVolume: 200, notes: '持續增加' },
{ day: 4, dailyVolume: 400, notes: '保持穩定增長' },
{ day: 5, dailyVolume: 800, notes: '持續增長' },
{ day: 6, dailyVolume: 1600, notes: '一週後達到 1600' },
{ day: 7, dailyVolume: 3200, notes: '繼續增長' },
{ day: 8, dailyVolume: 6000, notes: '進入第二週' },
{ day: 9, dailyVolume: 10000, notes: '達到五位數' },
{ day: 10, dailyVolume: 15000, notes: '穩定增長' },
{ day: 11, dailyVolume: 20000, notes: '接近目標' },
{ day: 12, dailyVolume: 25000, notes: '繼續增長' },
{ day: 13, dailyVolume: 30000, notes: '接近預期量' },
{ day: 14, dailyVolume: 40000, notes: '兩週完成' },
{ day: 15, dailyVolume: 50000, notes: '達到預期每日發送量' },
];
/**
* 預熱最佳實踐
*/
const warmupBestPractices = [
'✅ 從最活躍、最近互動的用戶開始',
'✅ 每天增加 50-100%,不要突然跳躍',
'✅ 保持一致的發送時間',
'✅ 密切監控退信和投訴率',
'✅ 如果指標異常,放慢增長速度',
'✅ 使用高品質內容,確保高開信率',
'✅ 預熱期間避免促銷郵件',
'❌ 不要在預熱期間暫停',
'❌ 不要突然大量增加發送',
'❌ 不要忽略負面指標',
];
// 輸出預熱計劃
console.log('=== Dedicated IP Warming Plan ===\n');
console.log('Day | Daily Volume | Cumulative | Notes');
console.log('----|--------------|------------|------');
let cumulative = 0;
dedicatedIPWarmupPlan.forEach((schedule) => {
cumulative += schedule.dailyVolume;
console.log(
`${schedule.day.toString().padStart(3)} | ${schedule.dailyVolume
.toString()
.padStart(12)} | ${cumulative.toString().padStart(10)} | ${schedule.notes}`
);
});
console.log('\n=== Best Practices ===');
warmupBestPractices.forEach((practice) => console.log(practice));
// apps/kyo-otp-service/src/webhooks/ses-events.ts
import { FastifyPluginAsync } from 'fastify';
import { z } from 'zod';
const sesEventSchema = z.object({
Type: z.string(),
MessageId: z.string(),
Message: z.string(),
Timestamp: z.string(),
Signature: z.string(),
SigningCertURL: z.string(),
UnsubscribeURL: z.string(),
});
const sesBounceSchema = z.object({
eventType: z.literal('Bounce'),
bounce: z.object({
bounceType: z.enum(['Undetermined', 'Permanent', 'Transient']),
bounceSubType: z.string(),
bouncedRecipients: z.array(
z.object({
emailAddress: z.string(),
action: z.string().optional(),
status: z.string().optional(),
diagnosticCode: z.string().optional(),
})
),
timestamp: z.string(),
feedbackId: z.string(),
}),
mail: z.object({
messageId: z.string(),
timestamp: z.string(),
source: z.string(),
destination: z.array(z.string()),
}),
});
const sesComplaintSchema = z.object({
eventType: z.literal('Complaint'),
complaint: z.object({
complainedRecipients: z.array(
z.object({
emailAddress: z.string(),
})
),
timestamp: z.string(),
feedbackId: z.string(),
complaintFeedbackType: z.string().optional(),
}),
mail: z.object({
messageId: z.string(),
timestamp: z.string(),
source: z.string(),
}),
});
const sesDeliverySchema = z.object({
eventType: z.literal('Delivery'),
delivery: z.object({
timestamp: z.string(),
processingTimeMillis: z.number(),
recipients: z.array(z.string()),
smtpResponse: z.string(),
}),
mail: z.object({
messageId: z.string(),
}),
});
export const sesEventsRoutes: FastifyPluginAsync = async (server) => {
/**
* SNS 訂閱確認
*/
server.post('/ses/events', async (request, reply) => {
const body = sesEventSchema.parse(request.body);
// 處理訂閱確認
if (body.Type === 'SubscriptionConfirmation') {
const confirmUrl = body.UnsubscribeURL;
await fetch(confirmUrl);
server.log.info('SNS subscription confirmed');
return reply.code(200).send({ message: 'Subscription confirmed' });
}
// 處理通知
if (body.Type === 'Notification') {
const message = JSON.parse(body.Message);
switch (message.eventType) {
case 'Bounce':
await handleBounce(sesBounceSchema.parse(message));
break;
case 'Complaint':
await handleComplaint(sesComplaintSchema.parse(message));
break;
case 'Delivery':
await handleDelivery(sesDeliverySchema.parse(message));
break;
case 'Open':
await handleOpen(message);
break;
case 'Click':
await handleClick(message);
break;
default:
server.log.warn(`Unknown SES event type: ${message.eventType}`);
}
return reply.code(200).send({ message: 'Event processed' });
}
return reply.code(400).send({ error: 'Unknown message type' });
});
/**
* 處理退信
*/
async function handleBounce(event: z.infer<typeof sesBounceSchema>) {
server.log.info('Processing bounce event', {
messageId: event.mail.messageId,
bounceType: event.bounce.bounceType,
});
for (const recipient of event.bounce.bouncedRecipients) {
const email = recipient.emailAddress;
if (event.bounce.bounceType === 'Permanent') {
// 永久退信:立即從列表移除
await markEmailAsInvalid(email, 'hard_bounce');
server.log.warn(`Hard bounce: ${email} - removed from list`);
} else if (event.bounce.bounceType === 'Transient') {
// 暫時性退信:標記並限制重試
await incrementBounceCount(email);
server.log.info(`Soft bounce: ${email} - retry limited`);
}
// 記錄事件
await logEmailEvent({
type: 'bounce',
email,
messageId: event.mail.messageId,
metadata: {
bounceType: event.bounce.bounceType,
bounceSubType: event.bounce.bounceSubType,
diagnosticCode: recipient.diagnosticCode,
},
timestamp: new Date(event.bounce.timestamp),
});
}
}
/**
* 處理投訴
*/
async function handleComplaint(event: z.infer<typeof sesComplaintSchema>) {
server.log.warn('Processing complaint event', {
messageId: event.mail.messageId,
});
for (const recipient of event.complaint.complainedRecipients) {
const email = recipient.emailAddress;
// 立即退訂
await unsubscribeEmail(email, 'complaint');
// 標記為投訴
await markEmailAsComplaint(email);
server.log.error(`Complaint received: ${email} - unsubscribed`);
// 記錄事件
await logEmailEvent({
type: 'complaint',
email,
messageId: event.mail.messageId,
metadata: {
feedbackType: event.complaint.complaintFeedbackType,
},
timestamp: new Date(event.complaint.timestamp),
});
// 發送告警
await sendAlert({
type: 'complaint',
email,
messageId: event.mail.messageId,
});
}
}
/**
* 處理成功送達
*/
async function handleDelivery(event: z.infer<typeof sesDeliverySchema>) {
for (const email of event.delivery.recipients) {
await logEmailEvent({
type: 'delivered',
email,
messageId: event.mail.messageId,
metadata: {
processingTime: event.delivery.processingTimeMillis,
smtpResponse: event.delivery.smtpResponse,
},
timestamp: new Date(event.delivery.timestamp),
});
}
}
/**
* 處理開信
*/
async function handleOpen(event: any) {
await logEmailEvent({
type: 'open',
email: event.mail.destination[0],
messageId: event.mail.messageId,
metadata: {
userAgent: event.open.userAgent,
ipAddress: event.open.ipAddress,
},
timestamp: new Date(event.open.timestamp),
});
}
/**
* 處理點擊
*/
async function handleClick(event: any) {
await logEmailEvent({
type: 'click',
email: event.mail.destination[0],
messageId: event.mail.messageId,
metadata: {
link: event.click.link,
userAgent: event.click.userAgent,
},
timestamp: new Date(event.click.timestamp),
});
}
// 輔助函數(實際應連接資料庫)
async function markEmailAsInvalid(email: string, reason: string) {
// TODO: Update database
server.log.info(`Marking email as invalid: ${email} (${reason})`);
}
async function incrementBounceCount(email: string) {
// TODO: Update database
server.log.info(`Incrementing bounce count: ${email}`);
}
async function unsubscribeEmail(email: string, reason: string) {
// TODO: Update database
server.log.info(`Unsubscribing email: ${email} (${reason})`);
}
async function markEmailAsComplaint(email: string) {
// TODO: Update database
server.log.warn(`Marking email as complaint: ${email}`);
}
async function logEmailEvent(event: any) {
// TODO: Store in database
server.log.info('Email event logged', event);
}
async function sendAlert(alert: any) {
// TODO: Send to monitoring system
server.log.error('Alert triggered', alert);
}
};
/**
* AWS SES 成本分析 (ap-northeast-1 Tokyo)
*
* 基礎定價:
* - 前 62,000 封/月:$0(AWS Free Tier)
* - 之後:$0.10 / 1,000 封
* - 附件:$0.12 / GB
* - Dedicated IP:$24.95 / 月
* - 接收郵件:$0.10 / 1,000 封
*
* 成本範例:
*/
interface SESCostEstimate {
monthlyVolume: number;
attachmentsGB?: number;
dedicatedIP?: boolean;
}
function calculateSESCost(config: SESCostEstimate): {
emailCost: number;
attachmentCost: number;
dedicatedIPCost: number;
totalMonthlyCost: number;
} {
const FREE_TIER = 62000;
const COST_PER_1000 = 0.1;
const ATTACHMENT_COST_PER_GB = 0.12;
const DEDICATED_IP_COST = 24.95;
// 郵件發送成本
const billableEmails = Math.max(0, config.monthlyVolume - FREE_TIER);
const emailCost = (billableEmails / 1000) * COST_PER_1000;
// 附件成本
const attachmentCost = (config.attachmentsGB || 0) * ATTACHMENT_COST_PER_GB;
// Dedicated IP 成本
const dedicatedIPCost = config.dedicatedIP ? DEDICATED_IP_COST : 0;
const totalMonthlyCost = emailCost + attachmentCost + dedicatedIPCost;
return {
emailCost,
attachmentCost,
dedicatedIPCost,
totalMonthlyCost,
};
}
// 範例 1: 小型應用(免費額度內)
const smallApp = calculateSESCost({
monthlyVolume: 50000,
dedicatedIP: false,
});
console.log('=== Small App (50K emails/month) ===');
console.log('Email Cost: $' + smallApp.emailCost.toFixed(2));
console.log('Total: $' + smallApp.totalMonthlyCost.toFixed(2));
console.log('');
// 範例 2: 中型應用
const mediumApp = calculateSESCost({
monthlyVolume: 500000,
attachmentsGB: 5,
dedicatedIP: false,
});
console.log('=== Medium App (500K emails/month) ===');
console.log('Email Cost: $' + mediumApp.emailCost.toFixed(2));
console.log('Attachment Cost: $' + mediumApp.attachmentCost.toFixed(2));
console.log('Total: $' + mediumApp.totalMonthlyCost.toFixed(2));
console.log('');
// 範例 3: 大型應用(使用 Dedicated IP)
const largeApp = calculateSESCost({
monthlyVolume: 2000000,
attachmentsGB: 20,
dedicatedIP: true,
});
console.log('=== Large App (2M emails/month with Dedicated IP) ===');
console.log('Email Cost: $' + largeApp.emailCost.toFixed(2));
console.log('Attachment Cost: $' + largeApp.attachmentCost.toFixed(2));
console.log('Dedicated IP Cost: $' + largeApp.dedicatedIPCost.toFixed(2));
console.log('Total: $' + largeApp.totalMonthlyCost.toFixed(2));
console.log('');
/**
* 成本優化策略:
*
* 1. 利用 Free Tier
* - 前 62,000 封免費
* - 多帳號策略(不推薦,違反 ToS)
*
* 2. 批次發送
* - 減少 API 調用次數
* - 降低網路開銷
*
* 3. 移除無效地址
* - 減少浪費
* - 提升送達率
*
* 4. 附件優化
* - 使用外部連結代替附件
* - 壓縮附件檔案
*
* 5. Dedicated IP 評估
* - 僅在發送量 > 100K/day 時考慮
* - 需要 IP 預熱
* - 每月額外 $24.95
*/
我們今天完成了 AWS SES 的完整整合:
Shared IP vs Dedicated IP:
退信類型處理:
DKIM vs SPF vs DMARC: