af6cfdaa83
- 新增 daemon/channel/assistant/function/scheduler/logging worker 架构和 JetStream 消息协议 - 修复邮件通知误入 assistant、迁移 checksum、日志 subject 和工具调用无限循环风险 - 新增 assistant 工具轮次熔断、工具结果截断和提示词软收敛规则 - 补充邮件推送关键字、可信发件人配置、升级日志和验证测试
157 lines
5.3 KiB
Rust
157 lines
5.3 KiB
Rust
use std::env;
|
||
use std::time::Duration;
|
||
|
||
use anyhow::anyhow;
|
||
|
||
use crate::model::MailAccount;
|
||
|
||
#[derive(Debug, Clone)]
|
||
pub struct MailPollerConfig {
|
||
pub mail_account_id: Option<i64>,
|
||
pub name: String,
|
||
pub host: String,
|
||
pub port: u16,
|
||
pub username: String,
|
||
pub password: String,
|
||
pub mailbox: String,
|
||
pub source_key: String,
|
||
pub notify_account_id: String,
|
||
pub notify_user_id: String,
|
||
pub notification_subject_keywords: Vec<String>,
|
||
pub trusted_senders: Vec<String>,
|
||
pub poll_interval: Duration,
|
||
pub fetch_limit: usize,
|
||
pub accept_invalid_certs: bool,
|
||
}
|
||
|
||
impl MailPollerConfig {
|
||
pub fn from_account(account: MailAccount) -> Self {
|
||
Self {
|
||
mail_account_id: Some(account.id),
|
||
name: account.name,
|
||
source_key: account.source_key,
|
||
host: account.imap_host,
|
||
port: account.imap_port.clamp(1, u16::MAX as i32) as u16,
|
||
username: account.imap_username,
|
||
password: account.imap_password,
|
||
mailbox: account.mailbox,
|
||
notify_account_id: account.notify_account_id,
|
||
notify_user_id: account.notify_user_id,
|
||
notification_subject_keywords: normalize_setting_values(
|
||
account.notification_subject_keywords,
|
||
),
|
||
trusted_senders: normalize_setting_values(account.trusted_senders),
|
||
poll_interval: Duration::from_secs(account.poll_seconds.max(10) as u64),
|
||
fetch_limit: account.fetch_limit.clamp(1, 100) as usize,
|
||
accept_invalid_certs: account.accept_invalid_certs,
|
||
}
|
||
}
|
||
|
||
pub fn from_env() -> anyhow::Result<Option<Self>> {
|
||
if !env_bool("IA_MAIL_ENABLED", true) {
|
||
return Ok(None);
|
||
}
|
||
|
||
let host = optional_env("IA_MAIL_IMAP_HOST");
|
||
let username = optional_env("IA_MAIL_IMAP_USERNAME");
|
||
let password = optional_env("IA_MAIL_IMAP_PASSWORD");
|
||
let notify_account_id = optional_env("IA_MAIL_NOTIFY_ACCOUNT_ID");
|
||
let notify_user_id = optional_env("IA_MAIL_NOTIFY_USER_ID")
|
||
.or_else(|| optional_env("IA_MAIL_NOTIFY_FROM_USER_ID"));
|
||
|
||
let required_values = [
|
||
host.as_ref(),
|
||
username.as_ref(),
|
||
password.as_ref(),
|
||
notify_account_id.as_ref(),
|
||
notify_user_id.as_ref(),
|
||
];
|
||
if required_values.iter().all(|value| value.is_none()) {
|
||
return Ok(None);
|
||
}
|
||
if required_values.iter().any(|value| value.is_none()) {
|
||
return Err(anyhow!(
|
||
"邮件轮询配置不完整,需要 IA_MAIL_IMAP_HOST、IA_MAIL_IMAP_USERNAME、IA_MAIL_IMAP_PASSWORD、IA_MAIL_NOTIFY_ACCOUNT_ID、IA_MAIL_NOTIFY_USER_ID"
|
||
));
|
||
}
|
||
|
||
let host = host.unwrap();
|
||
let username = username.unwrap();
|
||
let port = optional_env("IA_MAIL_IMAP_PORT")
|
||
.and_then(|value| value.parse::<u16>().ok())
|
||
.unwrap_or(993);
|
||
let mailbox = optional_env("IA_MAIL_IMAP_MAILBOX").unwrap_or_else(|| "INBOX".to_string());
|
||
let poll_seconds = optional_env("IA_MAIL_POLL_SECONDS")
|
||
.and_then(|value| value.parse::<u64>().ok())
|
||
.unwrap_or(60)
|
||
.max(10);
|
||
let fetch_limit = optional_env("IA_MAIL_FETCH_LIMIT")
|
||
.and_then(|value| value.parse::<usize>().ok())
|
||
.unwrap_or(10)
|
||
.clamp(1, 100);
|
||
let notification_subject_keywords = optional_env_list(&[
|
||
"IA_MAIL_NOTIFICATION_SUBJECT_KEYWORDS",
|
||
"IA_MAIL_NOTIFY_SUBJECT_KEYWORDS",
|
||
]);
|
||
let trusted_senders = optional_env_list(&["IA_MAIL_TRUSTED_SENDERS"]);
|
||
|
||
Ok(Some(Self {
|
||
mail_account_id: None,
|
||
name: username.clone(),
|
||
source_key: format!("imap:{host}:{username}"),
|
||
host,
|
||
port,
|
||
username,
|
||
password: password.unwrap(),
|
||
mailbox,
|
||
notify_account_id: notify_account_id.unwrap(),
|
||
notify_user_id: notify_user_id.unwrap(),
|
||
notification_subject_keywords,
|
||
trusted_senders,
|
||
poll_interval: Duration::from_secs(poll_seconds),
|
||
fetch_limit,
|
||
accept_invalid_certs: env_bool("IA_MAIL_ACCEPT_INVALID_CERTS", false),
|
||
}))
|
||
}
|
||
}
|
||
|
||
fn optional_env(name: &str) -> Option<String> {
|
||
env::var(name)
|
||
.ok()
|
||
.map(|value| value.trim().to_string())
|
||
.filter(|value| !value.is_empty())
|
||
}
|
||
|
||
fn optional_env_list(names: &[&str]) -> Vec<String> {
|
||
names
|
||
.iter()
|
||
.find_map(|name| optional_env(name))
|
||
.map(|value| parse_setting_list(&value))
|
||
.unwrap_or_default()
|
||
}
|
||
|
||
pub(crate) fn parse_setting_list(value: &str) -> Vec<String> {
|
||
normalize_setting_values(
|
||
value
|
||
.split([',', ',', ';', ';', '\n'])
|
||
.map(ToString::to_string),
|
||
)
|
||
}
|
||
|
||
pub(crate) fn normalize_setting_values(values: impl IntoIterator<Item = String>) -> Vec<String> {
|
||
values
|
||
.into_iter()
|
||
.map(|value| value.trim().to_string())
|
||
.filter(|value| !value.is_empty())
|
||
.collect()
|
||
}
|
||
|
||
fn env_bool(name: &str, default: bool) -> bool {
|
||
env::var(name)
|
||
.map(|value| {
|
||
let value = value.trim().to_ascii_lowercase();
|
||
!matches!(value.as_str(), "0" | "false" | "no" | "off")
|
||
})
|
||
.unwrap_or(default)
|
||
}
|