use std::collections::HashMap;
use std::sync::Arc;
use futures::StreamExt;
use indexmap::IndexMap;
use tokio::sync::Mutex;
use melib::backends::{BackendEventConsumer, EnvelopeHashBatch, FlagOp, IsSubscribedFn, MailBackend};
use melib::conf::AccountSettings;
use melib::email::address::MessageID;
use melib::email::attachment_types::{ContentType, Text};
use melib::imap::ImapType;
use melib::{AccountHash, EnvelopeHash, Mail, MailboxHash};
use crate::config::Config;
use crate::models::{AttachmentData, Folder, MessageSummary};
pub struct ImapSession {
backend: Arc<Mutex<Box<ImapType>>>,
mailbox_paths: Mutex<HashMap<MailboxHash, String>>,
}
impl std::fmt::Debug for ImapSession {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ImapSession").finish_non_exhaustive()
}
}
impl ImapSession {
pub async fn connect(config: Config) -> Result<Arc<Self>, String> {
let mut extra = IndexMap::new();
extra.insert("server_hostname".into(), config.imap_server.clone());
extra.insert("server_username".into(), config.username.clone());
extra.insert("server_password".into(), config.password.clone());
extra.insert("server_port".into(), config.imap_port.to_string());
extra.insert("use_tls".into(), "true".into());
extra.insert(
"use_starttls".into(),
if config.use_starttls {
"true"
} else {
"false"
}
.into(),
);
extra.insert("danger_accept_invalid_certs".into(), "false".into());
let account_settings = AccountSettings {
name: config.username.clone(),
root_mailbox: "INBOX".into(),
format: "imap".into(),
identity: config.username.clone(),
extra,
..Default::default()
};
let is_subscribed: IsSubscribedFn =
(Arc::new(|_: &str| true) as Arc<dyn Fn(&str) -> bool + Send + Sync>).into();
let event_consumer = BackendEventConsumer::new(Arc::new(
|_account_hash: AccountHash, event: melib::backends::BackendEvent| {
log::debug!("IMAP backend event: {:?}", event);
},
));
let backend = ImapType::new(&account_settings, is_subscribed, event_consumer)
.map_err(|e| format!("Failed to create IMAP backend: {}", e))?;
let session = ImapSession {
backend: Arc::new(Mutex::new(backend)),
mailbox_paths: Mutex::new(HashMap::new()),
};
{
let backend = session.backend.lock().await;
let online_future = backend
.is_online()
.map_err(|e| format!("IMAP is_online failed: {}", e))?;
online_future
.await
.map_err(|e| format!("IMAP connection failed: {}", e))?;
}
Ok(Arc::new(session))
}
pub async fn fetch_folders(self: &Arc<Self>) -> Result<Vec<Folder>, String> {
let future = {
let backend = self.backend.lock().await;
backend
.mailboxes()
.map_err(|e| format!("Failed to request mailboxes: {}", e))?
};
let mailboxes = future
.await
.map_err(|e| format!("Failed to fetch mailboxes: {}", e))?;
let mut folders: Vec<Folder> = Vec::with_capacity(mailboxes.len());
let mut path_map = HashMap::new();
for (hash, mailbox) in &mailboxes {
let (total, unseen) = mailbox
.count()
.map_err(|e| format!("Failed to get mailbox count: {}", e))?;
path_map.insert(*hash, mailbox.path().to_string());
folders.push(Folder {
name: mailbox.name().to_string(),
path: mailbox.path().to_string(),
unread_count: unseen as u32,
total_count: total as u32,
mailbox_hash: hash.0,
});
}
folders.sort_by(|a, b| {
if a.path == "INBOX" {
std::cmp::Ordering::Less
} else if b.path == "INBOX" {
std::cmp::Ordering::Greater
} else {
a.path.cmp(&b.path)
}
});
*self.mailbox_paths.lock().await = path_map;
Ok(folders)
}
pub async fn fetch_messages(
self: &Arc<Self>,
mailbox_hash: MailboxHash,
) -> Result<Vec<MessageSummary>, String> {
let stream = {
let mut backend = self.backend.lock().await;
backend
.fetch(mailbox_hash)
.map_err(|e| format!("Failed to start fetch: {}", e))?
};
let mut stream = std::pin::pin!(stream);
let mut messages = Vec::new();
while let Some(batch_result) = stream.next().await {
let envelopes = batch_result
.map_err(|e| format!("Error fetching envelopes: {}", e))?;
for envelope in envelopes {
let from_str = envelope
.from()
.iter()
.map(|a| a.to_string())
.collect::<Vec<_>>()
.join(", ");
let to_str = envelope
.to()
.iter()
.map(|a| a.to_string())
.collect::<Vec<_>>()
.join(", ");
let msg_id = envelope.message_id().to_string();
let refs = envelope.references();
let thread_id = Some(compute_thread_id(&msg_id, refs));
let thread_depth = refs.len() as u32;
let in_reply_to = envelope
.in_reply_to()
.and_then(|r| r.refs().last().map(|id| id.to_string()));
let reply_to = envelope
.other_headers()
.get("Reply-To")
.map(|s| s.to_string());
messages.push(MessageSummary {
uid: envelope.hash().0,
subject: envelope.subject().to_string(),
from: from_str,
to: to_str,
date: envelope.date_as_str().to_string(),
is_read: envelope.is_seen(),
is_starred: envelope.flags().is_flagged(),
has_attachments: envelope.has_attachments,
thread_id,
envelope_hash: envelope.hash().0,
timestamp: envelope.timestamp as i64,
mailbox_hash: mailbox_hash.0,
message_id: msg_id,
in_reply_to,
reply_to,
thread_depth,
});
}
}
Ok(messages)
}
pub async fn set_flags(
self: &Arc<Self>,
envelope_hash: EnvelopeHash,
mailbox_hash: MailboxHash,
flags: Vec<FlagOp>,
) -> Result<(), String> {
let future = {
let mut backend = self.backend.lock().await;
backend
.set_flags(
EnvelopeHashBatch::from(envelope_hash),
mailbox_hash,
flags,
)
.map_err(|e| format!("Failed to request set_flags: {}", e))?
};
future
.await
.map_err(|e| format!("Failed to set flags: {}", e))?;
Ok(())
}
pub async fn move_messages(
self: &Arc<Self>,
envelope_hash: EnvelopeHash,
source_mailbox_hash: MailboxHash,
destination_mailbox_hash: MailboxHash,
) -> Result<(), String> {
let future = {
let mut backend = self.backend.lock().await;
backend
.copy_messages(
EnvelopeHashBatch::from(envelope_hash),
source_mailbox_hash,
destination_mailbox_hash,
true, )
.map_err(|e| format!("Failed to request move: {}", e))?
};
future
.await
.map_err(|e| format!("Failed to move message: {}", e))?;
Ok(())
}
pub async fn fetch_body(
self: &Arc<Self>,
envelope_hash: EnvelopeHash,
) -> Result<(String, String, Vec<AttachmentData>), String> {
let future = {
let backend = self.backend.lock().await;
backend
.envelope_bytes_by_hash(envelope_hash)
.map_err(|e| format!("Failed to request message bytes: {}", e))?
};
let bytes = future
.await
.map_err(|e| format!("Failed to fetch message bytes: {}", e))?;
let mail =
Mail::new(bytes, None).map_err(|e| format!("Failed to parse message: {}", e))?;
let body_attachment = mail.body();
let (text_plain, text_html, attachments) = extract_body(&body_attachment);
let plain_rendered = crate::mime::render_body(
text_plain.as_deref(),
text_html.as_deref(),
);
let markdown_rendered = crate::mime::render_body_markdown(
text_plain.as_deref(),
text_html.as_deref(),
);
Ok((markdown_rendered, plain_rendered, attachments))
}
pub async fn watch(
self: &Arc<Self>,
) -> Result<
impl futures::Stream<Item = melib::error::Result<melib::backends::BackendEvent>>,
String,
> {
let stream = {
let backend = self.backend.lock().await;
backend
.watch()
.map_err(|e| format!("Failed to start watch: {}", e))?
};
Ok(stream)
}
}
fn compute_thread_id(message_id: &str, references: &[MessageID]) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
if let Some(root) = references.first() {
root.to_string().hash(&mut hasher);
} else {
message_id.hash(&mut hasher);
}
hasher.finish()
}
fn extract_body(
att: &melib::email::attachments::Attachment,
) -> (Option<String>, Option<String>, Vec<AttachmentData>) {
let mut text_plain = None;
let mut text_html = None;
let mut attachments = Vec::new();
extract_parts(att, &mut text_plain, &mut text_html, &mut attachments);
(text_plain, text_html, attachments)
}
fn extract_parts(
att: &melib::email::attachments::Attachment,
plain: &mut Option<String>,
html: &mut Option<String>,
attachments: &mut Vec<AttachmentData>,
) {
match &att.content_type {
ContentType::Text {
kind: Text::Plain, ..
} if !att.content_disposition.kind.is_attachment() => {
let bytes = att.decode(Default::default());
let text = String::from_utf8_lossy(&bytes);
if !text.trim().is_empty() {
let combined = plain.take().unwrap_or_default() + &text;
*plain = Some(combined);
}
}
ContentType::Text {
kind: Text::Html, ..
} if !att.content_disposition.kind.is_attachment() => {
let bytes = att.decode(Default::default());
let text = String::from_utf8_lossy(&bytes);
if !text.trim().is_empty() {
let combined = html.take().unwrap_or_default() + &text;
*html = Some(combined);
}
}
ContentType::Multipart { parts, .. } => {
for part in parts {
extract_parts(part, plain, html, attachments);
}
}
_ => {
let filename = att
.filename()
.or_else(|| att.content_disposition.filename.clone())
.unwrap_or_else(|| "unnamed".into());
attachments.push(AttachmentData {
filename,
mime_type: att.content_type.to_string(),
data: att.decode(Default::default()),
});
}
}
}