]> diplodocus.org Git - nmh/blob - docs/imap/DESIGN
expect this commit to be force-replaced periodically
[nmh] / docs / imap / DESIGN
1 #+TITLE: IMAP Synchronization for nmh
2
3 * Status
4
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.
8
9 All functionality behind --enable-experimental-imap-folder-manager
10 configuration flag.
11
12 * Rationale
13
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:
16
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.
22
23 We can use IMAP to make nmh a first-class citizen in the modern mail world.
24
25 * Goals
26
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.
45
46 ** Deferred goals
47
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...
57
58 * Approach
59
60 ** Architecture
61
62 *** Managed folders
63
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.
67
68 "Minimize impact to the nmh code-base" means continuing to represent folders
69 as directories and messages as numbered files within.
70
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.
77
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.
82
83 *** Source of truth
84
85 "Treat the IMAP server as the source of truth" gives us a simple and
86 robust model.
87
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=.
94
95 ** Language
96
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.
100
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.
105
106 Since Rust supports the C ABI, we can seamlessly embed Rust into nmh programs.
107 Why not give it a shot?
108
109 In this case it's even easier: write the folder manager in Rust.
110
111 * Implementation
112
113 ** Managed folders
114
115 *** New profile parameters
116
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
119 folder manager.
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.
122
123 *** Starting the folder manager
124
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.
127 3. ~pipe(2)~
128 4. ~fork~
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
133
134 *** Protocol
135
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.
138
139 Before refining this protocol, we should look into IPC.
140
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.
145
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.
148
149 **** Common messages
150
151 The following common message types are defined:
152
153 ***** Folder
154
155 This message contains only a single folder name.
156
157 #+BEGIN_EXAMPLE
158 folder:...
159 #+END_EXAMPLE
160
161 ***** Mark
162
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.
165
166 #+BEGIN_EXAMPLE
167 folder:...
168 msgs:...
169 seqs:...
170 #+END_EXAMPLE
171
172 ***** Empty response
173
174 Many operations have nothing else to respond with but status and optional error message.
175
176 #+BEGIN_EXAMPLE
177 status:OK | NOTOK
178 error:error message present only if status:NOTOK
179 #+END_EXAMPLE
180
181 **** inc
182
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.
187
188 ***** request
189
190 The =inc= request is =op:inc= followed by the folder message:
191
192 #+BEGIN_EXAMPLE
193 op:inc
194 folder:...
195 #+END_EXAMPLE
196
197 ***** response
198
199 THe inc response is a series of messages.
200
201 First, one inc message response per message:
202
203 #+BEGIN_EXAMPLE
204 status:INC_FOLDER
205 folder:...
206 msg:N
207 unseen:N
208 #+END_EXAMPLE
209
210 Finally a status:OK empty response indicates completion.
211
212 **** mark-add / mark-del
213
214 These operations are inverse of each other, so request and response are the same.
215
216 ***** request
217
218 #+BEGIN_EXAMPLE
219 op:mark-(add / del)
220 folder:...
221 msgs:...
222 seqs:...;
223 #+END_EXAMPLE
224
225 ***** response
226
227 The empty response.
228
229 **** TODO the rest
230
231 ** IMAP
232
233 *** New profile parameters
234
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
243
244 *** Folder store
245
246 imap-folder-manager needs to store the following per folder (mailbox):
247
248 - forward and reverse map of message UID and local message number
249 - mailbox UIDNEXT
250 - mailbox UIDVALIDITY
251
252 We store UIDNEXT and UIDVALIDITY in plain text files. For the UID maps, we
253 need something fast.
254
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.
258
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.
263
264 *** mhimap - a new nmh command for manipulating the imap-folder-manager
265
266 Currently, the only facilities this offers are for the tests. They are:
267
268 - start
269 - shutdown
270 - noop
271 - check
272
273 =shutdown= may be useful to users who don't want to leave the authenticated
274 process running indefinitely.
275
276 =check= verifies the integrity of the synchronization database.
277
278 *** Code organization
279
280 **** /sbr/rust - crate nmh
281
282 =nmh= is the base crate. It offers Rusty wrappers around the core subroutines
283 in the =sbr= directory (=libmh.a=).
284
285 **** /managedfolders
286
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.
290
291 **** /imap/types
292
293 Exposes types common to the 4 IMAP crates.
294
295 **** /imap/store
296
297 This crate implements Folder store described above.
298
299 **** /imap/folder-manager
300
301 This is =imap-folder-manager= itself.
302
303 **** /imap/mhimap
304
305 This is the =mhimap= command described above.
306
307 ** Dependencies
308
309 *** Direct
310 libc = 0.2.78
311 lmdb-rkv = 0.14
312 lmdb-rkv-sys = 0.11
313 log = 0.4
314
315 *** Indirect
316 bitflags = 1.2.1
317 byteorder = 1.3
318 cc = 1.0.60
319 cfg-if = 0.1.10
320 pkg-config = 0.3.18
321
322 *** Unstable
323 cfg-if = "0.1.10"
324 libc = "0.2.78"
325 lmdb-rkv = "0.14.0"
326 lmdb-rkv-sys = "0.11.0"
327 log = "0.4.13"
328 pkg-config = "0.3.18"
329
330 *** License
331 pkg-config MIT/Apache-2.0
332 bitflags MIT/Apache-2.0
333 byteorder MIT
334 cc MIT/Apache-2.0
335 cfg-if MIT/Apache-2.0
336 libc MIT OR Apache-2.0
337 lmdb-rkv Apache-2.0
338 lmdb-rkv-sys Apache-2.0
339 log MIT OR Apache-2.0
340
341 * Testing
342
343 Look in the =testing= directory beside this file for more information.
344
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.
348
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=.
353
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
356 test suite.
357
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".
361
362 * Supported servers somewhere
363
364 ** Free implementations
365
366 - Cyrus
367 - Dovecot
368 - Panda
369
370 ** Service providers
371
372 - Gandi (they seem to run Dovecot)
373 - GMail