feat: add Cloudflare Email Sending support
This commit is contained in:
@@ -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',
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const zh = {
|
|||||||
totalSendLack: '剩余发送次数不足',
|
totalSendLack: '剩余发送次数不足',
|
||||||
senderAccountNotExist: '发件人邮箱不存在',
|
senderAccountNotExist: '发件人邮箱不存在',
|
||||||
noResendToken: 'Resend未配置,只能给站内邮箱发件',
|
noResendToken: 'Resend未配置,只能给站内邮箱发件',
|
||||||
|
noSendProvider: '发信服务未配置,只能给站内邮箱发件',
|
||||||
sendEmailNotCurUser: '发件人邮箱非当前用户所有',
|
sendEmailNotCurUser: '发件人邮箱非当前用户所有',
|
||||||
notExistEmailReply: '邮件不存在无法回复',
|
notExistEmailReply: '邮件不存在无法回复',
|
||||||
imageAttLimit: '图片不能超过10个',
|
imageAttLimit: '图片不能超过10个',
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user