alun_plugin/
notification.rs1use async_trait::async_trait;
4use alun_core::{Plugin, Result};
5use alun_config::NotificationConfig;
6use lettre::{
7 Transport, Message,
8 message::{Mailbox, MultiPart},
9 transport::smtp::{authentication::Credentials, SmtpTransport},
10};
11use tracing::{info, error};
12
13pub struct NotificationPlugin {
19 config: NotificationConfig,
21 mailer: Option<SmtpTransport>,
23 from: Option<Mailbox>,
25}
26
27impl NotificationPlugin {
28 pub fn from_config(config: &NotificationConfig) -> Self {
33 let (mailer, from) = if config.enabled && !config.smtp_host.is_empty() && !config.smtp_user.is_empty() {
34 let from_email = if config.from_email.is_empty() { &config.smtp_user } else { &config.from_email };
35 let creds = Credentials::new(config.smtp_user.clone(), config.smtp_pass.clone());
36
37 let builder = if from_email.ends_with("@icloud.com") || from_email.ends_with("@swisscows.email") {
38 SmtpTransport::starttls_relay(&config.smtp_host)
39 } else {
40 SmtpTransport::relay(&config.smtp_host)
41 };
42
43 let builder = match builder {
44 Ok(b) => b.port(config.smtp_port).credentials(creds),
45 Err(e) => {
46 error!("SMTP 传输初始化失败: {}", e);
47 return Self { config: config.clone(), mailer: None, from: None };
48 }
49 };
50
51 let transport = builder.build();
52
53 let from_name = if config.from_name.is_empty() { "系统通知" } else { &config.from_name };
54 let from_mb = format!("{} <{}>", from_name, from_email)
55 .parse::<Mailbox>()
56 .ok();
57
58 (Some(transport), from_mb)
59 } else {
60 (None, None)
61 };
62
63 Self { config: config.clone(), mailer, from }
64 }
65
66 pub fn send_text(&self, to: &str, subject: &str, body: &str) -> Result<()> {
68 if let (Some(ref mailer), Some(ref from)) = (&self.mailer, &self.from) {
69 let to_mb: Mailbox = to.parse().map_err(|e| {
70 alun_core::Error::Msg(format!("收件人地址无效: {}", e))
71 })?;
72 let email = Message::builder()
73 .from(from.clone())
74 .to(to_mb)
75 .subject(subject)
76 .body(body.to_string())
77 .map_err(|e| alun_core::Error::Msg(format!("邮件构建失败: {}", e)))?;
78
79 mailer.send(&email).map_err(|e| {
80 error!("邮件发送失败: {}", e);
81 alun_core::Error::Msg(format!("邮件发送失败: {}", e))
82 })?;
83 info!("邮件已发送 to={} subject={}", to, subject);
84 Ok(())
85 } else {
86 info!("邮件功能未配置,跳过发送: to={} subject={}", to, subject);
87 Ok(())
88 }
89 }
90
91 pub fn send_html(&self, to: &str, subject: &str, html_body: &str) -> Result<()> {
95 if let (Some(ref mailer), Some(ref from)) = (&self.mailer, &self.from) {
96 let to_mb: Mailbox = to.parse().map_err(|e| {
97 alun_core::Error::Msg(format!("收件人地址无效: {}", e))
98 })?;
99 let plain_text = Self::html_to_text(html_body);
100 let email = Message::builder()
101 .from(from.clone())
102 .to(to_mb)
103 .subject(subject)
104 .multipart(MultiPart::alternative_plain_html(
105 plain_text,
106 html_body.to_string(),
107 ))
108 .map_err(|e| alun_core::Error::Msg(format!("邮件构建失败: {}", e)))?;
109
110 mailer.send(&email).map_err(|e| {
111 error!("邮件发送失败: {}", e);
112 alun_core::Error::Msg(format!("邮件发送失败: {}", e))
113 })?;
114 info!("HTML 邮件已发送 to={} subject={}", to, subject);
115 Ok(())
116 } else {
117 info!("邮件功能未配置,跳过发送: to={} subject={}", to, subject);
118 Ok(())
119 }
120 }
121
122 pub fn is_configured(&self) -> bool {
124 self.mailer.is_some()
125 }
126
127 fn html_to_text(html: &str) -> String {
129 let mut text = String::with_capacity(html.len());
130 let mut in_tag = false;
131
132 for ch in html.chars() {
133 match ch {
134 '<' => in_tag = true,
135 '>' => in_tag = false,
136 _ if !in_tag => text.push(ch),
137 _ => {}
138 }
139 }
140
141 let text = text
142 .replace("&", "&")
143 .replace("<", "<")
144 .replace(">", ">")
145 .replace(""", "\"")
146 .replace("'", "'")
147 .replace(" ", " ");
148
149 let mut result = String::with_capacity(text.len());
150 let mut prev_was_whitespace = false;
151 for ch in text.chars() {
152 if ch == '\n' {
153 result.push('\n');
154 prev_was_whitespace = true;
155 } else if ch.is_whitespace() {
156 if !prev_was_whitespace {
157 result.push(' ');
158 prev_was_whitespace = true;
159 }
160 } else {
161 result.push(ch);
162 prev_was_whitespace = false;
163 }
164 }
165
166 result.trim().to_string()
167 }
168}
169
170#[async_trait]
171impl Plugin for NotificationPlugin {
172 fn name(&self) -> &str { "notification" }
173
174 async fn start(&self) -> Result<()> {
175 if self.is_configured() {
176 info!("通知插件就绪: SMTP {}:{}", self.config.smtp_host, self.config.smtp_port);
177 } else {
178 info!("通知插件: 未配置(跳过)");
179 }
180 Ok(())
181 }
182
183 async fn stop(&self) -> Result<()> { Ok(()) }
184}