From db0e930bd1695330c34cd840e8c5a73e306de1af Mon Sep 17 00:00:00 2001 From: eoao Date: Sun, 10 May 2026 13:42:03 +0800 Subject: [PATCH] feat: add Cloudflare Email Sending support --- mail-worker/src/i18n/en.js | 1 + mail-worker/src/i18n/zh.js | 1 + mail-worker/src/service/email-service.js | 155 +++++++++++++++++++---- mail-worker/wrangler.toml | 3 + 4 files changed, 135 insertions(+), 25 deletions(-) diff --git a/mail-worker/src/i18n/en.js b/mail-worker/src/i18n/en.js index 38e5d06..a7de5c6 100644 --- a/mail-worker/src/i18n/en.js +++ b/mail-worker/src/i18n/en.js @@ -21,6 +21,7 @@ const en = { totalSendLack: 'Not enough total remaining sends', senderAccountNotExist: 'Sender email does not exist', noResendToken: 'Resend API token not configured', + noSendProvider: 'Email sending service is not configured', sendEmailNotCurUser: 'Sender email does not belong to current user', notExistEmailReply: 'Mail does not exist and cannot be replied to', imageAttLimit: 'The maximum number of image attachments is 10', diff --git a/mail-worker/src/i18n/zh.js b/mail-worker/src/i18n/zh.js index 1852346..a4ba083 100644 --- a/mail-worker/src/i18n/zh.js +++ b/mail-worker/src/i18n/zh.js @@ -21,6 +21,7 @@ const zh = { totalSendLack: '剩余发送次数不足', senderAccountNotExist: '发件人邮箱不存在', noResendToken: 'Resend未配置,只能给站内邮箱发件', + noSendProvider: '发信服务未配置,只能给站内邮箱发件', sendEmailNotCurUser: '发件人邮箱非当前用户所有', notExistEmailReply: '邮件不存在无法回复', imageAttLimit: '图片不能超过10个', diff --git a/mail-worker/src/service/email-service.js b/mail-worker/src/service/email-service.js index 7e522de..d70c4fe 100644 --- a/mail-worker/src/service/email-service.js +++ b/mail-worker/src/service/email-service.js @@ -160,7 +160,7 @@ const emailService = { text, //邮件纯文本 content, //邮件内容 subject, //邮件标题 - attachments //附件 + attachments = [] //附件 } = params; const { resendTokens, r2Domain, send, domainList } = await settingService.query(c); @@ -230,10 +230,11 @@ const emailService = { const domain = emailUtils.getDomain(accountRow.email); const resendToken = resendTokens[domain]; + const useCloudflareEmail = !!c.env.EMAIL; - //如果接收方存在站外邮箱,又没有resend token - if (!resendToken && !allInternal) { - throw new BizError(t('noResendToken')); + //如果接收方存在站外邮箱,又没有发信服务 + if (!useCloudflareEmail && !resendToken && !allInternal) { + throw new BizError(t('noSendProvider')); } //没有发件人名字自动截取 @@ -256,34 +257,40 @@ const emailService = { } - let resendResult = {}; + let sendResult = {}; - //存在站外时邮箱全部由resend发送 + //存在站外邮箱时,如果配置了 Cloudflare Email Service 就优先使用,否则使用 Resend if (!allInternal) { - const resend = new Resend(resendToken); - - const sendForm = { - from: `${name} <${accountRow.email}>`, - to: [...receiveEmail], - subject: subject, - text: text, - html: html, - attachments: [...imageDataList, ...attachments] - }; - - if (sendType === 'reply') { - sendForm.headers = { - 'in-reply-to': emailRow.messageId, - 'references': emailRow.messageId - }; + if (useCloudflareEmail) { + sendResult = await this.sendByCloudflareEmail(c, { + name, + accountEmail: accountRow.email, + receiveEmail, + subject, + text, + html, + attachments: [...imageDataList, ...attachments], + sendType, + messageId: emailRow.messageId + }); + } else { + sendResult = await this.sendByResend(resendToken, { + name, + accountEmail: accountRow.email, + receiveEmail, + subject, + text, + html, + attachments: [...imageDataList, ...attachments], + sendType, + messageId: emailRow.messageId + }); } - resendResult = await resend.emails.send(sendForm); - } - const { data, error } = resendResult; + const { data, error } = sendResult; if (error) { @@ -367,6 +374,104 @@ const emailService = { return [ emailResult ]; }, + async sendByCloudflareEmail(c, params) { + const sendForm = { + from: { email: params.accountEmail, name: params.name }, + to: [...params.receiveEmail], + subject: params.subject + }; + + if (params.text) { + sendForm.text = params.text; + } + + if (params.html) { + sendForm.html = params.html; + } + + const attachments = await this.toCloudflareAttachments(params.attachments); + if (attachments.length > 0) { + sendForm.attachments = attachments; + } + + if (params.sendType === 'reply' && params.messageId) { + sendForm.headers = { + 'in-reply-to': params.messageId, + 'references': params.messageId + }; + } + + const result = await c.env.EMAIL.send(sendForm); + + return { + data: { + id: result.messageId + } + }; + }, + + async sendByResend(resendToken, params) { + const resend = new Resend(resendToken); + + const sendForm = { + from: `${params.name} <${params.accountEmail}>`, + to: [...params.receiveEmail], + subject: params.subject, + text: params.text, + html: params.html, + attachments: params.attachments + }; + + if (params.sendType === 'reply') { + sendForm.headers = { + 'in-reply-to': params.messageId, + 'references': params.messageId + }; + } + + return await resend.emails.send(sendForm); + }, + + async toCloudflareAttachments(attachments) { + + const result = []; + + for (const attachment of attachments) { + let content = attachment.content; + + if (!content && attachment.path) { + const response = await fetch(attachment.path); + if (!response.ok) { + throw new BizError(`Attachment fetch failed: ${attachment.filename}`); + } + content = await response.arrayBuffer(); + } + + if (!content) { + continue; + } + + if (typeof content === 'string' && content.startsWith('data:')) { + content = content.split(',')[1] || content; + } + + const item = { + content, + filename: attachment.filename, + type: attachment.mimeType || attachment.contentType || attachment.type || 'application/octet-stream', + disposition: attachment.contentId ? 'inline' : 'attachment' + }; + + if (attachment.contentId) { + item.contentId = attachment.contentId.replace(/^<|>$/g, ''); + } + + result.push(item); + } + + return result; + }, + //处理站内邮件发送 async HandleOnSiteEmail(c, receiveEmail, sendEmailData, attList) { diff --git a/mail-worker/wrangler.toml b/mail-worker/wrangler.toml index d81ffa6..c3c9bef 100644 --- a/mail-worker/wrangler.toml +++ b/mail-worker/wrangler.toml @@ -19,6 +19,9 @@ enabled = true #binding = "r2" #r2对象存储绑定名默认不可修改 #bucket_name = "" #r2对象存储桶的名字 +#[[send_email]] +#name = "EMAIL" #配置后站外发信优先使用 Cloudflare Email Service + [ai] binding = "AI"