From ab6846cb9f8d4ebdb13ec6643f178a6fc91b56f4 Mon Sep 17 00:00:00 2001 From: "Darren.Zeng" Date: Thu, 12 Mar 2026 11:27:52 +0800 Subject: [PATCH] feat(channel): make email subject configurable (#3190) Add default_subject field to EmailConfig to allow users to customize the default subject line for outgoing emails. Previously hardcoded as "ZeroClaw Message". - Add default_subject field with serde default - Update send() method to use configured default - Add tests for new functionality Fixes #2878 Co-authored-by: Argenis --- src/channels/email_channel.rs | 19 +++++++++++++++++-- src/gateway/api.rs | 2 ++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/channels/email_channel.rs b/src/channels/email_channel.rs index 147dffea5..4e6f7c222 100644 --- a/src/channels/email_channel.rs +++ b/src/channels/email_channel.rs @@ -67,6 +67,9 @@ pub struct EmailConfig { /// Allowed sender addresses/domains (empty = deny all, ["*"] = allow all) #[serde(default)] pub allowed_senders: Vec, + /// Default subject line for outgoing emails (default: "ZeroClaw Message") + #[serde(default = "default_subject")] + pub default_subject: String, } impl crate::config::traits::ChannelConfig for EmailConfig { @@ -93,6 +96,9 @@ fn default_idle_timeout() -> u64 { fn default_true() -> bool { true } +fn default_subject() -> String { + "ZeroClaw Message".into() +} impl Default for EmailConfig { fn default() -> Self { @@ -108,6 +114,7 @@ impl Default for EmailConfig { from_address: String::new(), idle_timeout_secs: default_idle_timeout(), allowed_senders: Vec::new(), + default_subject: default_subject(), } } } @@ -512,16 +519,17 @@ impl Channel for EmailChannel { async fn send(&self, message: &SendMessage) -> Result<()> { // Use explicit subject if provided, otherwise fall back to legacy parsing or default + let default_subject = self.config.default_subject.as_str(); let (subject, body) = if let Some(ref subj) = message.subject { (subj.as_str(), message.content.as_str()) } else if message.content.starts_with("Subject: ") { if let Some(pos) = message.content.find('\n') { (&message.content[9..pos], message.content[pos + 1..].trim()) } else { - ("ZeroClaw Message", message.content.as_str()) + (default_subject, message.content.as_str()) } } else { - ("ZeroClaw Message", message.content.as_str()) + (default_subject, message.content.as_str()) }; let email = Message::builder() @@ -635,10 +643,12 @@ mod tests { from_address: "bot@example.com".to_string(), idle_timeout_secs: 1200, allowed_senders: vec!["allowed@example.com".to_string()], + default_subject: "Custom Subject".to_string(), }; assert_eq!(config.imap_host, "imap.example.com"); assert_eq!(config.imap_folder, "Archive"); assert_eq!(config.idle_timeout_secs, 1200); + assert_eq!(config.default_subject, "Custom Subject"); } #[test] @@ -655,11 +665,13 @@ mod tests { from_address: "bot@test.com".to_string(), idle_timeout_secs: 1740, allowed_senders: vec!["*".to_string()], + default_subject: "Test Subject".to_string(), }; let cloned = config.clone(); assert_eq!(cloned.imap_host, config.imap_host); assert_eq!(cloned.smtp_port, config.smtp_port); assert_eq!(cloned.allowed_senders, config.allowed_senders); + assert_eq!(cloned.default_subject, config.default_subject); } // EmailChannel tests @@ -900,6 +912,7 @@ mod tests { from_address: "bot@example.com".to_string(), idle_timeout_secs: 1740, allowed_senders: vec!["allowed@example.com".to_string()], + default_subject: "Serialization Test".to_string(), }; let json = serde_json::to_string(&config).unwrap(); @@ -908,6 +921,7 @@ mod tests { assert_eq!(deserialized.imap_host, config.imap_host); assert_eq!(deserialized.smtp_port, config.smtp_port); assert_eq!(deserialized.allowed_senders, config.allowed_senders); + assert_eq!(deserialized.default_subject, config.default_subject); } #[test] @@ -925,6 +939,7 @@ mod tests { assert_eq!(config.smtp_port, 465); // default assert!(config.smtp_tls); // default assert_eq!(config.idle_timeout_secs, 1740); // default + assert_eq!(config.default_subject, "ZeroClaw Message"); // default } #[test] diff --git a/src/gateway/api.rs b/src/gateway/api.rs index f1859c07a..18eb5304b 100644 --- a/src/gateway/api.rs +++ b/src/gateway/api.rs @@ -1055,6 +1055,7 @@ mod tests { from_address: "agent@example.com".to_string(), idle_timeout_secs: 1740, allowed_senders: vec!["*".to_string()], + default_subject: "ZeroClaw Message".to_string(), }); cfg.model_routes = vec![crate::config::schema::ModelRouteConfig { hint: "reasoning".to_string(), @@ -1188,6 +1189,7 @@ mod tests { from_address: "agent@example.com".to_string(), idle_timeout_secs: 1740, allowed_senders: vec!["*".to_string()], + default_subject: "ZeroClaw Message".to_string(), }); current.model_routes = vec![ crate::config::schema::ModelRouteConfig {