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