mirror of
https://github.com/amtoaer/bili-sync.git
synced 2026-10-16 21:12:47 +08:00
feat: 支持 SMTP 邮件通知 (#747)
This commit is contained in:
Generated
+82
-7
@@ -220,7 +220,7 @@ checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"axum-core",
|
"axum-core",
|
||||||
"axum-macros",
|
"axum-macros",
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@@ -291,6 +291,12 @@ version = "0.22.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.23.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64ct"
|
name = "base64ct"
|
||||||
version = "1.8.3"
|
version = "1.8.3"
|
||||||
@@ -326,7 +332,7 @@ dependencies = [
|
|||||||
"async-stream",
|
"async-stream",
|
||||||
"async-tempfile",
|
"async-tempfile",
|
||||||
"axum",
|
"axum",
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bili_sync_entity",
|
"bili_sync_entity",
|
||||||
"bili_sync_migration",
|
"bili_sync_migration",
|
||||||
"built",
|
"built",
|
||||||
@@ -345,6 +351,7 @@ dependencies = [
|
|||||||
"hex",
|
"hex",
|
||||||
"itertools",
|
"itertools",
|
||||||
"leaky-bucket",
|
"leaky-bucket",
|
||||||
|
"lettre",
|
||||||
"md5",
|
"md5",
|
||||||
"memchr",
|
"memchr",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -1123,6 +1130,22 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "email-encoding"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "420b9da095f052ea597503e39073b5b3c522f7db933fbac202d91d24492693fd"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.23.1",
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "email_address"
|
||||||
|
version = "0.2.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encoding_rs"
|
name = "encoding_rs"
|
||||||
version = "0.8.35"
|
version = "0.8.35"
|
||||||
@@ -1641,7 +1664,7 @@ version = "0.1.20"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@@ -1903,6 +1926,33 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lettre"
|
||||||
|
version = "0.11.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2c646bd5cc763b1087b15493e29a64be6147ba8f19342004fa52048ee596eae"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"base64 0.23.1",
|
||||||
|
"email-encoding",
|
||||||
|
"email_address",
|
||||||
|
"fastrand",
|
||||||
|
"futures-io",
|
||||||
|
"futures-util",
|
||||||
|
"httpdate",
|
||||||
|
"idna",
|
||||||
|
"mime",
|
||||||
|
"nom",
|
||||||
|
"percent-encoding",
|
||||||
|
"quoted_printable",
|
||||||
|
"rustls",
|
||||||
|
"socket2",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"url",
|
||||||
|
"webpki-roots",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.186"
|
version = "0.2.186"
|
||||||
@@ -2095,6 +2145,15 @@ dependencies = [
|
|||||||
"memoffset",
|
"memoffset",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nom"
|
||||||
|
version = "8.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ntapi"
|
name = "ntapi"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@@ -2646,6 +2705,12 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quoted_printable"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "478e0585659a122aa407eb7e3c0e1fa51b1d8a870038bd29f0cf4a8551eea972"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "r-efi"
|
name = "r-efi"
|
||||||
version = "5.3.0"
|
version = "5.3.0"
|
||||||
@@ -2804,7 +2869,7 @@ version = "0.13.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
|
checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"cookie",
|
"cookie",
|
||||||
"cookie_store",
|
"cookie_store",
|
||||||
@@ -2996,6 +3061,7 @@ version = "0.23.41"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f"
|
checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"ring",
|
"ring",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
@@ -3558,7 +3624,7 @@ version = "0.8.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
|
checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bigdecimal",
|
"bigdecimal",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -3636,7 +3702,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
|
checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atoi",
|
"atoi",
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bigdecimal",
|
"bigdecimal",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
@@ -3683,7 +3749,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
|
checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atoi",
|
"atoi",
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bigdecimal",
|
"bigdecimal",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
@@ -4566,6 +4632,15 @@ dependencies = [
|
|||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "1.0.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
|
||||||
|
dependencies = [
|
||||||
|
"rustls-pki-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "whoami"
|
name = "whoami"
|
||||||
version = "1.6.1"
|
version = "1.6.1"
|
||||||
|
|||||||
@@ -40,6 +40,13 @@ handlebars = "6.4.3"
|
|||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
itertools = "0.14.0"
|
itertools = "0.14.0"
|
||||||
leaky-bucket = "1.1.2"
|
leaky-bucket = "1.1.2"
|
||||||
|
lettre = { version = "0.11.23", default-features = false, features = [
|
||||||
|
"builder",
|
||||||
|
"smtp-transport",
|
||||||
|
"tokio1-rustls",
|
||||||
|
"ring",
|
||||||
|
"webpki-roots",
|
||||||
|
] }
|
||||||
md5 = "0.8.1"
|
md5 = "0.8.1"
|
||||||
memchr = "2.8.3"
|
memchr = "2.8.3"
|
||||||
once_cell = "1.21.4"
|
once_cell = "1.21.4"
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ handlebars = { workspace = true }
|
|||||||
hex = { workspace = true }
|
hex = { workspace = true }
|
||||||
itertools = { workspace = true }
|
itertools = { workspace = true }
|
||||||
leaky-bucket = { workspace = true }
|
leaky-bucket = { workspace = true }
|
||||||
|
lettre = { workspace = true }
|
||||||
md5 = { workspace = true }
|
md5 = { workspace = true }
|
||||||
memchr = { workspace = true }
|
memchr = { workspace = true }
|
||||||
once_cell = { workspace = true }
|
once_cell = { workspace = true }
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ mod message;
|
|||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::{Result, anyhow};
|
||||||
use futures::future;
|
use futures::future;
|
||||||
pub use info::DownloadNotifyInfo;
|
pub use info::DownloadNotifyInfo;
|
||||||
|
use lettre::message::{Attachment, MultiPart, SinglePart};
|
||||||
|
use lettre::transport::smtp::authentication::Credentials;
|
||||||
|
use lettre::{AsyncSmtpTransport, AsyncTransport, Tokio1Executor};
|
||||||
pub use message::Message;
|
pub use message::Message;
|
||||||
use reqwest::header;
|
use reqwest::header;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -30,6 +33,25 @@ pub enum Notifier {
|
|||||||
// 一个内部辅助字段,用于决定是否强制渲染当前模板,在测试时使用
|
// 一个内部辅助字段,用于决定是否强制渲染当前模板,在测试时使用
|
||||||
ignore_cache: Option<()>,
|
ignore_cache: Option<()>,
|
||||||
},
|
},
|
||||||
|
Smtp {
|
||||||
|
host: String,
|
||||||
|
port: u16,
|
||||||
|
encryption: SmtpEncryption,
|
||||||
|
#[serde(default)]
|
||||||
|
username: String,
|
||||||
|
#[serde(default)]
|
||||||
|
password: String,
|
||||||
|
from: String,
|
||||||
|
to: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum SmtpEncryption {
|
||||||
|
None,
|
||||||
|
Tls,
|
||||||
|
StartTls,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn webhook_template_key(url: &str) -> String {
|
pub fn webhook_template_key(url: &str) -> String {
|
||||||
@@ -110,6 +132,56 @@ impl Notifier {
|
|||||||
|
|
||||||
client.post(url).headers(headers_map).body(payload).send().await?;
|
client.post(url).headers(headers_map).body(payload).send().await?;
|
||||||
}
|
}
|
||||||
|
Notifier::Smtp {
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
encryption,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
} => {
|
||||||
|
let email = lettre::Message::builder()
|
||||||
|
.from(from.parse()?)
|
||||||
|
.to(to.parse()?)
|
||||||
|
.subject("BiliSync 通知");
|
||||||
|
let email = if let Some(image_url) = &message.image_url {
|
||||||
|
let response = client.get(image_url).send().await?.error_for_status()?;
|
||||||
|
let content_type = response
|
||||||
|
.headers()
|
||||||
|
.get(header::CONTENT_TYPE)
|
||||||
|
.ok_or_else(|| anyhow!("Missing image Content-Type"))?
|
||||||
|
.to_str()?
|
||||||
|
.parse()?;
|
||||||
|
let image = response.bytes().await?.to_vec();
|
||||||
|
let content_id = uuid::Uuid::new_v4().to_string();
|
||||||
|
let html = format!(
|
||||||
|
r#"<p>{}</p><img src="cid:{content_id}" style="max-width: 100%;" alt="">"#,
|
||||||
|
handlebars::html_escape(&message.message).replace('\n', "<br>")
|
||||||
|
);
|
||||||
|
email.multipart(
|
||||||
|
MultiPart::alternative()
|
||||||
|
.singlepart(SinglePart::plain(message.message.to_string()))
|
||||||
|
.multipart(
|
||||||
|
MultiPart::related()
|
||||||
|
.singlepart(SinglePart::html(html))
|
||||||
|
.singlepart(Attachment::new_inline(content_id).body(image, content_type)),
|
||||||
|
),
|
||||||
|
)?
|
||||||
|
} else {
|
||||||
|
email.singlepart(SinglePart::plain(message.message.to_string()))?
|
||||||
|
};
|
||||||
|
let mut mailer = match encryption {
|
||||||
|
SmtpEncryption::None => AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(host),
|
||||||
|
SmtpEncryption::Tls => AsyncSmtpTransport::<Tokio1Executor>::relay(host)?,
|
||||||
|
SmtpEncryption::StartTls => AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(host)?,
|
||||||
|
}
|
||||||
|
.port(*port);
|
||||||
|
if !username.is_empty() || !password.is_empty() {
|
||||||
|
mailer = mailer.credentials(Credentials::new(username.clone(), password.clone()));
|
||||||
|
}
|
||||||
|
mailer.build().send(email).await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-1
@@ -333,7 +333,18 @@ export interface WebhookNotifier {
|
|||||||
headers?: Record<string, string> | null;
|
headers?: Record<string, string> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Notifier = TelegramNotifier | WebhookNotifier;
|
export interface SmtpNotifier {
|
||||||
|
type: 'smtp';
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
encryption: 'none' | 'tls' | 'starttls';
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Notifier = TelegramNotifier | WebhookNotifier | SmtpNotifier;
|
||||||
|
|
||||||
export type Trigger = number | string;
|
export type Trigger = number | string;
|
||||||
|
|
||||||
|
|||||||
@@ -767,6 +767,13 @@
|
|||||||
{notifier.url}
|
{notifier.url}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{:else if notifier.type === 'smtp'}
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<Badge variant="secondary">SMTP</Badge>
|
||||||
|
<span class="text-muted-foreground text-sm break-all">
|
||||||
|
{notifier.host}:{notifier.port} → {notifier.to}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
@@ -945,7 +952,7 @@
|
|||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
<Dialog.Overlay class="bg-background/80 fixed inset-0 z-50 backdrop-blur-sm" />
|
<Dialog.Overlay class="bg-background/80 fixed inset-0 z-50 backdrop-blur-sm" />
|
||||||
<Dialog.Content
|
<Dialog.Content
|
||||||
class="bg-background fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg"
|
class="bg-background fixed top-[50%] left-[50%] z-50 grid max-h-[90vh] w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 overflow-y-auto border p-6 shadow-lg duration-200 sm:rounded-lg"
|
||||||
>
|
>
|
||||||
<Dialog.Header>
|
<Dialog.Header>
|
||||||
<Dialog.Title>
|
<Dialog.Title>
|
||||||
|
|||||||
@@ -1,22 +1,30 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import PasswordInput from '$lib/components/custom/password-input.svelte';
|
||||||
import { Button } from '$lib/components/ui/button/index.js';
|
import { Button } from '$lib/components/ui/button/index.js';
|
||||||
import { Checkbox } from '$lib/components/ui/checkbox/index.js';
|
import { Checkbox } from '$lib/components/ui/checkbox/index.js';
|
||||||
import { Input } from '$lib/components/ui/input/index.js';
|
import { Input } from '$lib/components/ui/input/index.js';
|
||||||
import { Label } from '$lib/components/ui/label/index.js';
|
import { Label } from '$lib/components/ui/label/index.js';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import type { Notifier } from '$lib/types';
|
import type { Notifier, SmtpNotifier } from '$lib/types';
|
||||||
|
|
||||||
export let notifier: Notifier | null = null;
|
export let notifier: Notifier | null = null;
|
||||||
export let onSave: (notifier: Notifier) => void;
|
export let onSave: (notifier: Notifier) => void;
|
||||||
export let onCancel: () => void;
|
export let onCancel: () => void;
|
||||||
|
|
||||||
let type: 'telegram' | 'webhook' = 'telegram';
|
let type: Notifier['type'] = 'telegram';
|
||||||
let botToken = '';
|
let botToken = '';
|
||||||
let chatId = '';
|
let chatId = '';
|
||||||
let skipImage = false;
|
let skipImage = false;
|
||||||
let webhookUrl = '';
|
let webhookUrl = '';
|
||||||
let webhookTemplate = '';
|
let webhookTemplate = '';
|
||||||
let webhookHeaders: { key: string; value: string }[] = [];
|
let webhookHeaders: { key: string; value: string }[] = [];
|
||||||
|
let smtpHost = '';
|
||||||
|
let smtpPort: number | undefined = 465;
|
||||||
|
let smtpEncryption: SmtpNotifier['encryption'] = 'tls';
|
||||||
|
let smtpUsername = '';
|
||||||
|
let smtpPassword = '';
|
||||||
|
let smtpFrom = '';
|
||||||
|
let smtpTo = '';
|
||||||
|
|
||||||
// 初始化表单
|
// 初始化表单
|
||||||
$: {
|
$: {
|
||||||
@@ -26,7 +34,7 @@
|
|||||||
botToken = notifier.bot_token;
|
botToken = notifier.bot_token;
|
||||||
chatId = notifier.chat_id;
|
chatId = notifier.chat_id;
|
||||||
skipImage = notifier.skip_image;
|
skipImage = notifier.skip_image;
|
||||||
} else {
|
} else if (notifier.type === 'webhook') {
|
||||||
type = 'webhook';
|
type = 'webhook';
|
||||||
webhookUrl = notifier.url;
|
webhookUrl = notifier.url;
|
||||||
webhookTemplate = notifier.template || '';
|
webhookTemplate = notifier.template || '';
|
||||||
@@ -35,6 +43,15 @@
|
|||||||
} else {
|
} else {
|
||||||
webhookHeaders = [];
|
webhookHeaders = [];
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
type = 'smtp';
|
||||||
|
smtpHost = notifier.host;
|
||||||
|
smtpPort = notifier.port;
|
||||||
|
smtpEncryption = notifier.encryption;
|
||||||
|
smtpUsername = notifier.username;
|
||||||
|
smtpPassword = notifier.password;
|
||||||
|
smtpFrom = notifier.from;
|
||||||
|
smtpTo = notifier.to;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
type = 'telegram';
|
type = 'telegram';
|
||||||
@@ -44,6 +61,13 @@
|
|||||||
webhookUrl = '';
|
webhookUrl = '';
|
||||||
webhookTemplate = '';
|
webhookTemplate = '';
|
||||||
webhookHeaders = [];
|
webhookHeaders = [];
|
||||||
|
smtpHost = '';
|
||||||
|
smtpPort = 465;
|
||||||
|
smtpEncryption = 'tls';
|
||||||
|
smtpUsername = '';
|
||||||
|
smtpPassword = '';
|
||||||
|
smtpFrom = '';
|
||||||
|
smtpTo = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +89,7 @@
|
|||||||
skip_image: skipImage
|
skip_image: skipImage
|
||||||
};
|
};
|
||||||
onSave(newNotifier);
|
onSave(newNotifier);
|
||||||
} else {
|
} else if (type === 'webhook') {
|
||||||
if (!webhookUrl.trim()) {
|
if (!webhookUrl.trim()) {
|
||||||
toast.error('请输入 Webhook URL');
|
toast.error('请输入 Webhook URL');
|
||||||
return;
|
return;
|
||||||
@@ -94,6 +118,36 @@
|
|||||||
headers: Object.keys(headers).length > 0 ? headers : null
|
headers: Object.keys(headers).length > 0 ? headers : null
|
||||||
};
|
};
|
||||||
onSave(newNotifier);
|
onSave(newNotifier);
|
||||||
|
} else {
|
||||||
|
if (!smtpHost.trim()) {
|
||||||
|
toast.error('请输入 SMTP 服务器地址');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
smtpPort === undefined ||
|
||||||
|
!Number.isInteger(smtpPort) ||
|
||||||
|
smtpPort < 1 ||
|
||||||
|
smtpPort > 65535
|
||||||
|
) {
|
||||||
|
toast.error('端口必须是 1 到 65535 之间的整数');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!smtpFrom.trim() || !smtpTo.trim()) {
|
||||||
|
toast.error('请输入发件人和收件人');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newNotifier: Notifier = {
|
||||||
|
type: 'smtp',
|
||||||
|
host: smtpHost.trim(),
|
||||||
|
port: smtpPort,
|
||||||
|
encryption: smtpEncryption,
|
||||||
|
username: smtpUsername.trim(),
|
||||||
|
password: smtpPassword,
|
||||||
|
from: smtpFrom.trim(),
|
||||||
|
to: smtpTo.trim()
|
||||||
|
};
|
||||||
|
onSave(newNotifier);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -108,6 +162,7 @@
|
|||||||
>
|
>
|
||||||
<option value="telegram">Telegram Bot</option>
|
<option value="telegram">Telegram Bot</option>
|
||||||
<option value="webhook">Webhook</option>
|
<option value="webhook">Webhook</option>
|
||||||
|
<option value="smtp">SMTP 邮件</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -188,6 +243,51 @@
|
|||||||
添加自定义请求头,例如:Authorization: Bearer your_token
|
添加自定义请求头,例如:Authorization: Bearer your_token
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{:else if type === 'smtp'}
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="smtp-host">SMTP 服务器</Label>
|
||||||
|
<Input id="smtp-host" placeholder="smtp.example.com" bind:value={smtpHost} />
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="smtp-encryption">加密方式</Label>
|
||||||
|
<select
|
||||||
|
id="smtp-encryption"
|
||||||
|
class="border-input bg-background ring-offset-background focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||||
|
bind:value={smtpEncryption}
|
||||||
|
onchange={(event) => {
|
||||||
|
smtpPort = { none: 25, tls: 465, starttls: 587 }[
|
||||||
|
event.currentTarget.value as SmtpNotifier['encryption']
|
||||||
|
];
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="tls">TLS</option>
|
||||||
|
<option value="starttls">STARTTLS</option>
|
||||||
|
<option value="none">无加密</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="smtp-port">端口</Label>
|
||||||
|
<Input id="smtp-port" type="number" min="1" max="65535" bind:value={smtpPort} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="smtp-username">用户名(可选)</Label>
|
||||||
|
<Input id="smtp-username" placeholder="SMTP 登录用户名" bind:value={smtpUsername} />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="smtp-password">密码/授权码(可选)</Label>
|
||||||
|
<PasswordInput id="smtp-password" placeholder="密码或邮箱授权码" bind:value={smtpPassword} />
|
||||||
|
<p class="text-muted-foreground text-xs">无需认证时,用户名和密码都留空。</p>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="smtp-from">发件人</Label>
|
||||||
|
<Input id="smtp-from" placeholder="sender@example.com" bind:value={smtpFrom} />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="smtp-to">收件人</Label>
|
||||||
|
<Input id="smtp-to" placeholder="recipient@example.com" bind:value={smtpTo} />
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user