feat: add Cloudflare Email Sending support

This commit is contained in:
eoao
2026-05-10 13:42:03 +08:00
parent 1f898e8f65
commit db0e930bd1
4 changed files with 135 additions and 25 deletions

View File

@@ -21,6 +21,7 @@ const en = {
totalSendLack: 'Not enough total remaining sends', totalSendLack: 'Not enough total remaining sends',
senderAccountNotExist: 'Sender email does not exist', senderAccountNotExist: 'Sender email does not exist',
noResendToken: 'Resend API token not configured', noResendToken: 'Resend API token not configured',
noSendProvider: 'Email sending service is not configured',
sendEmailNotCurUser: 'Sender email does not belong to current user', sendEmailNotCurUser: 'Sender email does not belong to current user',
notExistEmailReply: 'Mail does not exist and cannot be replied to', notExistEmailReply: 'Mail does not exist and cannot be replied to',
imageAttLimit: 'The maximum number of image attachments is 10', imageAttLimit: 'The maximum number of image attachments is 10',

View File

@@ -21,6 +21,7 @@ const zh = {
totalSendLack: '剩余发送次数不足', totalSendLack: '剩余发送次数不足',
senderAccountNotExist: '发件人邮箱不存在', senderAccountNotExist: '发件人邮箱不存在',
noResendToken: 'Resend未配置只能给站内邮箱发件', noResendToken: 'Resend未配置只能给站内邮箱发件',
noSendProvider: '发信服务未配置,只能给站内邮箱发件',
sendEmailNotCurUser: '发件人邮箱非当前用户所有', sendEmailNotCurUser: '发件人邮箱非当前用户所有',
notExistEmailReply: '邮件不存在无法回复', notExistEmailReply: '邮件不存在无法回复',
imageAttLimit: '图片不能超过10个', imageAttLimit: '图片不能超过10个',

View File

@@ -160,7 +160,7 @@ const emailService = {
text, //邮件纯文本 text, //邮件纯文本
content, //邮件内容 content, //邮件内容
subject, //邮件标题 subject, //邮件标题
attachments //附件 attachments = [] //附件
} = params; } = params;
const { resendTokens, r2Domain, send, domainList } = await settingService.query(c); const { resendTokens, r2Domain, send, domainList } = await settingService.query(c);
@@ -230,10 +230,11 @@ const emailService = {
const domain = emailUtils.getDomain(accountRow.email); const domain = emailUtils.getDomain(accountRow.email);
const resendToken = resendTokens[domain]; const resendToken = resendTokens[domain];
const useCloudflareEmail = !!c.env.EMAIL;
//如果接收方存在站外邮箱,又没有resend token //如果接收方存在站外邮箱,又没有发信服务
if (!resendToken && !allInternal) { if (!useCloudflareEmail && !resendToken && !allInternal) {
throw new BizError(t('noResendToken')); throw new BizError(t('noSendProvider'));
} }
//没有发件人名字自动截取 //没有发件人名字自动截取
@@ -256,34 +257,40 @@ const emailService = {
} }
let resendResult = {}; let sendResult = {};
//存在站外邮箱全部由resend发送 //存在站外邮箱时,如果配置了 Cloudflare Email Service 就优先使用,否则使用 Resend
if (!allInternal) { if (!allInternal) {
const resend = new Resend(resendToken); if (useCloudflareEmail) {
sendResult = await this.sendByCloudflareEmail(c, {
const sendForm = { name,
from: `${name} <${accountRow.email}>`, accountEmail: accountRow.email,
to: [...receiveEmail], receiveEmail,
subject: subject, subject,
text: text, text,
html: html, html,
attachments: [...imageDataList, ...attachments] attachments: [...imageDataList, ...attachments],
}; sendType,
messageId: emailRow.messageId
if (sendType === 'reply') { });
sendForm.headers = { } else {
'in-reply-to': emailRow.messageId, sendResult = await this.sendByResend(resendToken, {
'references': emailRow.messageId 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) { if (error) {
@@ -367,6 +374,104 @@ const emailService = {
return [ emailResult ]; 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) { async HandleOnSiteEmail(c, receiveEmail, sendEmailData, attList) {

View File

@@ -19,6 +19,9 @@ enabled = true
#binding = "r2" #r2对象存储绑定名默认不可修改 #binding = "r2" #r2对象存储绑定名默认不可修改
#bucket_name = "" #r2对象存储桶的名字 #bucket_name = "" #r2对象存储桶的名字
#[[send_email]]
#name = "EMAIL" #配置后站外发信优先使用 Cloudflare Email Service
[ai] [ai]
binding = "AI" binding = "AI"