forked from mjl-/mox
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdevelop.txt
More file actions
444 lines (377 loc) · 19.8 KB
/
Copy pathdevelop.txt
File metadata and controls
444 lines (377 loc) · 19.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
This file has notes useful for contributors.
# Building & testing
For a full build, you'll need a recent Go compiler/toolchain and nodejs/npm for
the frontend. Run "make build" to do a full build. Run "make test" to run the
test suite. With docker installed, you can run "make test-integration" to start
up a few mox instances, a dns server, a postfix instance, and send email
between them.
The mox localserve command is a convenient way to test locally. Most of the
code paths are reachable/testable with mox localserve, but some use cases will
require a full setup.
Before committing, run at least "make fmt" and "make check" (which requires
staticcheck and ineffassign, run "make install-staticcheck install-ineffassign"
once). Also run "make check-shadow" and fix any shadowed variables other than
"err" (which are filtered out, but causes the command to always exit with an
error code; run "make install-shadow" once to install the shadow command). If
you've updated RFC references, run "make" in rfc/, it verifies the referenced
files exist.
When making changes to the public API of a package listed in
apidiff/packages.txt, run "make genapidiff" to update the list of changes in
the upcoming release (run "make install-apidiff" once to install the apidiff
command).
New features may be worth mentioning on the website, see website/ and
instructions below.
# Code style, guidelines, notes
- Keep the same style as existing code.
- For Windows: use package "path/filepath" when dealing with files/directories.
Test code can pass forward-slashed paths directly to standard library functions,
but use proper filepath functions when parameters are passed and in non-test
code. Mailbox names always use forward slash, so use package "path" for mailbox
name/path manipulation. Do not remove/rename files that are still open.
- Use github.com/mjl-/adns for DNS where possible, it returns whether DNS
results are DNSSEC-verified. Keep in mind that some code paths don't use "adns",
e.g. HTTP requests (which go through the standard library, and we can't replace
the DNS library from the standard library, and we couldn't get the
DNSSEC-verification status from the "http" package anyway.
- We don't have an internal/ directory, really just to prevent long paths in
the repo, and to keep all Go code matching *.go */*.go (without matching
vendor/). Part of the packages are reusable by other software. Those reusable
packages must not cause mox implementation details (such as bstore) to get out,
which would cause unexpected dependencies. Those packages also only expose the
standard slog package for logging, not our mlog package. Packages not intended
for reuse do use mlog as it is more convenient. Internally, we always use
mlog.Log to do the logging, wrapping an slog.Logger.
- The code uses panic for error handling in quite a few places, including
smtpserver, imapserver and web API calls. Functions/methods, variables, struct
fields and types that begin with an "x" indicate they can panic on errors. Both
for i/o errors that are fatal for a connection, and also often for user-induced
errors, for example bad IMAP commands or invalid web API requests. These panics
are caught again at the top of a command or top of the connection. In general,
write code that is panic-safe, using defer to clean up and release resources.
- Try to check all errors, at the minimum using mlog.Log.Check() to log an error
at the appropriate level. Also when just closing a file. Log messages sometimes
unexpectedly point out latent issues. Only when there is no point in logging,
for example when previous writes to stderr failed, can error logging be skipped.
Test code is less strict about checking errors.
- See the Makefile for commonly used command invocations, e.g. for tests,
various checks, formatting.
- Keep the number of dependencies limited. Certainly protocol (especially
email) handling code should not be in an external dependency. By maintaining
that code ourselves, it's easier to ensure we don't introduce unexpected
changes in behaviour, and it allows us to thoroughly document our code against
the RFCs.
- Mox can be compiled deterministically with the "go install" command (with
"CGO_ENABLED=0 go install -trimpath"). The frontend code is written in
typescript. To get working binaries after "go install", the generated/compiled
javascript code is also committed in the repository. The frontend machinations
should be kept simple, with a straightforward translation from typescript files
to javascript.
- Write tests for high-level functional scenario's at a minimum, especially for
protocols. For server code, write the implementation so a server can be
started and driven from tests. This is relatively easy because of the bstore
database library used: All state is written to a simple single-file database
file that can easily be removed between test runs. Also write a client. That
will deepen your understanding of the protocol, and makes it easier to find
corner cases to test for. Keep long-term maintainability into account. Of both
this implementations, and the wider protocol/ecosystem.
- Protocol/standards-related code is heavily cross-referenced with RFCs, by
placing comments such as "Command: ../rfc/9051:4330 ../rfc/3501:2992
../rfc/7162:864" in the code. These reference line numbers in multiple RFCs.
Your editor/IDE may support opening them by clicking on them. Because these
references are on a single line, we consider them related. In this case, it's a
single command with references to its description in multiple versions of IMAP.
Run "make" in ./rfc to download RFCs and adding references back to the code.
See https://www.xmox.nl/xr/dev/ for an online version of cross-referenced RFCs
and code. The RFCs also point back to the code where the RFC-references have
been turned into links back to RFCs. It's easy to navigate between code and
(historic) versions of RFCs. Ideally, you can read the RFC for a
protocol/format, and see that all requirements/behaviour are implemented, and
where the code is that handles it. If annotations start with "// todo", "//
todo <word>:", the RFCs are annotated with "todo" or "todo <word>" to indicate
that functionality/behaviour may not be fully implemented.
- When creating a PR, it helps to explain your considerations, including which
choices you face and how you came to decisions. Ensure you update all
relevant facets of the change. For example, when adding support for a new
IMAP-related feature, you may also have to add similar functionality to the
webmail interface.
- Write documentation so packages stay readable at
https://pkg.go.dev/github.com/mjl-/mox.
- Keep mechanisms as simple, standalone and with as few machinations as
possible. Automate where possible. Don't add dependencies on external services.
This helps long-term maintainability. For example, the "help" output of commands
are generated when building and included as documentation, so we can use
pkg.go.dev for documentation. The mox website also contains a generated copy of
the help output. We don't have to sync it manually, or remember to update it in
multiple places.
# Storage
Email messages are stored in files. Each message is in its own file. We don't
use the mbox format (too much rewriting of data) or maildir format (better than
mbox, but too limited for efficient handling of modern server requirements). We
explicitly don't want other applications touching files in the directory that
holds an accounts messages. Users should use standard protocols (e.g. IMAP) or
the web interface to access email messages. Storage is per user account (that
can have multiple addresses). All email message files are kept in a per-account
directory, with subdirectories so they are grouped in sets of 8k (for
reasonable directory listing performance). The email message file only contains
the message data. Headers added by the SMTP server are not in this message
file, but in the message index database file.
Each account has a bstore database file with an index of the database messages.
The email message files are named after IDs of records of the "Message" table
in the database. The Message table also has all other metadata information
about a message. Such as which mailbox it belongs to, and message flags (e.g.
"seen" (also known as "read"), "junk", "non-junk"), various IMAP and
synchronisation-related metadata (e.g. IMAP's UID, MODSEQ), and
reputation-related information used for spam classification (e.g. the verified
DKIM/SPF domains of the message, the originating IP address).
Bstore database files are built on bbolt database files (like LMDB but in Go;
transactional multi-bucket key/value b+trees in a single file, accessed by an
in-process library (like sqlite) instead of a separate application (like
PostgreSQL)). Bstore adds higher-level functionality:
referential/unique/nonzero constraints, (multikey) indices, automatic schema
management based on Go types and struct tags, and a query API. Bstore has no
higher-level concepts such as joins (but those aren't typically needed) or a
query language (the query API and getting/updating/deleting individual records
by primary key are enough).
Bbolt has known limitations. One relevant limitation is that writes are kept in
memory until the end of the transaction. For large transactions where lots of
data changes (e.g. when importing lots of messages, or updating all records in
a large table), you will need to make changes in multiple transactions,
committing partial work (and possibly adding a mechanism to undo the partial
work). We want to keep working on machines with little memory too.
See package "store" for details about storage in mox. See
https://pkg.go.dev/github.com/mjl-/bstore for details about the bstore database
files/library.
# Reusable packages
Most non-server Go packages are meant to be reusable. This means internal
details are not exposed in the API, and we don't make unneeded changes. We can
still make breaking changes when it improves mox: We don't want to be stuck
with bad API. Third party users aren't affected too seriously due to Go's
minimal version selection. The reusable packages are in apidiff/packages.txt.
We generate the incompatible changes with each release.
# Web interfaces/frontend
The web interface frontends (for webmail/, webadmin/ and webaccount/) are
written in strict TypeScript. The web API is a simple self-documenting
HTTP/JSON RPC API mechanism called sherpa,
https://www.ueber.net/who/mjl/sherpa/. The web API exposes types and functions
as implemented in Go, using https://github.com/mjl-/sherpa. API definitions in
JSON form are generated with https://github.com/mjl-/sherpadoc. Those API
definitions are used to generate TypeScript clients with by
https://github.com/mjl-/sherpats/.
The JavaScript that is generated from the TypeScript is included in the
repository. This makes it available for inclusion in the binary, which is
practical for users, and desirable given Go's reproducible builds. When
developing, run "make" to also build the frontend code. Run "make
install-frontend" once to install the TypeScript compiler into ./node_modules/.
There are no other external (runtime or devtime) frontend dependencies. A
light-weight abstraction over the DOM is provided by ./lib.ts. A bit more
manual UI state management must be done compared to "frameworks", but it is
little code, and this allows JavaScript/TypeScript developer to quickly get
started. UI state is often encapsulated in a JavaScript object with a
TypeScript interface exposing a "root" HTMLElement that is added to the DOM,
and functions for accessing/changing the internal state, keeping the UI
manageable.
# Website
The content of the public website at https://www.xmox.nl is in website/, as
markdown files. The website HTML is generated with "make genwebsite", which
writes to website/html/ (files not committed). The FAQ is taken from
README.md, the protocol support table is generated from rfc/index.txt. The
website is kept in this repository so a commit can change both the
implementation and the documentation on the website. Some of the info in
README.md is duplicated on the website, often more elaborate and possibly with
a slightly less technical audience. The website should also mostly be readable
through the markdown in the git repo.
Large files (images/videos) are in https://github.com/mjl-/mox-website-files to
keep the repository reasonably sized.
The public website may serve the content from the "website" branch. After a
release, the main branch (with latest development code and corresponding
changes to the website about new features) is merged into the website branch.
Commits to the website branch (e.g. for a news item, or any other change
unrelated to a new release) is merged back into the main branch.
# TLS certificates
https://github.com/cloudflare/cfssl is useful for testing with TLS
certificates. Create a CA and configure it in mox.conf TLS.CA.CertFiles, and
sign host certificates and configure them in the listeners TLS.KeyCerts.
Setup a local CA with cfssl, run once:
```sh
go install github.com/cloudflare/cfssl/cmd/cfssl@latest
go install github.com/cloudflare/cfssl/cmd/cfssljson@latest
mkdir -p local/cfssl
cd local/cfssl
cfssl print-defaults config > ca-config.json # defaults are fine
# Based on: cfssl print-defaults csr > ca-csr.json
cat <<EOF >ca-csr.json
{
"CN": "mox ca",
"key": {
"algo": "ecdsa",
"size": 256
},
"names": [
{
"C": "NL"
}
]
}
EOF
cfssl gencert -initca ca-csr.json | cfssljson -bare ca - # Generate ca key and cert.
# Generate wildcard certificates for one or more domains, add localhost for use with pebble, see below.
domains="moxtest.example localhost"
for domain in $domains; do
cat <<EOF >wildcard.$domain.csr.json
{
"key": {
"algo": "ecdsa",
"size": 256
},
"names": [
{
"O": "mox"
}
],
"hosts": [
"$domain",
"*.$domain"
]
}
EOF
cfssl gencert -ca ca.pem -ca-key ca-key.pem -profile=www wildcard.$domain.csr.json | cfssljson -bare wildcard.$domain
done
```
Now configure mox.conf to add the cfssl CA root certificate:
```
TLS:
CA:
AdditionalToSystem: true
CertFiles:
# Assuming local/<env>/config/mox.conf and local/cfssl/.
- ../../cfssl/ca.pem
[...]
Listeners:
public:
TLS:
KeyCerts:
# Assuming local/<env>/config/mox.conf and local/cfssl/.
CertFile: ../../cfssl/wildcard.$domain.pem
KeyFile: ../../cfssl/wildcard.$domain-key.pem
```
# ACME
https://github.com/letsencrypt/pebble is useful for testing with ACME. Start a
pebble instance that uses the localhost TLS cert/key created by cfssl for its
TLS serving. Pebble generates a new CA certificate for its own use each time it
is started. Fetch it from https://localhost:15000/roots/0, write it to a file, and
add it to mox.conf TLS.CA.CertFiles. See below.
Setup pebble, run once:
```sh
go install github.com/letsencrypt/pebble/cmd/pebble@latest
mkdir -p local/pebble
cat <<EOF >local/pebble/config.json
{
"pebble": {
"listenAddress": "localhost:14000",
"managementListenAddress": "localhost:15000",
"certificate": "local/cfssl/localhost.pem",
"privateKey": "local/cfssl/localhost-key.pem",
"httpPort": 80,
"tlsPort": 443,
"ocspResponderURL": "",
"externalAccountBindingRequired": false
}
}
EOF
```
Start pebble, this generates a new temporary pebble CA certificate:
```sh
pebble -config local/pebble/config.json
```
Write new CA bundle that includes pebble's temporary CA cert:
```sh
export CURL_CA_BUNDLE=local/ca-bundle.pem # for curl
export SSL_CERT_FILE=local/ca-bundle.pem # for go apps
cat /etc/ssl/certs/ca-certificates.crt local/cfssl/ca.pem >local/ca-bundle.pem
curl https://localhost:15000/roots/0 >local/pebble/ca.pem # fetch temp pebble ca, DO THIS EVERY TIME PEBBLE IS RESTARTED!
cat /etc/ssl/certs/ca-certificates.crt local/cfssl/ca.pem local/pebble/ca.pem >local/ca-bundle.pem # create new list that includes cfssl ca and temp pebble ca.
rm -r local/*/data/acme/keycerts/pebble # remove existing pebble-signed certs in acme cert/key cache, they are invalid due to newly generated temp pebble ca.
```
Edit mox.conf, adding pebble ACME and its ca.pem:
```
ACME:
pebble:
DirectoryURL: https://localhost:14000/dir
ContactEmail: root@mox.example
TLS:
CA:
AdditionalToSystem: true
CertFiles:
# Assuming local/<env>/config/mox.conf and local/pebble/ca.pem and local/cfssl/ca.pem.
- ../../pebble/ca.pem
- ../../cfssl/ca.pem
[...]
Listeners:
public:
TLS:
ACME: pebble
```
For mail clients and browsers to accept pebble-signed certificates, you must add
the temporary pebble CA cert to their trusted root CA store each time pebble is
started (e.g. to your thunderbird/firefox testing profile). Pebble has no option
to not regenerate its CA certificate, presumably for fear of people using it for
non-testing purposes. Unfortunately, this also makes it inconvenient to use for
testing purposes.
# Messages for testing
For compatibility and performance testing, it helps to have many messages,
created a long time ago and recently, by different mail user agents. A helpful
source is the Linux kernel mailing list. Archives are available as multiple git
repositories (split due to size) at
https://lore.kernel.org/lkml/_/text/mirror/. The git repo's can be converted
to compressed mbox files (about 800MB each) with:
```
# 0 is the first epoch (with over half a million messages), 12 is last
# already-complete epoch at the time of writing (with a quarter million
# messages). The archives are large, converting will take some time.
for i in 0 12; do
git clone --mirror http://lore.kernel.org/lkml/$i lkml-$i.git
(cd lkml-$i.git && time ./tombox.sh | gzip >../lkml-$i.mbox.gz)
done
```
With the following "tombox.sh" script:
```
#!/bin/sh
pre=''
for rev in $(git rev-list master | reverse); do
printf "$pre"
echo "From sender@host $(date '+%a %b %e %H:%M:%S %Y' -d @$(git show -s --format=%ct $rev))"
git show ${rev}:m | sed 's/^>*From />&/'
pre='\n'
done
```
# Release process
- Gather feedback on recent changes.
- Check if dependencies need updates.
- Run "make modernize".
- Update to latest publicsuffix/ list ("make fetch-publicsuffixlist").
- Check code if there are deprecated features that can be removed.
- Generate apidiff ("make genapidiff") and check if breaking changes can be prevented. Update moxtools.
- Update features & roadmap in README.md and website.
- Write release notes, copy from previous.
- Build ("make build check buildall") and run tests with previous major Go release, run "make docker-release" to test building images.
- Run tests ("make test"), including with race detector ("make test-race", also with TZ= for UTC-behaviour and with -count 2 ("make test-more").
- Run integration ("make test-integration") and upgrade tests ("make test-upgrade").
- Run fuzzing tests for a while ("make fuzz").
- Deploy to test environment. Test the update instructions.
- Test mox localserve on various OSes (linux, bsd, macos, windows).
- Send and receive email through the major webmail providers, check headers.
- Send and receive email with imap4/smtp clients.
- Check DNS check admin page.
- Check with https://internet.nl.
- Move apidiff/next.txt to apidiff/<version>.txt, and create empty next.txt.
- Add release to the Latest release & News sections of website/index.md.
- Create git tag (note: "#" is comment, not title/header), push code.
- Build and publish new docker image ("make docker-release").
- Deploy update to website.
- Create new release on the github page, so watchers get a notification.
Copy/paste it manually from the tag text, and add link to download/compile
instructions to prevent confusion about "assets" github links to.
- Publish new cross-referenced code/rfc to www.xmox.nl/xr/.
- Update moxtools with latest version.
- Update implementations support matrix.
- Publish signed release notes for updates.xmox.nl and update DNS record.