|
| 1 | +//! Email forwarding to external SMTP servers. |
| 2 | +
|
| 3 | +use lettre::message::Mailbox; |
| 4 | +use lettre::transport::smtp::authentication::Credentials; |
| 5 | +use lettre::transport::smtp::client::{Tls, TlsParameters}; |
| 6 | +use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor}; |
| 7 | +use std::sync::Arc; |
| 8 | +use tokio::sync::mpsc; |
| 9 | +use tracing::{debug, error, info}; |
| 10 | + |
| 11 | +use crate::email::MailRecord; |
| 12 | + |
| 13 | +/// Configuration for email forwarding. |
| 14 | +#[derive(Debug, Clone)] |
| 15 | +pub struct ForwardConfig { |
| 16 | + /// SMTP host to forward to |
| 17 | + pub host: String, |
| 18 | + /// SMTP port (default: 25 or 587) |
| 19 | + pub port: u16, |
| 20 | + /// Use TLS (STARTTLS or implicit) |
| 21 | + pub tls: bool, |
| 22 | + /// Use implicit TLS (SMTPS) |
| 23 | + pub implicit_tls: bool, |
| 24 | + /// Username for SMTP AUTH |
| 25 | + pub username: Option<String>, |
| 26 | + /// Password for SMTP AUTH |
| 27 | + pub password: Option<String>, |
| 28 | + /// Rewrite all recipients to this address |
| 29 | + pub to_override: Option<String>, |
| 30 | + /// Rewrite sender to this address |
| 31 | + pub from_override: Option<String>, |
| 32 | +} |
| 33 | + |
| 34 | +impl ForwardConfig { |
| 35 | + /// Check if forwarding is configured. |
| 36 | + #[must_use] |
| 37 | + pub const fn is_enabled(&self) -> bool { |
| 38 | + !self.host.is_empty() |
| 39 | + } |
| 40 | +} |
| 41 | + |
| 42 | +/// Email forwarder that sends emails to an external SMTP server. |
| 43 | +pub struct Forwarder { |
| 44 | + config: ForwardConfig, |
| 45 | +} |
| 46 | + |
| 47 | +impl Forwarder { |
| 48 | + /// Create a new forwarder with the given config. |
| 49 | + /// Returns the forwarder and a handle for submitting emails. |
| 50 | + #[must_use] |
| 51 | + pub fn new(config: ForwardConfig) -> (Arc<Self>, ForwardHandle) { |
| 52 | + let (tx, rx) = mpsc::channel(100); |
| 53 | + let forwarder = Arc::new(Self { config }); |
| 54 | + |
| 55 | + // Spawn the forwarding task |
| 56 | + let forwarder_clone = Arc::clone(&forwarder); |
| 57 | + tokio::spawn(async move { |
| 58 | + forwarder_clone.run(rx).await; |
| 59 | + }); |
| 60 | + |
| 61 | + let handle = ForwardHandle { tx }; |
| 62 | + (forwarder, handle) |
| 63 | + } |
| 64 | + |
| 65 | + async fn run(self: Arc<Self>, mut rx: mpsc::Receiver<MailRecord>) { |
| 66 | + info!( |
| 67 | + "Email forwarder started, forwarding to {}:{}", |
| 68 | + self.config.host, self.config.port |
| 69 | + ); |
| 70 | + |
| 71 | + while let Some(email) = rx.recv().await { |
| 72 | + if let Err(e) = self.forward_email(&email).await { |
| 73 | + error!("Failed to forward email {}: {}", email.id, e); |
| 74 | + } else { |
| 75 | + debug!("Forwarded email {} to {}", email.id, self.config.host); |
| 76 | + } |
| 77 | + } |
| 78 | + } |
| 79 | + |
| 80 | + async fn forward_email(&self, email: &MailRecord) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { |
| 81 | + // Build the email message |
| 82 | + let from: Mailbox = self |
| 83 | + .config |
| 84 | + .from_override |
| 85 | + .as_ref() |
| 86 | + .unwrap_or(&email.from) |
| 87 | + .parse() |
| 88 | + .map_err(|_| format!("Invalid from address: {}", email.from))?; |
| 89 | + |
| 90 | + // Determine recipients |
| 91 | + let recipients: Vec<String> = self.config.to_override.as_ref().map_or_else( |
| 92 | + || email.to.clone(), |
| 93 | + |override_to| vec![override_to.clone()], |
| 94 | + ); |
| 95 | + |
| 96 | + if recipients.is_empty() { |
| 97 | + return Err("No recipients".into()); |
| 98 | + } |
| 99 | + |
| 100 | + // Build message |
| 101 | + let mut builder = Message::builder() |
| 102 | + .from(from) |
| 103 | + .subject(email.subject.as_deref().unwrap_or("(no subject)")); |
| 104 | + |
| 105 | + for recipient in &recipients { |
| 106 | + let mailbox: Mailbox = recipient |
| 107 | + .parse() |
| 108 | + .map_err(|_| format!("Invalid recipient: {recipient}"))?; |
| 109 | + builder = builder.to(mailbox); |
| 110 | + } |
| 111 | + |
| 112 | + // Add Date header |
| 113 | + if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(&email.date) { |
| 114 | + builder = builder.date(std::time::SystemTime::from(dt)); |
| 115 | + } |
| 116 | + |
| 117 | + // Create the message with body |
| 118 | + let message = if let Some(ref html) = email.html { |
| 119 | + if let Some(ref text) = email.text { |
| 120 | + builder.multipart( |
| 121 | + lettre::message::MultiPart::alternative() |
| 122 | + .singlepart( |
| 123 | + lettre::message::SinglePart::builder() |
| 124 | + .header(lettre::message::header::ContentType::TEXT_PLAIN) |
| 125 | + .body(text.clone()), |
| 126 | + ) |
| 127 | + .singlepart( |
| 128 | + lettre::message::SinglePart::builder() |
| 129 | + .header(lettre::message::header::ContentType::TEXT_HTML) |
| 130 | + .body(html.clone()), |
| 131 | + ), |
| 132 | + )? |
| 133 | + } else { |
| 134 | + builder.header(lettre::message::header::ContentType::TEXT_HTML).body(html.clone())? |
| 135 | + } |
| 136 | + } else if let Some(ref text) = email.text { |
| 137 | + builder.header(lettre::message::header::ContentType::TEXT_PLAIN).body(text.clone())? |
| 138 | + } else { |
| 139 | + builder.body(String::new())? |
| 140 | + }; |
| 141 | + |
| 142 | + // Build SMTP transport |
| 143 | + let transport = self.build_transport()?; |
| 144 | + |
| 145 | + // Send the email |
| 146 | + transport.send(message).await?; |
| 147 | + |
| 148 | + Ok(()) |
| 149 | + } |
| 150 | + |
| 151 | + fn build_transport( |
| 152 | + &self, |
| 153 | + ) -> Result<AsyncSmtpTransport<Tokio1Executor>, Box<dyn std::error::Error + Send + Sync>> { |
| 154 | + let mut builder = if self.config.implicit_tls { |
| 155 | + AsyncSmtpTransport::<Tokio1Executor>::relay(&self.config.host)? |
| 156 | + } else if self.config.tls { |
| 157 | + AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(&self.config.host)? |
| 158 | + } else { |
| 159 | + let tls_params = TlsParameters::builder(self.config.host.clone()) |
| 160 | + .dangerous_accept_invalid_certs(true) |
| 161 | + .build()?; |
| 162 | + AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(&self.config.host) |
| 163 | + .tls(Tls::Opportunistic(tls_params)) |
| 164 | + }; |
| 165 | + |
| 166 | + builder = builder.port(self.config.port); |
| 167 | + |
| 168 | + if let (Some(ref user), Some(ref pass)) = (&self.config.username, &self.config.password) { |
| 169 | + builder = builder.credentials(Credentials::new(user.clone(), pass.clone())); |
| 170 | + } |
| 171 | + |
| 172 | + Ok(builder.build()) |
| 173 | + } |
| 174 | +} |
| 175 | + |
| 176 | +/// Handle for submitting emails to be forwarded. |
| 177 | +#[derive(Clone)] |
| 178 | +pub struct ForwardHandle { |
| 179 | + tx: mpsc::Sender<MailRecord>, |
| 180 | +} |
| 181 | + |
| 182 | +impl ForwardHandle { |
| 183 | + /// Queue an email for forwarding. |
| 184 | + pub async fn forward(&self, email: MailRecord) { |
| 185 | + if let Err(e) = self.tx.send(email).await { |
| 186 | + error!("Failed to queue email for forwarding: {}", e); |
| 187 | + } |
| 188 | + } |
| 189 | +} |
0 commit comments