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',
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',

View File

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

View File

@@ -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) {

View File

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