feat: smtp

This commit is contained in:
ggyy
2026-04-23 18:52:50 +08:00
parent 31b3f4d700
commit 8079b66d43
6 changed files with 65 additions and 4 deletions

View File

@@ -24,6 +24,7 @@
"vike-photon": "^0.1.24",
"vike-vue": "^0.9.11",
"vue": "^3.5.30",
"worker-mailer": "^1.2.1",
},
"devDependencies": {
"@tailwindcss/vite": "^4.2.1",
@@ -1093,6 +1094,8 @@
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"worker-mailer": ["worker-mailer@1.2.1", "", {}, "sha512-gS2ei/mrpRqNs+AHmqxhT6vFPwCLw2qnz5ShmyGD0ULaU0Q9hxnFAcx9jhAip/MnD6+MjgnQu6hQQgA8mlOkVA=="],
"workerd": ["workerd@1.20260401.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260401.1", "@cloudflare/workerd-darwin-arm64": "1.20260401.1", "@cloudflare/workerd-linux-64": "1.20260401.1", "@cloudflare/workerd-linux-arm64": "1.20260401.1", "@cloudflare/workerd-windows-64": "1.20260401.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-mUYCd+ohaWJWF5nhDzxugWaAD/DM8Dw0ze3B7bu8JaA7S70+XQJXcvcvwE8C4qGcxSdCyqjsrFzqxKubECDwzg=="],
"wrangler": ["wrangler@4.80.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.2", "@cloudflare/unenv-preset": "2.16.0", "blake3-wasm": "2.1.5", "esbuild": "0.27.3", "miniflare": "4.20260401.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260401.1" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260401.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-2ZKF7uPeOZy65BGk3YfvqBCPo/xH1MrAlMmH9mVP+tCNBrTUMnwOHSj1HrZHgR8LttkAqhko0fGz+I4ax1rzyQ=="],

View File

@@ -1,5 +1,5 @@
import { badRequestError, externalServiceError } from "../../lib/app-error";
import type { EmailApiConfigValue, EmailProviderAdapter, EmailSendInput } from "./types";
import type { EmailApiConfigValue, EmailProviderAdapter, EmailSendInput, EmailSmtpConfigValue } from "./types";
function normalizeBaseUrl(value: string) {
return value.replace(/\/+$/, "");
@@ -102,6 +102,39 @@ async function sendMailjetEmail(config: EmailApiConfigValue, input: EmailSendInp
};
}
export function createSmtpEmailAdapter(config: EmailSmtpConfigValue): EmailProviderAdapter {
return {
async send(input) {
if (!config.smtpHost || !config.smtpPort) {
throw badRequestError("SMTP 配置不完整", "SMTP_CONFIG_INCOMPLETE");
}
const { WorkerMailer } = await import("worker-mailer");
await WorkerMailer.send(
{
host: config.smtpHost,
port: config.smtpPort,
secure: config.smtpSecure ?? false,
credentials: config.smtpUsername
? { username: config.smtpUsername, password: config.smtpPassword ?? "" }
: undefined,
authType: config.smtpAuthType ?? "plain",
},
{
from: { email: config.fromEmail, name: config.fromName },
to: input.toEmail,
reply: input.replyTo || config.replyTo || undefined,
subject: input.subject,
text: input.text,
html: input.html,
},
);
return {};
},
};
}
export function createApiEmailAdapter(config: EmailApiConfigValue): EmailProviderAdapter {
return {
async send(input) {

View File

@@ -5,7 +5,7 @@ import { logger } from "../../lib/logger";
import { validateEmailConfigInput, validateEmailTemplateInput, validateTestEmailInput } from "../../lib/validators/email";
import { getAdminContext, logAdminOperation } from "../auth/service";
import { getSiteSetting } from "../site/service";
import { createApiEmailAdapter } from "./provider";
import { createApiEmailAdapter, createSmtpEmailAdapter } from "./provider";
import {
activateEmailConfigById,
createEmailConfigRecord,
@@ -270,7 +270,19 @@ async function sendByChannel(config: EmailConfigValue, input: {
}
if (config.provider === "SMTP") {
throw badRequestError("当前版本暂未在 Worker 中实现 SMTP 直连发送,请先使用 API 分类", "SMTP_NOT_IMPLEMENTED");
const adapter = createSmtpEmailAdapter(config);
const result = await adapter.send({
toEmail: input.toEmail,
subject: input.subject,
text: input.text,
html: input.html,
replyTo: config.replyTo,
});
return {
provider: config.provider,
apiProvider: undefined,
messageId: result.messageId,
};
}
throw badRequestError("当前版本暂未接入 CloudFlare Email Send binding 的运行时发送,请先使用 API 分类", "CLOUDFLARE_NOT_IMPLEMENTED");

View File

@@ -41,6 +41,7 @@ export interface EmailSmtpConfigValue extends EmailPushFlags {
smtpSecure?: boolean;
smtpUsername?: string;
smtpPassword?: string;
smtpAuthType?: "plain" | "login" | "cram-md5";
}
export interface EmailCloudflareConfigValue extends EmailPushFlags {

View File

@@ -35,7 +35,8 @@
"vike": "^0.4.255",
"vike-photon": "^0.1.24",
"vike-vue": "^0.9.11",
"vue": "^3.5.30"
"vue": "^3.5.30",
"worker-mailer": "^1.2.1"
},
"devDependencies": {
"@tailwindcss/vite": "^4.2.1",

View File

@@ -254,6 +254,13 @@
<label class="flex flex-col gap-1.5">
<span class="label-text font-medium">SMTP 密码</span>
<input v-model="configForm.smtpPassword" class="input input-bordered w-full" />
</label><label class="flex flex-col gap-1.5">
<span class="label-text font-medium">认证方式</span>
<select v-model="configForm.smtpAuthType" class="select select-bordered w-full">
<option value="plain">PLAIN</option>
<option value="login">LOGIN</option>
<option value="cram-md5">CRAM-MD5</option>
</select>
</label>
</div>
<label class="label cursor-pointer justify-start gap-3 w-fit">
@@ -574,6 +581,7 @@ interface ConfigFormState {
smtpSecure: boolean;
smtpUsername: string;
smtpPassword: string;
smtpAuthType: "plain" | "login" | "cram-md5";
// Cloudflare
cloudflareBindingName: string;
cloudflareDestinationAddress: string;
@@ -597,6 +605,7 @@ function createEmptyForm(): ConfigFormState {
smtpSecure: false,
smtpUsername: "",
smtpPassword: "",
smtpAuthType: "plain" as "plain" | "login" | "cram-md5",
cloudflareBindingName: "",
cloudflareDestinationAddress: "",
cloudflareAllowedText: "",
@@ -631,6 +640,7 @@ function openEditDialog(item: MailboxItem) {
smtpSecure: (item as any).smtpSecure || false,
smtpUsername: (item as any).smtpUsername || "",
smtpPassword: (item as any).smtpPassword || "",
smtpAuthType: (item as any).smtpAuthType || "plain",
cloudflareBindingName: (item as any).cloudflareBindingName || "",
cloudflareDestinationAddress: (item as any).cloudflareDestinationAddress || "",
cloudflareAllowedText: Array.isArray((item as any).cloudflareAllowedDestinationAddresses)
@@ -770,6 +780,7 @@ async function handleSaveConfig() {
payload.smtpSecure = configForm.smtpSecure;
payload.smtpUsername = configForm.smtpUsername;
payload.smtpPassword = configForm.smtpPassword;
payload.smtpAuthType = configForm.smtpAuthType;
} else {
payload.cloudflareBindingName = configForm.cloudflareBindingName;
payload.cloudflareDestinationAddress = configForm.cloudflareDestinationAddress;