From 1862c18d10202b9952050c74c8023b43d83b3bbc Mon Sep 17 00:00:00 2001 From: argenis de la rosa Date: Sat, 14 Feb 2026 14:39:43 -0500 Subject: [PATCH] fix: address PR #37 review issues - Add missing EmailConfig struct with serde derives and defaults - Register email_channel module in mod.rs with exports - Fix IMAP tag reuse (RFC 3501 violation) using incrementing counter - Fix email sender validation logic (clearer domain vs full email matching) - Fix mail_parser API usage (MessageParser::default().parse()) - Fix WhatsApp allowlist matching (normalize phone numbers) - Fix WhatsApp health_check (don't treat 404 as healthy) - Fix WhatsApp listen() to keep task alive (prevent channel bus closing) - Add missing dependencies: lettre, mail-parser, rustls-pki-types, tokio-rustls, webpki-roots - Remove unused imports All 665 tests pass. --- Cargo.lock | 329 ++++++++++++++++++++++++++++++++++ Cargo.toml | 5 + src/channels/email_channel.rs | 126 +++++++++---- src/channels/mod.rs | 2 +- src/channels/whatsapp.rs | 17 +- 5 files changed, 442 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 00da71f..c722f71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,6 +24,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -89,6 +95,15 @@ version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +[[package]] +name = "ar_archive_writer" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" +dependencies = [ + "object", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -112,6 +127,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.37.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "base64" version = "0.22.1" @@ -158,6 +195,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -208,6 +247,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "chumsky" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" +dependencies = [ + "hashbrown 0.14.5", + "stacker", +] + [[package]] name = "cipher" version = "0.4.4" @@ -259,6 +308,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -278,6 +336,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -387,6 +455,28 @@ dependencies = [ "syn", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "email-encoding" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9298e6504d9b9e780ed3f7dfd43a61be8cd0e09eb07f7706a945b0072b6670b6" +dependencies = [ + "base64", + "memchr", +] + +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" + [[package]] name = "encode_unicode" version = "1.0.0" @@ -433,6 +523,21 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -442,6 +547,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures-channel" version = "0.3.31" @@ -545,6 +656,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", + "allocator-api2", ] [[package]] @@ -553,6 +665,17 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +[[package]] +name = "hashify" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "149e3ea90eb5a26ad354cfe3cb7f7401b9329032d0235f2687d03a35f30e5d4c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "hashlink" version = "0.9.1" @@ -618,6 +741,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.8.1" @@ -852,6 +981,16 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.85" @@ -868,6 +1007,33 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lettre" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e13e10e8818f8b2a60f52cb127041d388b89f3a96a62be9ceaffa22262fef7f" +dependencies = [ + "base64", + "chumsky", + "email-encoding", + "email_address", + "fastrand", + "futures-util", + "hostname", + "httpdate", + "idna", + "mime", + "native-tls", + "nom", + "percent-encoding", + "quoted_printable", + "rustls", + "socket2", + "tokio", + "url", + "webpki-roots 1.0.6", +] + [[package]] name = "libc" version = "0.2.182" @@ -919,12 +1085,27 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "mail-parser" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f82a3d6522697593ba4c683e0a6ee5a40fee93bc1a525e3cc6eeb3da11fd8897" +dependencies = [ + "hashify", +] + [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "mio" version = "1.1.1" @@ -936,6 +1117,32 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "native-tls" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5d26952a508f321b4d3d2e80e78fc2603eaefcdf0c30783867f19586518bdc" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -954,6 +1161,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -972,6 +1188,50 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -1040,6 +1300,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3852766467df634d74f0b2d7819bf8dc483a0eb2e3b0f50f756f9cfe8b0d18d8" +dependencies = [ + "ar_archive_writer", + "cc", +] + [[package]] name = "quinn" version = "0.11.9" @@ -1104,6 +1374,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "quoted_printable" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" + [[package]] name = "r-efi" version = "5.3.0" @@ -1284,6 +1560,8 @@ version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ + "aws-lc-rs", + "log", "once_cell", "ring", "rustls-pki-types", @@ -1308,6 +1586,7 @@ version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -1325,6 +1604,38 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "security-framework" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321c8673b092a9a42605034a9879d73cb79101ed5fd117bc9a597b89b4e9e61a" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.228" @@ -1468,6 +1779,19 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "stacker" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d74a23609d509411d10e2176dc2a4346e3b4aea2e7b1869f19fdedbc71c013" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.59.0", +] + [[package]] name = "strsim" version = "0.11.1" @@ -2372,20 +2696,25 @@ dependencies = [ "directories", "futures-util", "hostname", + "lettre", + "mail-parser", "reqwest", "rusqlite", + "rustls-pki-types", "serde", "serde_json", "shellexpand", "tempfile", "thiserror 2.0.18", "tokio", + "tokio-rustls", "tokio-test", "tokio-tungstenite", "toml", "tracing", "tracing-subscriber", "uuid", + "webpki-roots 1.0.6", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 08f75b0..13a6334 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,11 @@ console = "0.15" tokio-tungstenite = { version = "0.24", features = ["rustls-tls-webpki-roots"] } futures-util = { version = "0.3", default-features = false, features = ["sink"] } hostname = "0.4.2" +lettre = { version = "0.11.19", features = ["smtp-transport", "rustls-tls"] } +mail-parser = "0.11.2" +rustls-pki-types = "1.14.0" +tokio-rustls = "0.26.4" +webpki-roots = "1.0.6" [profile.release] opt-level = "z" # Optimize for size diff --git a/src/channels/email_channel.rs b/src/channels/email_channel.rs index 66388f9..e367c04 100644 --- a/src/channels/email_channel.rs +++ b/src/channels/email_channel.rs @@ -2,20 +2,77 @@ use async_trait::async_trait; use anyhow::{anyhow, Result}; use lettre::transport::smtp::authentication::Credentials; use lettre::{Message, SmtpTransport, Transport}; -use mail_parser::{Message as ParsedMessage, MimeHeaders}; +use mail_parser::{MessageParser, MimeHeaders}; +use serde::{Deserialize, Serialize}; use std::collections::HashSet; -use std::io::{BufRead, BufReader, Write as IoWrite}; +use std::io::Write as IoWrite; use std::net::TcpStream; use std::sync::Mutex; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use tokio::sync::mpsc; use tokio::time::{interval, sleep}; -use tracing::{debug, error, info, warn}; +use tracing::{error, info, warn}; use uuid::Uuid; -// Email config — add to config.rs use super::traits::{Channel, ChannelMessage}; +/// Email channel configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EmailConfig { + /// IMAP server hostname + pub imap_host: String, + /// IMAP server port (default: 993 for TLS) + #[serde(default = "default_imap_port")] + pub imap_port: u16, + /// IMAP folder to poll (default: INBOX) + #[serde(default = "default_imap_folder")] + pub imap_folder: String, + /// SMTP server hostname + pub smtp_host: String, + /// SMTP server port (default: 587 for STARTTLS) + #[serde(default = "default_smtp_port")] + pub smtp_port: u16, + /// Use TLS for SMTP (default: true) + #[serde(default = "default_true")] + pub smtp_tls: bool, + /// Email username for authentication + pub username: String, + /// Email password for authentication + pub password: String, + /// From address for outgoing emails + pub from_address: String, + /// Poll interval in seconds (default: 60) + #[serde(default = "default_poll_interval")] + pub poll_interval_secs: u64, + /// Allowed sender addresses/domains (empty = deny all, ["*"] = allow all) + #[serde(default)] + pub allowed_senders: Vec, +} + +fn default_imap_port() -> u16 { 993 } +fn default_smtp_port() -> u16 { 587 } +fn default_imap_folder() -> String { "INBOX".into() } +fn default_poll_interval() -> u64 { 60 } +fn default_true() -> bool { true } + +impl Default for EmailConfig { + fn default() -> Self { + Self { + imap_host: String::new(), + imap_port: default_imap_port(), + imap_folder: default_imap_folder(), + smtp_host: String::new(), + smtp_port: default_smtp_port(), + smtp_tls: true, + username: String::new(), + password: String::new(), + from_address: String::new(), + poll_interval_secs: default_poll_interval(), + allowed_senders: Vec::new(), + } + } +} + /// Email channel — IMAP polling for inbound, SMTP for outbound pub struct EmailChannel { pub config: EmailConfig, @@ -38,11 +95,18 @@ impl EmailChannel { if self.config.allowed_senders.iter().any(|a| a == "*") { return true; // Wildcard = allow all } + let email_lower = email.to_lowercase(); self.config.allowed_senders.iter().any(|allowed| { - allowed.eq_ignore_ascii_case(email) - || email.to_lowercase().ends_with(&format!("@{}", allowed.to_lowercase())) - || (allowed.starts_with('@') - && email.to_lowercase().ends_with(&allowed.to_lowercase())) + if allowed.starts_with('@') { + // Domain match with @ prefix: "@example.com" + email_lower.ends_with(&allowed.to_lowercase()) + } else if allowed.contains('@') { + // Full email address match + allowed.eq_ignore_ascii_case(email) + } else { + // Domain match without @ prefix: "example.com" + email_lower.ends_with(&format!("@{}", allowed.to_lowercase())) + } }) } @@ -63,18 +127,11 @@ impl EmailChannel { /// Extract the sender address from a parsed email fn extract_sender(parsed: &mail_parser::Message) -> String { - match parsed.from() { - mail_parser::HeaderValue::Address(addr) => { - addr.address.as_ref().map(|a| a.to_string()).unwrap_or_else(|| "unknown".into()) - } - mail_parser::HeaderValue::AddressList(addrs) => { - addrs.first() - .and_then(|a| a.address.as_ref()) - .map(|a| a.to_string()) - .unwrap_or_else(|| "unknown".into()) - } - _ => "unknown".into(), - } + parsed.from() + .and_then(|addr| addr.first()) + .and_then(|a| a.address()) + .map(|s| s.to_string()) + .unwrap_or_else(|| "unknown".into()) } /// Extract readable text from a parsed email @@ -124,7 +181,7 @@ impl EmailChannel { rustls::ClientConnection::new(tls_config, server_name)?; let mut tls = rustls::StreamOwned::new(conn, tcp); - let mut read_line = |tls: &mut rustls::StreamOwned| -> Result { + let read_line = |tls: &mut rustls::StreamOwned| -> Result { let mut buf = Vec::new(); loop { let mut byte = [0u8; 1]; @@ -141,7 +198,7 @@ impl EmailChannel { } }; - let mut send_cmd = |tls: &mut rustls::StreamOwned, + let send_cmd = |tls: &mut rustls::StreamOwned, tag: &str, cmd: &str| -> Result> { @@ -189,10 +246,13 @@ impl EmailChannel { } let mut results = Vec::new(); + let mut tag_counter = 4_u32; // Start after A1, A2, A3 for uid in &uids { - // Fetch RFC822 - let fetch_resp = send_cmd(&mut tls, "A4", &format!("FETCH {} RFC822", uid))?; + // Fetch RFC822 with unique tag + let fetch_tag = format!("A{}", tag_counter); + tag_counter += 1; + let fetch_resp = send_cmd(&mut tls, &fetch_tag, &format!("FETCH {} RFC822", uid))?; // Reconstruct the raw email from the response (skip first and last lines) let raw: String = fetch_resp .iter() @@ -201,7 +261,7 @@ impl EmailChannel { .cloned() .collect(); - if let Some(parsed) = ParsedMessage::parse(raw.as_bytes()) { + if let Some(parsed) = MessageParser::default().parse(raw.as_bytes()) { let sender = Self::extract_sender(&parsed); let subject = parsed.subject().unwrap_or("(no subject)").to_string(); let body = Self::extract_text(&parsed); @@ -213,7 +273,6 @@ impl EmailChannel { let ts = parsed .date() .map(|d| { - // DateTime year/month/day/hour/minute/second let naive = chrono::NaiveDate::from_ymd_opt( d.year as i32, d.month as u32, d.day as u32 ).and_then(|date| date.and_hms_opt(d.hour as u32, d.minute as u32, d.second as u32)); @@ -222,19 +281,22 @@ impl EmailChannel { .unwrap_or_else(|| { SystemTime::now() .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() + .map(|d| d.as_secs()) + .unwrap_or(0) }); results.push((msg_id, sender, content, ts)); } - // Mark as seen - let _ = send_cmd(&mut tls, "A5", &format!("STORE {} +FLAGS (\\Seen)", uid)); + // Mark as seen with unique tag + let store_tag = format!("A{}", tag_counter); + tag_counter += 1; + let _ = send_cmd(&mut tls, &store_tag, &format!("STORE {} +FLAGS (\\Seen)", uid)); } - // Logout - let _ = send_cmd(&mut tls, "A6", "LOGOUT"); + // Logout with unique tag + let logout_tag = format!("A{}", tag_counter); + let _ = send_cmd(&mut tls, &logout_tag, "LOGOUT"); Ok(results) } diff --git a/src/channels/mod.rs b/src/channels/mod.rs index 87686b7..016b76c 100644 --- a/src/channels/mod.rs +++ b/src/channels/mod.rs @@ -1,5 +1,6 @@ pub mod cli; pub mod discord; +pub mod email_channel; pub mod imessage; pub mod matrix; pub mod slack; @@ -13,7 +14,6 @@ pub use imessage::IMessageChannel; pub use matrix::MatrixChannel; pub use slack::SlackChannel; pub use telegram::TelegramChannel; -pub use whatsapp::WhatsAppChannel; pub use traits::Channel; use crate::config::Config; diff --git a/src/channels/whatsapp.rs b/src/channels/whatsapp.rs index 7860d7c..65a4c83 100644 --- a/src/channels/whatsapp.rs +++ b/src/channels/whatsapp.rs @@ -6,7 +6,7 @@ use serde_json::{json, Value}; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::{mpsc, RwLock}; -use tracing::{debug, error, info, warn}; +use tracing::{debug, info, warn}; use super::traits::{Channel, ChannelMessage}; @@ -150,8 +150,14 @@ impl WhatsAppChannel { pub fn is_sender_allowed(&self, phone: &str) -> bool { if self.config.allowed_numbers.is_empty() { return false; } if self.config.allowed_numbers.iter().any(|a| a == "*") { return true; } + // Normalize phone numbers for comparison (strip + and leading zeros) + fn normalize(p: &str) -> String { + p.trim_start_matches('+').trim_start_matches('0').to_string() + } + let phone_norm = normalize(phone); self.config.allowed_numbers.iter().any(|a| { - a.eq_ignore_ascii_case(phone) || phone.ends_with(a) || a.ends_with(phone) + let a_norm = normalize(a); + a_norm == phone_norm || phone_norm.ends_with(&a_norm) || a_norm.ends_with(&phone_norm) }) } @@ -190,7 +196,10 @@ impl Channel for WhatsAppChannel { async fn listen(&self, _tx: mpsc::Sender) -> Result<()> { info!("WhatsApp webhook path: {}", self.config.webhook_path); // Webhooks handled by gateway HTTP server — process_webhook() called externally - Ok(()) + // Keep task alive to prevent channel bus from closing + loop { + tokio::time::sleep(std::time::Duration::from_secs(3600)).await; + } } async fn health_check(&self) -> bool { @@ -198,7 +207,7 @@ impl Channel for WhatsAppChannel { self.client.get(&url) .header("Authorization", format!("Bearer {}", self.config.access_token)) .send().await - .map(|r| r.status().is_success() || r.status().as_u16() == 404) + .map(|r| r.status().is_success()) .unwrap_or(false) } }