use crate::events::{EventBus, ServerEvent};
use crate::state::AppState;
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
use std::path::Path;
use std::time::Duration;
use tokio::sync::mpsc;
const DEBOUNCE: Duration = Duration::from_secs(2);
pub fn spawn(state: AppState) -> anyhow::Result<RecommendedWatcher> {
let root = state.store.paths().maildir_root.clone();
let (tx, rx) = mpsc::unbounded_channel();
let mut watcher = notify::recommended_watcher(move |event: notify::Result<notify::Event>| {
if let Ok(event) = event {
if is_delivery(&event) {
let _ = tx.send(());
}
}
})?;
watcher.watch(Path::new(&root), RecursiveMode::Recursive)?;
tokio::spawn(debounce_loop(state, rx));
tracing::info!(root = %root.display(), "watching maildir for delivered mail");
Ok(watcher)
}
fn is_delivery(event: ¬ify::Event) -> bool {
use notify::EventKind;
if !matches!(
event.kind,
EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_)
) {
return false;
}
event.paths.iter().any(|p| {
let text = p.to_string_lossy();
!text.contains("/.notmuch/") && !text.ends_with(".mbsyncstate") && !text.ends_with(".lock")
})
}
async fn debounce_loop(state: AppState, mut rx: mpsc::UnboundedReceiver<()>) {
while rx.recv().await.is_some() {
while let Ok(Some(())) = tokio::time::timeout(DEBOUNCE, rx.recv()).await {}
if state.read_only {
tracing::debug!("maildir changed but the server is read-only; not indexing");
continue;
}
match state.store.notmuch().index_new().await {
Ok(revision) => {
if state.own_write(&revision).await {
tracing::debug!(%revision, "maildir changed to match our own tag write");
continue;
}
if let Err(err) = state.store.refresh_index().await {
tracing::warn!(%err, "could not bring the mail index up to the new mail");
}
tracing::info!(%revision, "indexed newly delivered mail");
state.events.publish(ServerEvent::MailChanged { revision });
}
Err(err) => {
tracing::warn!(%err, "could not index delivered mail");
publish_error(&state.events, &err.to_string());
}
}
}
}
fn publish_error(bus: &EventBus, detail: &str) {
bus.publish(ServerEvent::Error {
detail: detail.to_string(),
});
}
#[cfg(test)]
mod tests {
use super::*;
use notify::event::{CreateKind, EventKind};
use std::path::PathBuf;
fn event(kind: EventKind, path: &str) -> notify::Event {
notify::Event {
kind,
paths: vec![PathBuf::from(path)],
attrs: Default::default(),
}
}
#[test]
fn a_new_maildir_file_counts_as_a_delivery() {
assert!(is_delivery(&event(
EventKind::Create(CreateKind::File),
"/Mail/main/Inbox/cur/123:2,"
)));
}
#[test]
fn notmuch_writing_its_own_database_does_not_count() {
assert!(!is_delivery(&event(
EventKind::Create(CreateKind::File),
"/Mail/.notmuch/xapian/postlist.glass"
)));
}
#[test]
fn mbsync_state_files_do_not_count() {
assert!(!is_delivery(&event(
EventKind::Create(CreateKind::File),
"/Mail/main/Inbox/.mbsyncstate"
)));
}
#[test]
fn access_events_do_not_count() {
assert!(!is_delivery(&event(
EventKind::Access(notify::event::AccessKind::Read),
"/Mail/main/Inbox/cur/123:2,"
)));
}
}