use crate::{
types::{SimpleMessage, MessageType, EmailConfig, SecureMessage},
error::{Result, EmailError},
};
use std::string::ToString;
use std::collections::HashMap;
#[cfg(feature = "email")]
use lettre::{
message::{header, Mailbox, Message, SinglePart},
transport::smtp::authentication::Credentials,
SmtpTransport, Transport,
};
use base64::Engine as _;
#[cfg(feature = "email")]
#[derive(Debug, Clone)]
pub struct EmailTransport {
config: EmailConfig,
smtp_transport: SmtpTransport,
}
#[cfg(not(feature = "email"))]
pub struct EmailTransport {
config: EmailConfig, }
#[cfg(feature = "email")]
impl EmailTransport {
pub async fn new(config: EmailConfig) -> Result<Self> {
let smtp_transport = if config.smtp.use_ssl {
SmtpTransport::relay(&config.smtp.host)
.map_err(|e| EmailError::SmtpConnection(e.to_string()))?
.port(config.smtp.port)
.credentials(Credentials::new(
config.smtp.username.clone(),
config.smtp.password.clone(),
))
.build()
} else {
let transport_builder = SmtpTransport::relay(&config.smtp.host)
.map_err(|e| EmailError::SmtpConnection(e.to_string()))?
.port(config.smtp.port)
.credentials(Credentials::new(
config.smtp.username.clone(),
config.smtp.password.clone(),
));
if config.smtp.use_tls {
transport_builder.build()
} else {
transport_builder.build()
}
};
Ok(Self {
config,
smtp_transport,
})
}
pub async fn send_message(&self, secure_msg: &SecureMessage, from_email: &str, to_email: &str, simple_msg: &SimpleMessage) -> Result<()> {
let email_message = self.create_email_message(secure_msg, from_email, to_email, simple_msg)?;
let _response = self.smtp_transport.send(&email_message)
.map_err(|e| EmailError::SendFailed(e.to_string()))?;
tracing::debug!("Email sent successfully");
Ok(())
}
fn create_email_message(&self, secure_msg: &SecureMessage, from_email: &str, to_email: &str, simple_msg: &SimpleMessage) -> Result<Message> {
let from_mailbox = from_email.parse::<Mailbox>()
.map_err(|e| EmailError::InvalidFormat(format!("Invalid from address: {}", e)))?;
let to_mailbox = to_email.parse::<Mailbox>()
.map_err(|e| EmailError::InvalidFormat(format!("Invalid to address: {}", e)))?;
let subject = self.generate_subject(simple_msg);
let body_content = if secure_msg.encrypted_content.is_empty() {
simple_msg.content.clone()
} else {
base64::engine::general_purpose::STANDARD.encode(&secure_msg.encrypted_content)
};
let message = Message::builder()
.from(from_mailbox)
.to(to_mailbox)
.subject(subject)
.singlepart(SinglePart::builder()
.header(header::ContentType::TEXT_PLAIN)
.body(body_content))
.map_err(|e| EmailError::InvalidFormat(e.to_string()))?;
Ok(message)
}
fn generate_subject(&self, simple_msg: &SimpleMessage) -> String {
match simple_msg.message_type {
MessageType::ToolCall => format!("[Synapse Tool Call] {} → {}", simple_msg.from_entity, simple_msg.to),
MessageType::ToolResponse => format!("[Synapse Tool Response] {} → {}", simple_msg.from_entity, simple_msg.to),
MessageType::System => format!("[Synapse System] {} → {}", simple_msg.from_entity, simple_msg.to),
MessageType::Broadcast => format!("[Synapse Broadcast] {}", simple_msg.from_entity),
MessageType::StreamChunk => format!("[Synapse Stream] {} → {}", simple_msg.from_entity, simple_msg.to),
MessageType::Direct => {
let words: Vec<&str> = simple_msg.content.split_whitespace().take(5).collect();
let preview = words.join(" ");
let preview = if simple_msg.content.split_whitespace().count() > 5 {
format!("{}...", preview)
} else {
preview
};
format!("[Synapse] {}", preview)
}
}
}
pub async fn receive_messages(&self) -> Result<Vec<SynapseEmailMessage>> {
tracing::debug!("Checking for new messages via IMAP simulation");
let messages = Vec::new();
tracing::debug!("Retrieved {} messages from IMAP", messages.len());
Ok(messages)
}
pub async fn receive_messages_imap(&self) -> Result<Vec<SynapseEmailMessage>> {
tracing::info!("Attempting IMAP connection to {}", self.config.imap.host);
tracing::warn!("Full IMAP implementation requires async-imap dependency");
Ok(Vec::new())
}
pub fn is_smtp_configured(&self) -> bool {
!self.config.smtp.username.is_empty()
&& !self.config.smtp.password.is_empty()
&& !self.config.smtp.host.is_empty()
&& self.config.smtp.port > 0
}
pub fn is_imap_configured(&self) -> bool {
!self.config.imap.username.is_empty()
&& !self.config.imap.password.is_empty()
&& !self.config.imap.host.is_empty()
&& self.config.imap.port > 0
}
pub fn is_configured(&self) -> bool {
self.is_smtp_configured() && self.is_imap_configured()
}
pub async fn start(&self) -> Result<()> {
Ok(())
}
pub async fn stop(&self) -> Result<()> {
Ok(())
}
}
#[cfg(not(feature = "email"))]
impl EmailTransport {
pub async fn new(config: EmailConfig) -> Result<Self> {
Ok(Self { _config: config })
}
pub async fn send_message(&self, _message: &SimpleMessage) -> Result<()> {
Err(crate::error::SynapseError::Anyhow("Email feature not enabled".to_string()))
}
pub async fn receive_messages(&self) -> Result<Vec<SimpleMessage>> {
Ok(Vec::new())
}
pub fn is_smtp_configured(&self) -> bool {
false
}
pub fn is_imap_configured(&self) -> bool {
false
}
}
#[derive(Debug, Clone)]
pub struct SynapseEmailMessage {
pub from_entity: String,
pub to_entity: String,
pub content: String,
pub message_type: String,
pub encrypted: bool,
pub signed: bool,
pub request_id: Option<String>,
pub metadata: HashMap<String, String>,
}
impl SynapseEmailMessage {
pub fn to_simple_message(&self) -> Result<SimpleMessage> {
let message_type = match self.message_type.as_str() {
"direct" => MessageType::Direct,
"tool_call" => MessageType::ToolCall,
"tool_response" => MessageType::ToolResponse,
"system" => MessageType::System,
"broadcast" => MessageType::Broadcast,
"stream_chunk" => MessageType::StreamChunk,
_ => MessageType::Direct,
};
let simple_msg = SimpleMessage {
to: self.to_entity.clone(),
from_entity: self.from_entity.clone(),
content: self.content.clone(),
message_type,
metadata: self.metadata.clone(),
};
Ok(simple_msg)
}
}
#[cfg(all(test, feature = "email"))]
mod tests {
use super::*;
#[test]
fn test_subject_generation() {
let transport = create_test_transport();
let tool_call = SimpleMessage {
to: "FileSystem".to_string(),
from_entity: "Claude".to_string(),
content: "list_files /home".to_string(),
message_type: MessageType::ToolCall,
metadata: HashMap::new(),
};
let subject = transport.generate_subject(&tool_call);
assert_eq!(subject, "[Synapse Tool Call] Claude → FileSystem");
let direct_msg = SimpleMessage {
to: "Eric".to_string(),
from_entity: "Claude".to_string(),
content: "Hello! How can I help you today?".to_string(),
message_type: MessageType::Direct,
metadata: HashMap::new(),
};
let subject = transport.generate_subject(&direct_msg);
assert_eq!(subject, "[Synapse] Hello! How can I help...");
}
fn create_test_transport() -> EmailTransport {
let config = EmailConfig {
smtp: crate::types::SmtpConfig {
host: "localhost".to_string(),
port: 587,
username: "test@localhost".to_string(),
password: "test".to_string(),
use_tls: false,
use_ssl: false,
},
imap: crate::types::ImapConfig {
host: "localhost".to_string(),
port: 993,
username: "test@localhost".to_string(),
password: "test".to_string(),
use_ssl: false,
},
};
EmailTransport {
config,
smtp_transport: SmtpTransport::unencrypted_localhost(),
}
}
}