1 #+TITLE: IMAP Synchronization for nmh
5 An EXPERIMENTAL, almost-entirely-happy-path-only partial implementation
6 exists. Most of the nmh commands that are within the scope of the project
7 have not yet been updated.
9 All functionality behind --enable-experimental-imap-folder-manager
14 The world is now mobile-first, and many of are are mobile-only, or nearly so.
15 When my smart phone took over my life, two things happened to my nmh usage:
17 - At work, I was frustrated constantly by messages read under nmh being unread
18 on my phone and vice versa.
19 - In my personal life, that same frustration drove me away from nmh completely.
20 Whenever I need to reply to email from my computer, I find my self
21 incorporating months of messages.
23 We can use IMAP to make nmh a first-class citizen in the modern mail world.
27 - Treat the IMAP server as the source of truth.
28 - Minimize startup penalty to nmh commands.
29 - Minimize the impact to the nmh code-base.
30 - Minimize round-trips to the server.
31 - Enhance =inc= to incorporate mail from a hierarchy of IMAP mailboxes.
32 - Enhance =inc= to delete mail locally that has been deleted on the server.
33 - Enhance all commands interpreting the user's =Unseen-Sequence= (i.e. all those
34 taking message specifications (=mh-sequence(5)=), e.g. =mark=, =pick=, =scan=,
35 =show=) to interpret references to the =Unseen-Sequence= as all messages not
36 flagged as =\Seen= on the server.
37 - Enhance =scan= output to reflect live =\Seen= status when it is asked to
38 display whether messages appear in the user's =Unseen-Sequence=.
39 - Enhance =show= to set the =\Seen= flag on messages shown.
40 - Enhance =mark= to set or clear the =\Seen= flag for messages removed from or
41 added to the user's =Unseen-Sequence=, respectively.
42 - Enhance =rmm= to delete messages from the server.
43 - Enhance =refile= to move messages on the server.
44 - Enhance =comp= and =repl= to allow usage of a server-side draft folder.
48 - Off-line mode might be useful
49 - Synchronizing flags other than =\Seen=
50 - Storing sequences on the server (possibly the same problem as the previous)
51 - IMAP defines a message UID as an unsigned 32-bit integer, allowing for
52 4,294,967,295 messages, but nmh uses SIGNED =int=, which is 32 bits on
53 amd64, allowing for only 2,147,483,647 messages. It's not clear any file
54 system offers reasonable performance with directories of that size, so we
55 may need to redesign nmh folders before the =int= becomes a problem.
56 Assuming any of this is a practical concern...
64 "Minimize startup penalty" and "Minimize round-trips" suggest we start a
65 separate process which can keep state between nmh commands. Each nmh command
66 does not have to can pay the relatively expensive TLS connection setup cost.
68 "Minimize impact to the nmh code-base" means continuing to represent folders
69 as directories and messages as numbered files within.
71 To solve those, we introduce managed folders, which are folders in the nmh
72 mail store managed by a folder manager. When the user requests an operation
73 on a managed folder, we start by informing the folder manager of the operation
74 to be taken. In some cases, the folder does nearly all the work. In other
75 cases, it simply saves some information from the server where the rest of nmh
76 can easily find it and then gets out of the way.
78 For example, =scan= hands the user-specified message sequences to the folder
79 manager, which consults the IMAP server and stores the results in a special
80 sequence, and then the rest of =scan= as we all know and love it takes over,
81 blissfully unaware the folder is special.
85 "Treat the IMAP server as the source of truth" gives us a simple and
88 If a user =rmm= a message, we first delete it from the server. If that fails,
89 we report the failure to the user and do not proceed. The message remains in
90 both the nmh folder and on the IMAP server. If deletion from the server
91 succeeds, but we fail to delete locally, we still report an error, but the
92 message is now treated as though some other client had deleted it from the
93 server: we will delete it on the next =inc=.
97 I've programmed in C and C++ for most of my career and I've just about had it
98 with unsafe languages. The thought of implementing IMAP synchronization in C
99 discouraged me from even starting for years.
101 In my last position at Google, my fellow tech lead and I would often joke
102 about dividing the team in two, with one team continuing to chase down a
103 memory corruption error we'd failed for months to find, and the other
104 rewriting the system in Rust. It was a joke, but the idea stuck with me.
106 Since Rust supports the C ABI, we can seamlessly embed Rust into nmh programs.
107 Why not give it a shot?
109 In this case it's even easier: write the folder manager in Rust.
115 *** New profile parameters
117 - =Managed-Folders= is a space-separated list of folders to be managed.
118 Each folder must have a corresponding =[folder]-manager= component naming the
120 - =[folder]-manager= where =folder= is the name of the folder manager to start.
121 - [folder]-socket= is the path of the UNIX domain socket.
123 *** Starting the folder manager
125 1. Try to connect to running manager through =[folder]-socket=. If that works, done!
126 2. If error was not ~ENOENT~, ~unlink(2)~ the stale socket.
129 5. child - folder-manager; prints to stdout if successfully started (e.g. connected to IMAP)
130 5. parent - read from pipe to child
131 6. parent - if error, die
132 7. parent - else, time to connect again! this time, die if error
136 The protocol isn't the best, and we might want to replace it at some point, but
137 I have no plans to do that until finishing the first feature set.
139 Before refining this protocol, we should look into IPC.
141 nmh commands and the folder manager communicate over a UNIX domain socket
142 configured in the MH profile. An nmh command opens a connection to the folder
143 manager and sends it a message encoded as a standard mail message header.
144 The folder manager replies in the same format.
146 Unlike in mail messages, these messages MUST start with a =len= component whose
147 value is the total length of the messgae including the =len= component itself.
151 The following common message types are defined:
155 This message contains only a single folder name.
163 This message contains a folder in which to operate, specifications of which
164 messages to pick and sequences to add to or remove from.
174 Many operations have nothing else to respond with but status and optional error message.
178 error:error message present only if status:NOTOK
183 =inc= sends the request to the folder manager, which opens the local database to
184 see which which messages exist, sends IMAP =FETCH= requests (up to
185 =max-fetch-messages-in-flight= at a time), stores the messages, and sends one
186 response back to =inc= per message, so =inc= can scan we go.
190 The =inc= request is =op:inc= followed by the folder message:
199 THe inc response is a series of messages.
201 First, one inc message response per message:
210 Finally a status:OK empty response indicates completion.
212 **** mark-add / mark-del
214 These operations are inverse of each other, so request and response are the same.
233 *** New profile parameters
235 - [folder]-host, [folder]-port - to connect
236 - [folder]-tls TLS configuration
237 - [folder]-user: username
238 - [folder]-auth: PREAUTH | SASL - how to authenticate
239 - [folder]-authservice: for XOAUTH2
240 - [folder]-saslmech: force a SASL mechanism in case nmh chooses the wrong default
241 - [folder]-db: path to the database
242 - [folder]-mailbox-root: only look for mailboxes under this root
246 imap-folder-manager needs to store the following per folder (mailbox):
248 - forward and reverse map of message UID and local message number
250 - mailbox UIDVALIDITY
252 We store UIDNEXT and UIDVALIDITY in plain text files. For the UID maps, we
255 LMDB is a simple and fast embedded memory-mapped database library with ACID
256 semantics, and seems like a reasonable choice. It might be worth trying out
257 some alternatives, but this is a fine place to start.
259 lmdb-rkv is a Rust wrapper from Mozilla which seemed like a decent choice in 2020,
260 but that was before the Great Rust Layoff, and it now seems to be abandoned.
261 It might be worth looking into taking it over, or finding an alternative, or just
262 building our own simple binding only exposing what we need.
264 *** mhimap - a new nmh command for manipulating the imap-folder-manager
266 Currently, the only facilities this offers are for the tests. They are:
273 =shutdown= may be useful to users who don't want to leave the authenticated
274 process running indefinitely.
276 =check= verifies the integrity of the synchronization database.
278 *** Code organization
280 **** /sbr/rust - crate nmh
282 =nmh= is the base crate. It offers Rusty wrappers around the core subroutines
283 in the =sbr= directory (=libmh.a=).
287 The =managed_folders= crate implements the folder-manager protocol.
288 Nothing specific to IMAP is here; if anyone wanted to implement a different
289 folder manager, they could use this.
293 Exposes types common to the 4 IMAP crates.
297 This crate implements Folder store described above.
299 **** /imap/folder-manager
301 This is =imap-folder-manager= itself.
305 This is the =mhimap= command described above.
326 lmdb-rkv-sys = "0.11.0"
328 pkg-config = "0.3.18"
331 pkg-config MIT/Apache-2.0
332 bitflags MIT/Apache-2.0
335 cfg-if MIT/Apache-2.0
336 libc MIT OR Apache-2.0
338 lmdb-rkv-sys Apache-2.0
339 log MIT OR Apache-2.0
343 Look in the =testing= directory beside this file for more information.
345 - =tests/imap/test-live= exercises as much of the IMAP functionality as possible
346 against any supported server. Because we run it against all servers, any test
347 that can live here should live here.
349 - New tests using the common nmh test suite, run under =make check=.
350 These tests use the classic Panda IMAP server. =make check= also includes
351 =tests/imap/test-live=, against Panda only. Add test cases at this level only
352 if they can't be written for =test-live=.
354 - Rust "integration tests", which go in the =tests= directory of each crate.
355 Add test cases at this level only if they can't be written for the common nmh
358 - Rust "unit tests", which go in =tests= sub-modules under the =src= directory
359 of each crate. Add test cases at this level only if they can't be written as
360 Rust "integration tests".
362 * Supported servers somewhere
364 ** Free implementations
372 - Gandi (they seem to run Dovecot)