Skip to main content

rmut_session/
worker.rs

1//! The IMAP connection, on a thread of its own.
2//!
3//! An IMAP conversation is one command at a time over one socket, and
4//! every one of them can take as long as the network feels like. Run
5//! on the thread that draws the screen, that is a freeze: no keys
6//! read, nothing repainted, no way out. So the connection lives here
7//! instead, behind a channel: the session sends a [`Job`], the thread
8//! runs it, and the answer comes back as a [`Done`].
9//!
10//! What the session needs to know cheaply and constantly (which
11//! account, which folder, where the cache is) is not down the
12//! channel; it is in [`Facts`], kept beside the handle.
13
14use std::path::PathBuf;
15use std::sync::mpsc::{Receiver, RecvError, Sender, TryRecvError, channel};
16use std::sync::{Arc, Mutex};
17use std::thread::JoinHandle;
18
19use anyhow::{Result, anyhow};
20use rmut_core::config::Account;
21use rmut_core::maildir::Flags;
22use rmut_core::remote::{Progress, Remote};
23
24/// What the session asks the connection to do.
25pub enum Job {
26    /// Complete the cached messages whose files hold headers only.
27    FetchBodies(Vec<PathBuf>),
28    /// A `$` sync: flag changes first, then the purge.
29    Sync {
30        flags: Vec<(PathBuf, Flags)>,
31        deletes: Vec<PathBuf>,
32    },
33    /// $trash: UID COPY before a purge.
34    CopyToFolder {
35        paths: Vec<PathBuf>,
36        mailbox: String,
37    },
38    /// APPEND: into a named folder, or into the account's Sent when
39    /// no name is given (an Fcc).
40    Append {
41        mailbox: Option<String>,
42        flags: Flags,
43        body: Vec<u8>,
44    },
45    /// The poll tick: has anything happened on the server?
46    CheckNew,
47    /// The folder browser's list, with UNSEEN counts.
48    Folders,
49    /// The unread counts of these folders, for a sidebar.
50    Unseen(Vec<String>),
51    /// Server-side `~b`: which UIDs hold this text.
52    SearchBody(String),
53    /// Another folder of the same account, on this connection.
54    Switch(String),
55    /// mutt's folder management on the open account.
56    Manage(Manage),
57}
58
59/// One folder-management action, its folder names already stripped of
60/// the `imap:account/` prefix.
61#[derive(Clone)]
62pub enum Manage {
63    Create(String),
64    Delete(String),
65    Rename(String, String),
66    Subscribe(String, bool),
67}
68
69impl Job {
70    /// What to say while it runs, and in anything that goes wrong.
71    pub fn what(&self) -> &'static str {
72        match self {
73            Job::FetchBodies(paths) => match paths.len() {
74                1 => "fetching the message",
75                _ => "fetching the messages",
76            },
77            Job::Sync { .. } => "syncing",
78            Job::CopyToFolder { .. } => "copying to the trash",
79            Job::Append { .. } => "saving to the server",
80            Job::CheckNew => "checking for new mail",
81            Job::Folders => "listing folders",
82            Job::Manage(_) => "managing folders",
83            Job::Unseen(_) => "counting unread",
84            Job::SearchBody(_) => "searching on the server",
85            Job::Switch(_) => "opening the folder",
86        }
87    }
88}
89
90/// What a job left behind.
91pub enum Done {
92    /// Nothing to report but success.
93    Nothing,
94    /// How many messages arrived (a check, or a switch).
95    Arrived(usize),
96    Folders(Vec<(String, usize)>),
97    /// The counts asked for, in the order they were asked for.
98    Counts(Vec<usize>),
99    Uids(Vec<u32>),
100    /// Where an append landed.
101    Folder(String),
102    /// A switch: the facts that came with the new folder. Boxed,
103    /// since an Account is far bigger than a count.
104    Switched(Box<Facts>),
105}
106
107/// What the session knows about the open folder without asking the
108/// connection. Cheap, and unchanged while a job is in flight.
109#[derive(Clone)]
110pub struct Facts {
111    /// `imap:account/mailbox`, for the status line and the browser.
112    pub spec: String,
113    pub account: Account,
114    pub mailbox: String,
115    pub cache: PathBuf,
116    /// Older UIDs a huge folder left unfetched at open; the session
117    /// hands them to the background backfill.
118    pub pending_backfill: Vec<u32>,
119}
120
121impl Facts {
122    fn of(remote: &Remote) -> Facts {
123        Facts {
124            spec: remote.spec.clone(),
125            account: remote.account.clone(),
126            mailbox: remote.mailbox.clone(),
127            cache: remote.cache.clone(),
128            pending_backfill: remote.pending_backfill.clone(),
129        }
130    }
131}
132
133/// A handle on the connection: the facts, and the thread doing the
134/// talking.
135pub struct Imap {
136    pub facts: Facts,
137    jobs: Sender<Job>,
138    answers: Receiver<Result<Done>>,
139    thread: Option<JoinHandle<()>>,
140    /// What the connection last said it was doing, written from the
141    /// thread and read whenever the session gets round to it.
142    progress: Arc<Mutex<Option<String>>>,
143    /// The job in flight, if any: what it is, for anyone drawing a
144    /// status line.
145    busy: Option<&'static str>,
146    /// A way to cut the socket short, for mutt's Ctrl+G.
147    cutoff: rmut_core::net::Cutoff,
148}
149
150impl Imap {
151    /// Take an open connection onto a thread of its own.
152    pub fn new(remote: Remote) -> Imap {
153        let facts = Facts::of(&remote);
154        let cutoff = remote.cutoff();
155        let (jobs, inbox) = channel::<Job>();
156        let (outbox, answers) = channel::<Result<Done>>();
157        let progress = Arc::new(Mutex::new(None));
158        let thread = std::thread::spawn({
159            let progress = progress.clone();
160            move || run(remote, inbox, outbox, progress)
161        });
162        Imap {
163            facts,
164            jobs,
165            answers,
166            thread: Some(thread),
167            progress,
168            busy: None,
169            cutoff,
170        }
171    }
172
173    /// mutt's Ctrl+G: cut the job in flight short. The socket goes
174    /// down, whatever was blocked on it fails, and the next job
175    /// reconnects. Does nothing when nothing is running.
176    pub fn abort(&self) {
177        if self.busy.is_some() {
178            self.cutoff.cut();
179        }
180    }
181
182    /// A progress callback that writes where [`Imap::progress`] can
183    /// read it, for the open that happens before there is a thread.
184    pub fn progress_sink(slot: &Arc<Mutex<Option<String>>>) -> Progress {
185        let slot = slot.clone();
186        Box::new(move |line: &str| {
187            if let Ok(mut slot) = slot.lock() {
188                *slot = Some(line.to_string());
189            }
190        })
191    }
192
193    /// The last thing the connection said it was doing, taken.
194    pub fn take_progress(&mut self) -> Option<String> {
195        self.progress.lock().ok().and_then(|mut slot| slot.take())
196    }
197
198    /// What the connection is doing, if anything.
199    pub fn busy(&self) -> Option<&'static str> {
200        self.busy
201    }
202
203    /// Send a job off, to be collected later.
204    pub fn start(&mut self, job: Job) -> Result<()> {
205        let what = job.what();
206        self.jobs
207            .send(job)
208            .map_err(|_| anyhow!("the connection is gone"))?;
209        self.busy = Some(what);
210        Ok(())
211    }
212
213    /// The answer, if the job is done; None while it is still running.
214    pub fn collect(&mut self) -> Option<Result<Done>> {
215        match self.answers.try_recv() {
216            Ok(done) => {
217                self.busy = None;
218                Some(self.remember(done))
219            }
220            Err(TryRecvError::Empty) => None,
221            Err(TryRecvError::Disconnected) => {
222                self.busy = None;
223                Some(Err(anyhow!("the connection is gone")))
224            }
225        }
226    }
227
228    /// Run a job and wait for it, the way the operations did when they
229    /// held the connection themselves. Every caller that learns to
230    /// collect its answer later stops calling this.
231    pub fn blocking(&mut self, job: Job) -> Result<Done> {
232        self.start(job)?;
233        self.wait()
234    }
235
236    /// Wait for the job in flight. The session uses this to settle
237    /// what it started before asking for something else: one
238    /// connection, one conversation.
239    pub fn wait(&mut self) -> Result<Done> {
240        let done = match self.answers.recv() {
241            Ok(done) => done,
242            Err(RecvError) => Err(anyhow!("the connection is gone")),
243        };
244        self.busy = None;
245        self.remember(done)
246    }
247
248    /// A switch changes the facts; keep them in step.
249    fn remember(&mut self, done: Result<Done>) -> Result<Done> {
250        if let Ok(Done::Switched(facts)) = &done {
251            self.facts = (**facts).clone();
252        }
253        done
254    }
255
256    /// The backfill takes the UIDs the open left over; they are only
257    /// handed out once.
258    pub fn take_backfill(&mut self) -> Vec<u32> {
259        std::mem::take(&mut self.facts.pending_backfill)
260    }
261}
262
263impl Drop for Imap {
264    fn drop(&mut self) {
265        // Closing the channel ends the loop after whatever it is in
266        // the middle of; the LOGOUT is the connection's own Drop.
267        let (jobs, _) = channel();
268        let _ = std::mem::replace(&mut self.jobs, jobs);
269        if let Some(thread) = self.thread.take() {
270            let _ = thread.join();
271        }
272    }
273}
274
275/// The thread: one job at a time, in the order they were asked for.
276fn run(
277    mut remote: Remote,
278    jobs: Receiver<Job>,
279    answers: Sender<Result<Done>>,
280    progress: Arc<Mutex<Option<String>>>,
281) {
282    remote.set_progress(Imap::progress_sink(&progress));
283    while let Ok(job) = jobs.recv() {
284        let what = job.what();
285        let done = do_job(&mut remote, job).map_err(|err| err.context(what));
286        if let Ok(mut slot) = progress.lock() {
287            *slot = None;
288        }
289        if answers.send(done).is_err() {
290            break; // nobody is listening any more
291        }
292    }
293}
294
295fn do_job(remote: &mut Remote, job: Job) -> Result<Done> {
296    match job {
297        Job::FetchBodies(paths) => {
298            for path in &paths {
299                remote.fetch_body(path)?;
300            }
301            Ok(Done::Nothing)
302        }
303        Job::Sync { flags, deletes } => {
304            for (path, flags) in &flags {
305                remote.push_flags(path, *flags)?;
306            }
307            if !deletes.is_empty() {
308                remote.delete(&deletes)?;
309            }
310            Ok(Done::Nothing)
311        }
312        Job::CopyToFolder { paths, mailbox } => {
313            remote.copy_to_folder(&paths, &mailbox)?;
314            Ok(Done::Nothing)
315        }
316        Job::Append {
317            mailbox,
318            flags,
319            body,
320        } => {
321            let folder = match &mailbox {
322                Some(mailbox) => remote.append_to(mailbox, flags, &body)?,
323                None => remote.append_sent(&body)?,
324            };
325            Ok(Done::Folder(folder))
326        }
327        Job::CheckNew => Ok(Done::Arrived(remote.check_new()?)),
328        Job::Folders => Ok(Done::Folders(remote.folders()?)),
329        Job::Unseen(folders) => Ok(Done::Counts(
330            folders.iter().map(|f| remote.unseen(f)).collect(),
331        )),
332        Job::SearchBody(term) => Ok(Done::Uids(remote.search_body(&term)?)),
333        Job::Switch(mailbox) => {
334            remote.switch(&mailbox)?;
335            Ok(Done::Switched(Box::new(Facts::of(remote))))
336        }
337        Job::Manage(action) => {
338            match action {
339                Manage::Create(name) => remote.create_folder(&name)?,
340                Manage::Delete(name) => remote.delete_folder(&name)?,
341                Manage::Rename(from, to) => remote.rename_folder(&from, &to)?,
342                Manage::Subscribe(name, on) => remote.subscribe_folder(&name, on)?,
343            }
344            Ok(Done::Nothing)
345        }
346    }
347}