Compare commits

..

236 commits

Author SHA1 Message Date
1c2e8f0985
fix readme 2024-05-29 10:35:55 +02:00
b9ce588603
Merge branch 'caldav' 2024-05-29 10:14:51 +02:00
5954de6efb
upgrade cargo2nix 2024-05-29 10:14:16 +02:00
06a24bb559
fix DAV header for iOS 2024-05-29 09:57:34 +02:00
3a8b45a0b1
re-enable imap behavior tests 2024-05-29 08:49:56 +02:00
f9fab60e5e
test report sync-collection 2024-05-29 08:47:56 +02:00
a2f5b451bd
initial implementation of sync-collection 2024-05-28 17:21:30 +02:00
18f2154151
implement propfind sync-token 2024-05-28 16:03:25 +02:00
171a762768
implement sync multistatus extension 2024-05-28 13:59:40 +02:00
410d663a5e
add a multistatus extension entrypoint 2024-05-28 13:04:46 +02:00
10dac17ce1
fix report 2024-05-28 12:43:20 +02:00
1c9d2eab69
parse property for sync + versioning 2024-05-28 12:38:22 +02:00
5b1da2a33b
webdav sync core codec 2024-05-27 18:16:53 +02:00
418adf92be
debug support of calendar-data pruning 2024-05-27 08:03:21 +02:00
68e08bed4f
add prune logic 2024-05-26 15:31:12 +02:00
ac528d2156
test fetching pending VTODOs 2024-05-26 11:07:24 +02:00
d5a222967d
support multiple same name components, properties & parameters 2024-05-26 11:03:39 +02:00
6b9720844a
better support for time-range 2024-05-26 10:33:04 +02:00
52f870633c
add a new aero-ical module 2024-05-25 19:30:59 +02:00
ff823a10f0
improve ical date parsing 2024-05-23 10:01:43 +02:00
7687065bfc
test calendar-multiget 2024-05-23 09:24:06 +02:00
a859fe38b1
test calendar-query vevent filtering 2024-05-23 08:55:53 +02:00
54d10ed482
check calendar autodiscovery 2024-05-22 23:48:34 +02:00
a4df1a6ef1
test rfc5397 current-user-principal 2024-05-22 23:38:41 +02:00
2ca485fb87
test webdav core get, delete, update 2024-05-22 23:22:03 +02:00
e522251bec
test webdav put 2024-05-22 19:58:20 +02:00
649a7b8b1b
webdav propfind integration tests 2024-05-22 19:36:27 +02:00
742beeeafb
fix unit tests 2024-05-22 15:28:14 +02:00
6ca7082197
fix: parsing components & times 2024-05-22 15:02:53 +02:00
194e34d4e1
first full filter implementation 2024-05-22 10:05:52 +02:00
51ec1d7ff9
calendar-query filter propertiers 2024-05-21 18:09:21 +02:00
b2c75242eb
WIP filter 2024-05-16 21:47:21 +02:00
32dfd25f57
format + WIP calendar-query 2024-05-16 17:38:34 +02:00
6b9542088c
Add icalendar dependency 2024-04-30 13:02:59 +02:00
e1d7cf88af
Working ICS GET/PUT/DELETE 2024-04-24 17:35:00 +02:00
52d767edae
Parse If-{None-}Match headers 2024-04-24 11:43:57 +02:00
5d85fd16f2
basic thunderbird event is working! 2024-04-23 18:19:07 +02:00
6de63055a2
successfully return ICS in REPORT queries 2024-04-23 18:07:00 +02:00
adbccd8834
Add support for content type 2024-04-23 15:43:48 +02:00
50ce8621c2
GET implementation 2024-04-23 15:20:29 +02:00
4594e068db
PUT seems to work 2024-04-23 10:35:43 +02:00
936f851fdb
Do not silently drop an invalid frame 2024-04-21 13:47:45 +02:00
b6c656de8f
Add a virtual node CreateEventNode 2024-04-20 20:01:07 +02:00
e2bf412337
Finalize refactor 2024-04-18 16:08:10 +02:00
2bda8ef081
split dav module in multiple files 2024-04-18 13:55:57 +02:00
66eac8ec7a
Refactor DAV 2024-04-06 13:05:57 +02:00
bf7fb55965
It compiles again 2024-04-05 15:30:10 +02:00
a2d2649ef9
WIP dav integration 2024-04-05 10:19:07 +02:00
054bd52279
Implement diff 2024-04-04 15:40:26 +02:00
272b93f04a
GET logic 2024-04-04 14:59:47 +02:00
2efdd40b8e
Write PUT 2024-04-04 11:57:32 +02:00
f179479c30
WIP implem storage 2024-04-04 11:28:15 +02:00
9afbfeb427
Testing DAG sync 2024-03-27 16:16:37 +01:00
a146a0babc
Sync algorithm 2024-03-27 15:09:18 +01:00
0b57200eeb
Dav DAG wip 2024-03-27 10:33:46 +01:00
bc0f897803
Calendar Namespace 2024-03-26 15:08:04 +01:00
ed47855ef1
Share UniqueIdent between collections 2024-03-20 17:31:54 +01:00
22e4f29555
working report calendar-multiget 2024-03-20 14:46:07 +01:00
3c2d4e6987
Refactor Multistatus builder to better integrate with REPORT 2024-03-20 13:15:56 +01:00
311bc59c1b
Make Thunderbird sufficiently happy to send a REPORT 2024-03-19 18:30:26 +01:00
fb6a379f43
Working thunderbird autodetect 2024-03-19 17:36:32 +01:00
5bf3517acf
Pass thunderbird autodetect... 2024-03-18 22:56:49 +01:00
2c9ea0f09c
add support for 404 content 2024-03-18 21:44:44 +01:00
d0c47b93fe
Rework webdav types 2024-03-18 20:45:30 +01:00
2e7ffd4f4c
implement content type 2024-03-18 16:14:38 +01:00
bb0011dd17
full dav path 2024-03-18 15:33:28 +01:00
4a5ae87059
WIP DAV hierarchy 2024-03-18 15:04:46 +01:00
3b57d21e30
WIP DAV nodes 2024-03-18 12:00:40 +01:00
f372a95b01
basic propfind 2024-03-17 10:31:05 +01:00
902d33c434
bind streaming codec to hyper 1.x 2024-03-16 16:48:46 +01:00
3abdafb0db
TLS + Fix auth 2024-03-13 15:45:36 +01:00
98adb1e20d
fix caldecoder + xml 2024-03-13 09:11:52 +01:00
442433d70b
fix parsing 2024-03-12 10:18:13 +01:00
6d1f538091
Improve my XML parser 2024-03-08 22:03:46 +01:00
f50f6d68aa
finalize decoder caldav impl 2024-03-08 21:51:34 +01:00
17e42874f5
WIP decoder 2024-03-08 21:39:12 +01:00
7459f50b54
WIP implem cal decoder 2024-03-08 18:23:23 +01:00
b786573e08
Fixed 2 more bugs 2024-03-08 11:42:44 +01:00
4d65366ff3
Fixed some parsing bugs 2024-03-08 11:34:24 +01:00
b9f32d720a
Finalize Aerogramme's refactor 2024-03-08 10:20:45 +01:00
11462f80c4
Re-enable proto 2024-03-08 09:55:33 +01:00
1edf0b15ec
Re-enable collections 2024-03-08 08:43:28 +01:00
1a43ce5ac7
WIP refactor 2024-03-08 08:17:03 +01:00
bb9cb386b6
add a fuzzer 2024-03-07 15:45:05 +01:00
e52ce4a61d
Testing decoder against RFC 2024-03-07 14:25:08 +01:00
2d14587d83
Refactor decoder 2024-03-07 12:25:22 +01:00
db115ca247
successful multistatus decoding test 2024-03-07 09:49:09 +01:00
5e71a7d848
Rewrote the whole decoder 2024-03-06 23:24:54 +01:00
67e5953c24
drop anyprop as it can't be decoded 2024-03-06 20:58:41 +01:00
ce2fa5c3bc
Fix typing of Response 2024-03-06 18:35:54 +01:00
96a27d7b22
Implement lockinfo 2024-03-06 16:09:20 +01:00
05c952f020
WIP lock/propertyupdate implementation 2024-03-06 12:42:27 +01:00
ba32a0d4a6
decode errors 2024-03-06 10:12:02 +01:00
2dd6deae54
Re-enable + enhance DAV decode tests 2024-03-05 19:06:04 +01:00
1aafd752ca
Re-enable cal encoder tests 2024-03-05 18:15:03 +01:00
8fec92a086
Re-enable calendar encoder 2024-03-05 18:02:43 +01:00
f376e88c73
Restored WebDAV encoder tests 2024-03-05 16:26:15 +01:00
8e5d8a8aaa
Refactor encoder+decoder WIP (compile) 2024-03-05 16:07:47 +01:00
b7a990ecdb
Decoder is starting to work 2024-03-04 22:27:37 +01:00
c9edf6c37c
beginning... 2024-03-04 17:55:48 +01:00
ad25912a0f
Before refactoring the reader 2024-03-04 13:36:41 +01:00
4d3d1c8c19
Add new caldav test from RFC 2024-03-04 09:29:03 +01:00
352814aec9
caldav encoding test passing 2024-03-04 09:02:24 +01:00
e127ebeaa9
Still testing CalDAV 2024-03-03 13:07:22 +01:00
4276090314
WIP testing 2024-03-03 11:26:32 +01:00
463be750e1
CalEncoder should be fully implemented now 2024-03-03 11:08:00 +01:00
433e1f97f6
Param-filter encoding 2024-03-03 11:00:10 +01:00
99f8085e47
Serialize another caldav filter 2024-03-03 10:50:32 +01:00
17142bd687
WIP encoding 2024-03-02 23:01:56 +01:00
61ee5f153b
Serialize calendar-data 2024-03-02 19:01:20 +01:00
dba0dcdc41
Serialize CalDAV errors 2024-03-02 18:35:11 +01:00
2b2e3c032c
Encode Calendar Properties 2024-03-02 18:19:03 +01:00
9514af8f52
Calendar skeleton 2024-03-02 16:52:52 +01:00
f1861e3f12
Finalize caldav types iteration 2024-03-02 16:10:41 +01:00
4d325a2f7b
CalDAV many types 2024-03-02 15:52:26 +01:00
6688dcc383
WIP CalDAV types 2024-03-02 10:08:51 +01:00
8b948916e7
simple lock tests 2024-03-01 18:50:06 +01:00
9200b44941
Fix some logic on locked 2024-03-01 18:33:46 +01:00
4490afb1bf
Implement propertyupdate 2024-03-01 18:20:51 +01:00
dee970afe5
type refactor on <prop> 2024-03-01 17:17:51 +01:00
77e2f8abbb
test include 2024-03-01 16:37:27 +01:00
0cadcbea98
Test Allprop 2024-03-01 16:24:39 +01:00
c15f8856a8
propname tests 2024-03-01 15:32:40 +01:00
2b30c97084
fully serialize webdav core? 2024-03-01 14:28:36 +01:00
8d7c8713b6
Finalized encode ActiveLock 2024-03-01 13:21:19 +01:00
cd48825275
WIP DAV encoder 2024-03-01 10:56:05 +01:00
c52a659151
hook resource type 2024-03-01 10:29:16 +01:00
929a185f37
Add a property hook 2024-03-01 10:12:19 +01:00
8691c98f44
WIP property 2024-03-01 08:43:37 +01:00
e88e448179
Simplify code 2024-03-01 08:32:02 +01:00
33a02ff695
WIP encoder 2024-02-29 23:02:02 +01:00
fadadffc92
Fixed tests 2024-02-29 22:32:07 +01:00
1e3737a590
At least it compiles 2024-02-29 20:40:40 +01:00
9146537aaf
WIP XML encoder 2024-02-29 10:17:46 +01:00
ffe4d071f6
Dav XML types 2024-02-28 22:00:47 +01:00
c10eb33585
WIP DAV types & encoder 2024-02-28 10:20:28 +01:00
239df7bd14
Working on DAV router 2024-02-27 19:30:51 +01:00
7f35e68bfe
Refactor 2024-02-27 18:33:49 +01:00
9a58a4e932
WIP login 2024-02-27 01:05:51 +01:00
ea32a813a7
basic router, define URI pattern 2024-02-27 00:12:01 +01:00
3d3fd80629
Add basic DAV server 2024-02-26 23:59:29 +01:00
0dcf69f180
bump rust toolchain + fix publish script bug 2024-02-24 12:24:51 +01:00
d92ae5220c Merge pull request 'Perf measurement & bottleneck fix' (#102) from perf/cpu-ram-bottleneck into main
Reviewed-on: #102
2024-02-23 17:32:38 +00:00
1ea3de3099
bumping to 0.2.2 2024-02-23 18:32:09 +01:00
0b122582e8
fix code formatting 2024-02-23 18:28:04 +01:00
ab03c7a160
Upgrade Cargo.nix 2024-02-23 18:27:38 +01:00
2a084df300
Also share HTTPClient for K2V 2024-02-23 17:31:29 +01:00
02a8537556
Replace with a single AWS HTTP client 2024-02-23 17:01:51 +01:00
a579382042
update flake dependency 2024-02-23 08:46:05 +01:00
38a8c7de2a
upgrade cargo2nix 2024-02-22 17:32:18 +01:00
9b26e251e3
formatting 2024-02-22 17:31:03 +01:00
2adf73dd8e
Update imap-flow, clean IDLE 2024-02-22 17:30:40 +01:00
3f204b102a
fix test 2024-02-22 11:51:58 +01:00
4d501b6947
Compile streams 2024-02-22 11:35:39 +01:00
de5717a020
Upgrade Cargo.nix 2024-02-20 16:02:56 +01:00
64b474f682
Unsollicited response on APPEND was wrong, upgrade imap-flow to fix LITERAL+ 2024-02-20 13:24:42 +01:00
28b1f4f14d
Unsollicited responses on APPEND 2024-02-20 11:42:51 +01:00
4aa31ba8b5
Add datasets 2024-02-16 18:55:46 +01:00
0b20d726bb
Add a 100 emails dataset on Git LFS 2024-02-15 11:12:20 +01:00
0bb7cdf696
Set pipelinable commands to 64 2024-02-15 11:04:10 +01:00
d50b1dc178 Merge pull request 'Debug the Dovecot Auth Protocol' (#95) from bug/dovecot-auth-resp into main
Reviewed-on: #95
2024-02-13 16:13:53 +00:00
9377ca3ef4
Accept authz id == auth id 2024-02-13 16:57:01 +01:00
25e716a17f
dovecot plain auth inline continuation support 2024-02-13 11:21:11 +01:00
e778bebfd3
Fix nix develop 2024-02-13 10:32:11 +01:00
ede836fc80
automate publishing with nix 2024-02-10 18:04:27 +01:00
3dfe914fda
add building scripts 2024-02-10 17:29:32 +01:00
9954cea30f
fix cargo.nix 2024-02-10 13:44:02 +01:00
3b675ac357 Merge pull request 'WIP 0.2.1' (#93) from bug/deployment into main
Reviewed-on: #93
2024-02-10 11:11:55 +00:00
0e3cfe536f
Escape LMTP data 2024-02-10 12:11:01 +01:00
599480c3d3
Switch version to 0.2.1 2024-02-08 19:41:40 +01:00
59f4bdf9d0
fix idle loop error 2024-02-08 19:40:43 +01:00
678c5bacc6
add way more logging 2024-02-08 15:12:52 +01:00
22f0eb901a
format + fix storage bug 2024-01-31 11:01:18 +01:00
c27919a757
upgrade k2v to 0.9.1 2024-01-30 17:34:16 +01:00
1d6344363a
retrieve missing attributes ldap 2024-01-30 15:45:48 +01:00
93c0aa4b3a
Various post-release fixes 2024-01-25 11:35:33 +01:00
414634f597
Update cargo.nix 2024-01-25 10:03:22 +01:00
1730bd6c10 Merge pull request 'feat/finalize-v0.2' (#82) from feat/finalize-v0.2 into main
Reviewed-on: #82
2024-01-25 08:13:03 +00:00
efd9ae5def
Fix postfix bug 2024-01-24 23:09:29 +01:00
06d37d3399
correctly parse sasl 2024-01-24 22:15:33 +01:00
337b7bce6d
Encoding of server commmands 2024-01-24 22:06:22 +01:00
b86acd5ed0
implemented business logic 2024-01-24 21:36:46 +01:00
bbb050e399
Basic response encoding 2024-01-24 18:57:50 +01:00
0adb92e8ff
AuthOptions parsing 2024-01-24 18:30:28 +01:00
c1bab5808b
QoL connection management 2024-01-24 17:50:03 +01:00
f9d6c1c927
Basic parsing of Dovecot Client Commands 2024-01-24 17:32:47 +01:00
9afd2ea337
Dovecot auth types 2024-01-24 15:21:55 +01:00
9a265a09e2
WIP Dovecot Authentication Protocol Server 2024-01-23 21:09:57 +01:00
f67f04129a
Add TLS support 2024-01-23 16:14:58 +01:00
1f449dc7e9
Rework some details (env var, cargo desc) 2024-01-22 13:59:58 +01:00
4eebc2cb7d
Sync Cargo.nix with Cargo.lock 2024-01-20 19:31:21 +01:00
5711787e53
Fix Cargo.toml advertised licence, Aerogramme is EUPL 2024-01-20 19:27:56 +01:00
3fd22c6fa3
switch to version 0.2.0 2024-01-20 19:24:49 +01:00
49ff733a30 Merge pull request 'Implement LIST X Y RETURN (STATUS (UIDNEXT ...))' (#75) from feat/list-status into main
Reviewed-on: #75
2024-01-20 18:24:05 +00:00
9c3f447480
Test LIST-STATUS 2024-01-20 19:23:44 +01:00
9ae5701c7c
Implement LIST X Y RETURN (STATUS (UIDNEXT ...)) 2024-01-20 18:34:37 +01:00
4849d776b4 Merge pull request 'UIDPLUS' (#73) from uidplus into main
Reviewed-on: #73
2024-01-20 10:45:56 +00:00
369c68231f
test UIDPLUS 2024-01-20 11:45:32 +01:00
a042d9d29e
fix warnings 2024-01-19 17:42:57 +01:00
f5f3aba8d1
format code 2024-01-19 17:40:08 +01:00
c2a518a997
filter expunge 2024-01-19 17:39:55 +01:00
0cc38571f4
Implement some part of SPECIAL-USE 2024-01-19 16:47:20 +01:00
0f227e44e4 Merge pull request 'Implement IDLE' (#72) from feat/idle into main
Reviewed-on: #72
2024-01-19 14:04:03 +00:00
23aa313e11
Testing idle 2024-01-19 14:13:43 +01:00
2c5adc8f16
reformat code 2024-01-18 18:03:21 +01:00
43b668531f
fix a transition bug 2024-01-18 18:02:24 +01:00
185033c462
idling works!!! 2024-01-18 17:33:57 +01:00
e1161cab0e
idle sync 2024-01-17 16:56:05 +01:00
4a15ceacf1
Update dependency 2024-01-17 10:28:04 +01:00
1a0247e935
WIP idle 2024-01-17 10:14:48 +01:00
0eb8156cde
Delete EXAMINE that has been merged in SELECTED 2024-01-17 08:33:08 +01:00
3d23f0c936
WIP refactor idle 2024-01-17 08:22:15 +01:00
55e26d24a0 Merge pull request 'CONDSTORE' (#71) from feat/condstore-try-2 into main
Reviewed-on: #71
2024-01-15 07:07:06 +00:00
81bfed3b7d
testing condstore 2024-01-15 08:06:04 +01:00
22cd0764d8
rewrite store testing logic 2024-01-12 15:02:02 +01:00
c1e7f7264a
fix a condstore bug 2024-01-12 13:01:22 +01:00
6963287986
Fix unit tests 2024-01-12 09:54:58 +01:00
3c7186ab5a
Finalize implementation of CONDSTORE 2024-01-11 23:02:03 +01:00
d24eb9918e
Enable CONDSTORE on STORE/FETCH modifier 2024-01-11 17:13:59 +01:00
60a166185a
Fetch and store modifiers are parsed 2024-01-11 16:55:37 +01:00
a9d33c6708
MODSEQ is now returned on non empty search results 2024-01-11 11:55:40 +01:00
fbf2e9aa96
Enable CONDSTORE if SEARCH MODSEQ is queried 2024-01-11 11:48:02 +01:00
917c32ae0b
MODSEQ search key first implementation 2024-01-11 10:10:00 +01:00
f4cbf66549
Fecth MODSEQ now enables the CONDSTORE capability 2024-01-10 18:38:21 +01:00
f5b73182f2
Fetch now support MODSEQ data item 2024-01-10 18:08:44 +01:00
9cec7803d2
Implement HIGHESTMODSEQ for STATUS 2024-01-10 17:07:07 +01:00
96332c9bfe
upgrading imap-flow,codec,types 2024-01-10 15:15:12 +01:00
0c6e745d11
update imap-codec 2024-01-10 14:45:36 +01:00
20193aa023
Return highestmodseq in select+examine 2024-01-10 13:59:43 +01:00
51510c97f7
fix some logic error in the internals 2024-01-10 12:55:38 +01:00
a2d6efc962
[broken compilation] update mail internal 2024-01-10 11:24:01 +01:00
184328ebcf
Optional Parameters with the SELECT/EXAMINE Commands
See: https://datatracker.ietf.org/doc/html/rfc4466#section-2.4
2024-01-09 19:16:55 +01:00
6e798b90f5
prepare condstore 2024-01-09 17:40:23 +01:00
5dfa02e381
Disable UNSEEN again as it was a volunteer decision to not implement it 2024-01-09 16:53:32 +01:00
d49a2355f7
Reject \n alone, require \r\n 2024-01-08 22:46:39 +01:00
356776cba3 Merge pull request 'bug/thunderbird' (#68) from bug/thunderbird into main
Reviewed-on: #68
2024-01-08 20:34:58 +00:00
149 changed files with 27893 additions and 3925 deletions

8
.gitignore vendored
View file

@ -4,3 +4,11 @@ env.sh
aerogramme.toml aerogramme.toml
*.swo *.swo
*.swp *.swp
aerogramme.pid
cert.pem
ec_key.pem
provider-users.toml
setup.toml
test.eml
test.txt
users.toml

1413
Cargo.lock generated

File diff suppressed because it is too large Load diff

2413
Cargo.nix vendored

File diff suppressed because it is too large Load diff

View file

@ -1,22 +1,42 @@
[package] [workspace]
name = "aerogramme" resolver = "2"
version = "0.1.0" members = [
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"] "aero-user",
edition = "2021" "aero-bayou",
license = "AGPL-3.0" "aero-sasl",
description = "Encrypted mail storage over Garage" "aero-dav",
"aero-dav/fuzz",
"aero-collections",
"aero-proto",
"aerogramme",
]
default-members = ["aerogramme"]
[workspace.dependencies]
# internal crates
aero-user = { version = "0.3.0", path = "aero-user" }
aero-bayou = { version = "0.3.0", path = "aero-bayou" }
aero-sasl = { version = "0.3.0", path = "aero-sasl" }
aero-dav = { version = "0.3.0", path = "aero-dav" }
aero-ical = { version = "0.3.0", path = "aero-ical" }
aero-collections = { version = "0.3.0", path = "aero-collections" }
aero-proto = { version = "0.3.0", path = "aero-proto" }
aerogramme = { version = "0.3.0", path = "aerogramme" }
[dependencies]
# async runtime # async runtime
tokio = { version = "1.18", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] } tokio = { version = "1.36", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
tokio-util = { version = "0.7", features = [ "compat" ] } tokio-util = { version = "0.7", features = [ "compat" ] }
tokio-stream = { version = "0.1" }
futures = "0.3" futures = "0.3"
# debug # debug
log = "0.4" log = "0.4"
backtrace = "0.3" backtrace = "0.3"
console-subscriber = "0.2"
tracing-subscriber = "0.3" tracing-subscriber = "0.3"
tracing = "0.1" tracing = "0.1"
thiserror = "1.0.56"
# language extensions # language extensions
lazy_static = "1.4" lazy_static = "1.4"
@ -31,43 +51,53 @@ chrono = { version = "0.4", default-features = false, features = ["alloc"] }
nix = { version = "0.27", features = ["signal"] } nix = { version = "0.27", features = ["signal"] }
clap = { version = "3.1.18", features = ["derive", "env"] } clap = { version = "3.1.18", features = ["derive", "env"] }
# serialization # email protocols
eml-codec = "0.1.2"
smtp-message = { git = "http://github.com/Alexis211/kannader", branch = "feature/lmtp" }
smtp-server = { git = "http://github.com/Alexis211/kannader", branch = "feature/lmtp" }
imap-codec = { version = "2.0.0", features = ["bounded-static", "ext_condstore_qresync"] }
imap-flow = { git = "https://github.com/duesee/imap-flow.git", branch = "main" }
# dav protocols
icalendar = "0.16"
# http & web
http = "1.1"
http-body-util = "0.1.1"
hyper = "1.2"
hyper-rustls = { version = "0.26", features = ["http2"] }
hyper-util = { version = "0.1", features = ["full"] }
reqwest = { version = "0.12", features = [ "blocking" ]} # for testing purposes only
# serialization, compression & parsing
serde = "1.0.137" serde = "1.0.137"
rmp-serde = "0.15" rmp-serde = "0.15"
toml = "0.5" toml = "0.5"
base64 = "0.21" base64 = "0.21"
hex = "0.4" hex = "0.4"
nom = "7.1"
quick-xml = { version = "0.31", features = ["async-tokio"] }
zstd = { version = "0.9", default-features = false } zstd = { version = "0.9", default-features = false }
# cryptography & security # cryptography & security
sodiumoxide = "0.2" sodiumoxide = "0.2"
argon2 = "0.5" argon2 = "0.5"
rand = "0.8.5" rand = "0.8.5"
hyper-rustls = { version = "0.24", features = ["http2"] } rustls = "0.22"
rustls-pemfile = "2.0"
tokio-rustls = "0.25"
rpassword = "7.0" rpassword = "7.0"
# login # login
ldap3 = { version = "0.10", default-features = false, features = ["tls-rustls"] } ldap3 = { version = "0.10", default-features = false, features = ["tls-rustls"] }
# storage # storage
k2v-client = { git = "https://git.deuxfleurs.fr/Deuxfleurs/garage.git", tag = "v0.9.0" } k2v-client = { git = "https://git.deuxfleurs.fr/Deuxfleurs/garage.git", branch = "k2v/shared_http_client" }
aws-config = { version = "1.1.1", features = ["behavior-version-latest"] } aws-config = { version = "1", features = ["behavior-version-latest"] }
aws-sdk-s3 = "1.9.0" aws-sdk-s3 = "1"
aws-smithy-runtime = "1"
# email protocols aws-smithy-runtime-api = "1"
eml-codec = { git = "https://git.deuxfleurs.fr/Deuxfleurs/eml-codec.git", branch = "main" }
smtp-message = { git = "http://github.com/Alexis211/kannader", branch = "feature/lmtp" }
smtp-server = { git = "http://github.com/Alexis211/kannader", branch = "feature/lmtp" }
imap-codec = { version = "1.0.0", features = ["bounded-static", "ext_condstore_qresync"] }
imap-flow = { git = "https://github.com/duesee/imap-flow.git", rev = "e45ce7bb6ab6bda3c71a0c7b05e9b558a5902e90" }
[dev-dependencies]
[patch.crates-io] [patch.crates-io]
imap-types = { git = "https://github.com/duesee/imap-codec", branch = "v2" } imap-types = { git = "https://github.com/superboum/imap-codec", branch = "custom/aerogramme" }
imap-codec = { git = "https://github.com/duesee/imap-codec", branch = "v2" } imap-codec = { git = "https://github.com/superboum/imap-codec", branch = "custom/aerogramme" }
[[test]]
name = "behavior"
path = "tests/behavior.rs"
harness = false

View file

@ -18,11 +18,12 @@ A resilient & standards-compliant open-source IMAP server with built-in encrypti
## Roadmap ## Roadmap
- ✅ 0.1 Better emails parsing (july '23, see [eml-codec](https://git.deuxfleurs.fr/Deuxfleurs/eml-codec)). - ✅ 0.1 Better emails parsing.
- ⌛0.2 Support of IMAP4rev1. (~september '23). - ✅ 0.2 IMAP4 support.
- ⌛0.3 Subset of IMAP4rev2. (~december '23). - ✅ 0.3 CalDAV support.
- ⌛0.4 CalDAV support. (~february '24). - ⌛0.4 CardDAV support.
- ⌛0.5 CardDAV support. - ⌛0.5 Internals rework.
- ⌛0.6 Public beta.
## Sponsors and funding ## Sponsors and funding

19
aero-bayou/Cargo.toml Normal file
View file

@ -0,0 +1,19 @@
[package]
name = "aero-bayou"
version = "0.3.0"
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
edition = "2021"
license = "EUPL-1.2"
description = "A simplified version of Bayou by Terry et al. (ACM SIGOPS 1995)"
[dependencies]
aero-user.workspace = true
anyhow.workspace = true
hex.workspace = true
tracing.workspace = true
log.workspace = true
rand.workspace = true
serde.workspace = true
tokio.workspace = true

View file

@ -1,15 +1,18 @@
pub mod timestamp;
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, bail, Result};
use log::{debug, error, info}; use log::error;
use rand::prelude::*; use rand::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::sync::{watch, Notify}; use tokio::sync::{watch, Notify};
use crate::cryptoblob::*; use aero_user::cryptoblob::*;
use crate::login::Credentials; use aero_user::login::Credentials;
use crate::storage; use aero_user::storage;
use crate::timestamp::*; use crate::timestamp::*;
const KEEP_STATE_EVERY: usize = 64; const KEEP_STATE_EVERY: usize = 64;
@ -84,21 +87,21 @@ impl<S: BayouState> Bayou<S> {
// 1. List checkpoints // 1. List checkpoints
let checkpoints = self.list_checkpoints().await?; let checkpoints = self.list_checkpoints().await?;
debug!("(sync) listed checkpoints: {:?}", checkpoints); tracing::debug!("(sync) listed checkpoints: {:?}", checkpoints);
// 2. Load last checkpoint if different from currently used one // 2. Load last checkpoint if different from currently used one
let checkpoint = if let Some((ts, key)) = checkpoints.last() { let checkpoint = if let Some((ts, key)) = checkpoints.last() {
if *ts == self.checkpoint.0 { if *ts == self.checkpoint.0 {
(*ts, None) (*ts, None)
} else { } else {
debug!("(sync) loading checkpoint: {}", key); tracing::debug!("(sync) loading checkpoint: {}", key);
let buf = self let buf = self
.storage .storage
.blob_fetch(&storage::BlobRef(key.to_string())) .blob_fetch(&storage::BlobRef(key.to_string()))
.await? .await?
.value; .value;
debug!("(sync) checkpoint body length: {}", buf.len()); tracing::debug!("(sync) checkpoint body length: {}", buf.len());
let ck = open_deserialize::<S>(&buf, &self.key)?; let ck = open_deserialize::<S>(&buf, &self.key)?;
(*ts, Some(ck)) (*ts, Some(ck))
@ -112,7 +115,7 @@ impl<S: BayouState> Bayou<S> {
} }
if let Some(ck) = checkpoint.1 { if let Some(ck) = checkpoint.1 {
debug!( tracing::debug!(
"(sync) updating checkpoint to loaded state at {:?}", "(sync) updating checkpoint to loaded state at {:?}",
checkpoint.0 checkpoint.0
); );
@ -127,7 +130,7 @@ impl<S: BayouState> Bayou<S> {
// 3. List all operations starting from checkpoint // 3. List all operations starting from checkpoint
let ts_ser = self.checkpoint.0.to_string(); let ts_ser = self.checkpoint.0.to_string();
debug!("(sync) looking up operations starting at {}", ts_ser); tracing::debug!("(sync) looking up operations starting at {}", ts_ser);
let ops_map = self let ops_map = self
.storage .storage
.row_fetch(&storage::Selector::Range { .row_fetch(&storage::Selector::Range {
@ -161,7 +164,7 @@ impl<S: BayouState> Bayou<S> {
} }
} }
ops.sort_by_key(|(ts, _)| *ts); ops.sort_by_key(|(ts, _)| *ts);
debug!("(sync) {} operations", ops.len()); tracing::debug!("(sync) {} operations", ops.len());
if ops.len() < self.history.len() { if ops.len() < self.history.len() {
bail!("Some operations have disappeared from storage!"); bail!("Some operations have disappeared from storage!");
@ -238,12 +241,16 @@ impl<S: BayouState> Bayou<S> {
Ok(()) Ok(())
} }
pub fn notifier(&self) -> std::sync::Weak<Notify> {
Arc::downgrade(&self.watch.learnt_remote_update)
}
/// Applies a new operation on the state. Once this function returns, /// Applies a new operation on the state. Once this function returns,
/// the operation has been safely persisted to storage backend. /// the operation has been safely persisted to storage backend.
/// Make sure to call `.opportunistic_sync()` before doing this, /// Make sure to call `.opportunistic_sync()` before doing this,
/// and even before calculating the `op` argument given here. /// and even before calculating the `op` argument given here.
pub async fn push(&mut self, op: S::Op) -> Result<()> { pub async fn push(&mut self, op: S::Op) -> Result<()> {
debug!("(push) add operation: {:?}", op); tracing::debug!("(push) add operation: {:?}", op);
let ts = Timestamp::after( let ts = Timestamp::after(
self.history self.history
@ -257,7 +264,7 @@ impl<S: BayouState> Bayou<S> {
seal_serialize(&op, &self.key)?, seal_serialize(&op, &self.key)?,
); );
self.storage.row_insert(vec![row_val]).await?; self.storage.row_insert(vec![row_val]).await?;
self.watch.notify.notify_one(); self.watch.propagate_local_update.notify_one();
let new_state = self.state().apply(&op); let new_state = self.state().apply(&op);
self.history.push((ts, op, Some(new_state))); self.history.push((ts, op, Some(new_state)));
@ -305,18 +312,18 @@ impl<S: BayouState> Bayou<S> {
{ {
Some(i) => i, Some(i) => i,
None => { None => {
debug!("(cp) Oldest operation is too recent to trigger checkpoint"); tracing::debug!("(cp) Oldest operation is too recent to trigger checkpoint");
return Ok(()); return Ok(());
} }
}; };
if i_cp < CHECKPOINT_MIN_OPS { if i_cp < CHECKPOINT_MIN_OPS {
debug!("(cp) Not enough old operations to trigger checkpoint"); tracing::debug!("(cp) Not enough old operations to trigger checkpoint");
return Ok(()); return Ok(());
} }
let ts_cp = self.history[i_cp].0; let ts_cp = self.history[i_cp].0;
debug!( tracing::debug!(
"(cp) we could checkpoint at time {} (index {} in history)", "(cp) we could checkpoint at time {} (index {} in history)",
ts_cp.to_string(), ts_cp.to_string(),
i_cp i_cp
@ -324,13 +331,13 @@ impl<S: BayouState> Bayou<S> {
// Check existing checkpoints: if last one is too recent, don't checkpoint again. // Check existing checkpoints: if last one is too recent, don't checkpoint again.
let existing_checkpoints = self.list_checkpoints().await?; let existing_checkpoints = self.list_checkpoints().await?;
debug!("(cp) listed checkpoints: {:?}", existing_checkpoints); tracing::debug!("(cp) listed checkpoints: {:?}", existing_checkpoints);
if let Some(last_cp) = existing_checkpoints.last() { if let Some(last_cp) = existing_checkpoints.last() {
if (ts_cp.msec as i128 - last_cp.0.msec as i128) if (ts_cp.msec as i128 - last_cp.0.msec as i128)
< CHECKPOINT_INTERVAL.as_millis() as i128 < CHECKPOINT_INTERVAL.as_millis() as i128
{ {
debug!( tracing::debug!(
"(cp) last checkpoint is too recent: {}, not checkpointing", "(cp) last checkpoint is too recent: {}, not checkpointing",
last_cp.0.to_string() last_cp.0.to_string()
); );
@ -338,7 +345,7 @@ impl<S: BayouState> Bayou<S> {
} }
} }
debug!("(cp) saving checkpoint at {}", ts_cp.to_string()); tracing::debug!("(cp) saving checkpoint at {}", ts_cp.to_string());
// Calculate state at time of checkpoint // Calculate state at time of checkpoint
let mut last_known_state = (0, &self.checkpoint.1); let mut last_known_state = (0, &self.checkpoint.1);
@ -354,7 +361,7 @@ impl<S: BayouState> Bayou<S> {
// Serialize and save checkpoint // Serialize and save checkpoint
let cryptoblob = seal_serialize(&state_cp, &self.key)?; let cryptoblob = seal_serialize(&state_cp, &self.key)?;
debug!("(cp) checkpoint body length: {}", cryptoblob.len()); tracing::debug!("(cp) checkpoint body length: {}", cryptoblob.len());
let blob_val = storage::BlobVal::new( let blob_val = storage::BlobVal::new(
storage::BlobRef(format!("{}/checkpoint/{}", self.path, ts_cp.to_string())), storage::BlobRef(format!("{}/checkpoint/{}", self.path, ts_cp.to_string())),
@ -369,7 +376,7 @@ impl<S: BayouState> Bayou<S> {
// Delete blobs // Delete blobs
for (_ts, key) in existing_checkpoints[..last_to_keep].iter() { for (_ts, key) in existing_checkpoints[..last_to_keep].iter() {
debug!("(cp) drop old checkpoint {}", key); tracing::debug!("(cp) drop old checkpoint {}", key);
self.storage self.storage
.blob_rm(&storage::BlobRef(key.to_string())) .blob_rm(&storage::BlobRef(key.to_string()))
.await?; .await?;
@ -423,7 +430,8 @@ impl<S: BayouState> Bayou<S> {
struct K2vWatch { struct K2vWatch {
target: storage::RowRef, target: storage::RowRef,
rx: watch::Receiver<storage::RowRef>, rx: watch::Receiver<storage::RowRef>,
notify: Notify, propagate_local_update: Notify,
learnt_remote_update: Arc<Notify>,
} }
impl K2vWatch { impl K2vWatch {
@ -434,9 +442,15 @@ impl K2vWatch {
let storage = creds.storage.build().await?; let storage = creds.storage.build().await?;
let (tx, rx) = watch::channel::<storage::RowRef>(target.clone()); let (tx, rx) = watch::channel::<storage::RowRef>(target.clone());
let notify = Notify::new(); let propagate_local_update = Notify::new();
let learnt_remote_update = Arc::new(Notify::new());
let watch = Arc::new(K2vWatch { target, rx, notify }); let watch = Arc::new(K2vWatch {
target,
rx,
propagate_local_update,
learnt_remote_update,
});
tokio::spawn(Self::background_task(Arc::downgrade(&watch), storage, tx)); tokio::spawn(Self::background_task(Arc::downgrade(&watch), storage, tx));
@ -448,18 +462,24 @@ impl K2vWatch {
storage: storage::Store, storage: storage::Store,
tx: watch::Sender<storage::RowRef>, tx: watch::Sender<storage::RowRef>,
) { ) {
let mut row = match Weak::upgrade(&self_weak) { let (mut row, remote_update) = match Weak::upgrade(&self_weak) {
Some(this) => this.target.clone(), Some(this) => (this.target.clone(), this.learnt_remote_update.clone()),
None => return, None => return,
}; };
while let Some(this) = Weak::upgrade(&self_weak) { while let Some(this) = Weak::upgrade(&self_weak) {
debug!( tracing::debug!(
"bayou k2v watch bg loop iter ({}, {})", "bayou k2v watch bg loop iter ({}, {})",
this.target.uid.shard, this.target.uid.sort this.target.uid.shard,
this.target.uid.sort
); );
tokio::select!( tokio::select!(
// Needed to exit: will force a loop iteration every minutes,
// that will stop the loop if other Arc references have been dropped
// and free resources. Otherwise we would be blocked waiting forever...
_ = tokio::time::sleep(Duration::from_secs(60)) => continue, _ = tokio::time::sleep(Duration::from_secs(60)) => continue,
// Watch if another instance has modified the log
update = storage.row_poll(&row) => { update = storage.row_poll(&row) => {
match update { match update {
Err(e) => { Err(e) => {
@ -468,23 +488,30 @@ impl K2vWatch {
} }
Ok(new_value) => { Ok(new_value) => {
row = new_value.row_ref; row = new_value.row_ref;
if tx.send(row.clone()).is_err() { if let Err(e) = tx.send(row.clone()) {
tracing::warn!(err=?e, "(watch) can't record the new log ref");
break; break;
} }
tracing::debug!(row=?row, "(watch) learnt remote update");
this.learnt_remote_update.notify_waiters();
} }
} }
} }
_ = this.notify.notified() => {
// It appears we have modified the log, informing other people
_ = this.propagate_local_update.notified() => {
let rand = u128::to_be_bytes(thread_rng().gen()).to_vec(); let rand = u128::to_be_bytes(thread_rng().gen()).to_vec();
let row_val = storage::RowVal::new(row.clone(), rand); let row_val = storage::RowVal::new(row.clone(), rand);
if let Err(e) = storage.row_insert(vec![row_val]).await if let Err(e) = storage.row_insert(vec![row_val]).await
{ {
error!("Error in bayou k2v watch updater loop: {}", e); tracing::error!("Error in bayou k2v watch updater loop: {}", e);
tokio::time::sleep(Duration::from_secs(30)).await; tokio::time::sleep(Duration::from_secs(30)).await;
} }
} }
); );
} }
info!("bayou k2v watch bg loop exiting"); // unblock listeners
remote_update.notify_waiters();
tracing::info!("bayou k2v watch bg loop exiting");
} }
} }

View file

@ -1,7 +1,8 @@
use rand::prelude::*;
use std::str::FromStr; use std::str::FromStr;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use rand::prelude::*;
/// Returns milliseconds since UNIX Epoch /// Returns milliseconds since UNIX Epoch
pub fn now_msec() -> u64 { pub fn now_msec() -> u64 {
SystemTime::now() SystemTime::now()

View file

@ -0,0 +1,25 @@
[package]
name = "aero-collections"
version = "0.3.0"
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
edition = "2021"
license = "EUPL-1.2"
description = "Aerogramme own representation of the different objects it manipulates"
[dependencies]
aero-user.workspace = true
aero-bayou.workspace = true
anyhow.workspace = true
base64.workspace = true
futures.workspace = true
lazy_static.workspace = true
serde.workspace = true
hex.workspace = true
tokio.workspace = true
tracing.workspace = true
rand.workspace = true
im.workspace = true
sodiumoxide.workspace = true
eml-codec.workspace = true
icalendar.workspace = true

View file

@ -0,0 +1,204 @@
pub mod namespace;
use anyhow::{anyhow, bail, Result};
use tokio::sync::RwLock;
use aero_bayou::Bayou;
use aero_user::cryptoblob::{self, gen_key, Key};
use aero_user::login::Credentials;
use aero_user::storage::{self, BlobRef, BlobVal, Store};
use crate::davdag::{BlobId, DavDag, IndexEntry, SyncChange, Token};
use crate::unique_ident::*;
pub struct Calendar {
pub(super) id: UniqueIdent,
internal: RwLock<CalendarInternal>,
}
impl Calendar {
pub(crate) async fn open(creds: &Credentials, id: UniqueIdent) -> Result<Self> {
let bayou_path = format!("calendar/dag/{}", id);
let cal_path = format!("calendar/events/{}", id);
let mut davdag = Bayou::<DavDag>::new(creds, bayou_path).await?;
davdag.sync().await?;
let internal = RwLock::new(CalendarInternal {
id,
encryption_key: creds.keys.master.clone(),
storage: creds.storage.build().await?,
davdag,
cal_path,
});
Ok(Self { id, internal })
}
// ---- DAG sync utilities
/// Sync data with backing store
pub async fn force_sync(&self) -> Result<()> {
self.internal.write().await.force_sync().await
}
/// Sync data with backing store only if changes are detected
/// or last sync is too old
pub async fn opportunistic_sync(&self) -> Result<()> {
self.internal.write().await.opportunistic_sync().await
}
// ---- Data API
/// Access the DAG internal data (you can get the list of files for example)
pub async fn dag(&self) -> DavDag {
// Cloning is cheap
self.internal.read().await.davdag.state().clone()
}
/// Access the current token
pub async fn token(&self) -> Result<Token> {
self.internal.write().await.current_token().await
}
/// The diff API is a write API as we might need to push a merge node
/// to get a new sync token
pub async fn diff(&self, sync_token: Token) -> Result<(Token, Vec<SyncChange>)> {
self.internal.write().await.diff(sync_token).await
}
/// Get a specific event
pub async fn get(&self, evt_id: UniqueIdent) -> Result<Vec<u8>> {
self.internal.read().await.get(evt_id).await
}
/// Put a specific event
pub async fn put<'a>(&self, name: &str, evt: &'a [u8]) -> Result<(Token, IndexEntry)> {
self.internal.write().await.put(name, evt).await
}
/// Delete a specific event
pub async fn delete(&self, blob_id: UniqueIdent) -> Result<Token> {
self.internal.write().await.delete(blob_id).await
}
}
use base64::Engine;
const MESSAGE_KEY: &str = "message-key";
struct CalendarInternal {
#[allow(dead_code)]
id: UniqueIdent,
cal_path: String,
encryption_key: Key,
storage: Store,
davdag: Bayou<DavDag>,
}
impl CalendarInternal {
async fn force_sync(&mut self) -> Result<()> {
self.davdag.sync().await?;
Ok(())
}
async fn opportunistic_sync(&mut self) -> Result<()> {
self.davdag.opportunistic_sync().await?;
Ok(())
}
async fn get(&self, blob_id: BlobId) -> Result<Vec<u8>> {
// Fetch message from S3
let blob_ref = storage::BlobRef(format!("{}/{}", self.cal_path, blob_id));
let object = self.storage.blob_fetch(&blob_ref).await?;
// Decrypt message key from headers
let key_encrypted_b64 = object
.meta
.get(MESSAGE_KEY)
.ok_or(anyhow!("Missing key in metadata"))?;
let key_encrypted = base64::engine::general_purpose::STANDARD.decode(key_encrypted_b64)?;
let message_key_raw = cryptoblob::open(&key_encrypted, &self.encryption_key)?;
let message_key =
cryptoblob::Key::from_slice(&message_key_raw).ok_or(anyhow!("Invalid message key"))?;
// Decrypt body
let body = object.value;
cryptoblob::open(&body, &message_key)
}
async fn put<'a>(&mut self, name: &str, evt: &'a [u8]) -> Result<(Token, IndexEntry)> {
let message_key = gen_key();
let blob_id = gen_ident();
let encrypted_msg_key = cryptoblob::seal(&message_key.as_ref(), &self.encryption_key)?;
let key_header = base64::engine::general_purpose::STANDARD.encode(&encrypted_msg_key);
// Write event to S3
let message_blob = cryptoblob::seal(evt, &message_key)?;
let blob_val = BlobVal::new(
BlobRef(format!("{}/{}", self.cal_path, blob_id)),
message_blob,
)
.with_meta(MESSAGE_KEY.to_string(), key_header);
let etag = self.storage.blob_insert(blob_val).await?;
// Add entry to Bayou
let entry: IndexEntry = (blob_id, name.to_string(), etag);
let davstate = self.davdag.state();
let put_op = davstate.op_put(entry.clone());
let token = put_op.token();
self.davdag.push(put_op).await?;
Ok((token, entry))
}
async fn delete(&mut self, blob_id: BlobId) -> Result<Token> {
let davstate = self.davdag.state();
if !davstate.table.contains_key(&blob_id) {
bail!("Cannot delete event that doesn't exist");
}
let del_op = davstate.op_delete(blob_id);
let token = del_op.token();
self.davdag.push(del_op).await?;
let blob_ref = BlobRef(format!("{}/{}", self.cal_path, blob_id));
self.storage.blob_rm(&blob_ref).await?;
Ok(token)
}
async fn diff(&mut self, sync_token: Token) -> Result<(Token, Vec<SyncChange>)> {
let davstate = self.davdag.state();
let token_changed = davstate.resolve(sync_token)?;
let changes = token_changed
.iter()
.filter_map(|t: &Token| davstate.change.get(t))
.map(|s| s.clone())
.filter(|s| match s {
SyncChange::Ok((filename, _)) => davstate.idx_by_filename.get(filename).is_some(),
SyncChange::NotFound(filename) => davstate.idx_by_filename.get(filename).is_none(),
})
.collect();
let token = self.current_token().await?;
Ok((token, changes))
}
async fn current_token(&mut self) -> Result<Token> {
let davstate = self.davdag.state();
let heads = davstate.heads_vec();
let token = match heads.as_slice() {
[token] => *token,
_ => {
let op_mg = davstate.op_merge();
let token = op_mg.token();
self.davdag.push(op_mg).await?;
token
}
};
Ok(token)
}
}

View file

@ -0,0 +1,324 @@
use anyhow::{bail, Result};
use std::collections::{BTreeMap, HashMap};
use std::sync::{Arc, Weak};
use serde::{Deserialize, Serialize};
use aero_bayou::timestamp::now_msec;
use aero_user::cryptoblob::{open_deserialize, seal_serialize};
use aero_user::storage;
use super::Calendar;
use crate::unique_ident::{gen_ident, UniqueIdent};
use crate::user::User;
pub(crate) const CAL_LIST_PK: &str = "calendars";
pub(crate) const CAL_LIST_SK: &str = "list";
pub(crate) const MAIN_CAL: &str = "Personal";
pub(crate) const MAX_CALNAME_CHARS: usize = 32;
pub struct CalendarNs(std::sync::Mutex<HashMap<UniqueIdent, Weak<Calendar>>>);
impl CalendarNs {
/// Create a new calendar namespace
pub fn new() -> Self {
Self(std::sync::Mutex::new(HashMap::new()))
}
/// Open a calendar by name
pub async fn open(&self, user: &Arc<User>, name: &str) -> Result<Option<Arc<Calendar>>> {
let (list, _ct) = CalendarList::load(user).await?;
match list.get(name) {
None => Ok(None),
Some(ident) => Ok(Some(self.open_by_id(user, ident).await?)),
}
}
/// Open a calendar by unique id
/// Check user.rs::open_mailbox_by_id to understand this function
pub async fn open_by_id(&self, user: &Arc<User>, id: UniqueIdent) -> Result<Arc<Calendar>> {
{
let cache = self.0.lock().unwrap();
if let Some(cal) = cache.get(&id).and_then(Weak::upgrade) {
return Ok(cal);
}
}
let cal = Arc::new(Calendar::open(&user.creds, id).await?);
let mut cache = self.0.lock().unwrap();
if let Some(concurrent_cal) = cache.get(&id).and_then(Weak::upgrade) {
drop(cal); // we worked for nothing but at least we didn't starve someone else
Ok(concurrent_cal)
} else {
cache.insert(id, Arc::downgrade(&cal));
Ok(cal)
}
}
/// List calendars
pub async fn list(&self, user: &Arc<User>) -> Result<Vec<String>> {
CalendarList::load(user).await.map(|(list, _)| list.names())
}
/// Delete a calendar from the index
pub async fn delete(&self, user: &Arc<User>, name: &str) -> Result<()> {
// We currently assume that main cal is a bit specific
if name == MAIN_CAL {
bail!("Cannot delete main calendar");
}
let (mut list, ct) = CalendarList::load(user).await?;
if list.has(name) {
//@TODO: actually delete calendar content
list.bind(name, None);
list.save(user, ct).await?;
Ok(())
} else {
bail!("Calendar {} does not exist", name);
}
}
/// Rename a calendar in the index
pub async fn rename(&self, user: &Arc<User>, old: &str, new: &str) -> Result<()> {
if old == MAIN_CAL {
bail!("Renaming main calendar is not supported currently");
}
if !new.chars().all(char::is_alphanumeric) {
bail!("Unsupported characters in new calendar name, only alphanumeric characters are allowed currently");
}
if new.len() > MAX_CALNAME_CHARS {
bail!("Calendar name can't contain more than 32 characters");
}
let (mut list, ct) = CalendarList::load(user).await?;
list.rename(old, new)?;
list.save(user, ct).await?;
Ok(())
}
/// Create calendar
pub async fn create(&self, user: &Arc<User>, name: &str) -> Result<()> {
if name == MAIN_CAL {
bail!("Main calendar is automatically created, can't create it manually");
}
if !name.chars().all(char::is_alphanumeric) {
bail!("Unsupported characters in new calendar name, only alphanumeric characters are allowed");
}
if name.len() > MAX_CALNAME_CHARS {
bail!("Calendar name can't contain more than 32 characters");
}
let (mut list, ct) = CalendarList::load(user).await?;
match list.create(name) {
CalendarExists::Existed(_) => bail!("Calendar {} already exists", name),
CalendarExists::Created(_) => (),
}
list.save(user, ct).await?;
Ok(())
}
/// Has calendar
pub async fn has(&self, user: &Arc<User>, name: &str) -> Result<bool> {
CalendarList::load(user)
.await
.map(|(list, _)| list.has(name))
}
}
// ------
// ------ From this point, implementation is hidden from the rest of the crate
// ------
#[derive(Serialize, Deserialize)]
struct CalendarList(BTreeMap<String, CalendarListEntry>);
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
struct CalendarListEntry {
id_lww: (u64, Option<UniqueIdent>),
}
impl CalendarList {
// ---- Index persistence related functions
/// Load from storage
async fn load(user: &Arc<User>) -> Result<(Self, Option<storage::RowRef>)> {
let row_ref = storage::RowRef::new(CAL_LIST_PK, CAL_LIST_SK);
let (mut list, row) = match user
.storage
.row_fetch(&storage::Selector::Single(&row_ref))
.await
{
Err(storage::StorageError::NotFound) => (Self::new(), None),
Err(e) => return Err(e.into()),
Ok(rv) => {
let mut list = Self::new();
let (row_ref, row_vals) = match rv.into_iter().next() {
Some(row_val) => (row_val.row_ref, row_val.value),
None => (row_ref, vec![]),
};
for v in row_vals {
if let storage::Alternative::Value(vbytes) = v {
let list2 =
open_deserialize::<CalendarList>(&vbytes, &user.creds.keys.master)?;
list.merge(list2);
}
}
(list, Some(row_ref))
}
};
// Create default calendars (currently only one calendar is created)
let is_default_cal_missing = [MAIN_CAL]
.iter()
.map(|calname| list.create(calname))
.fold(false, |acc, r| {
acc || matches!(r, CalendarExists::Created(..))
});
// Save the index if we created a new calendar
if is_default_cal_missing {
list.save(user, row.clone()).await?;
}
Ok((list, row))
}
/// Save an updated index
async fn save(&self, user: &Arc<User>, ct: Option<storage::RowRef>) -> Result<()> {
let list_blob = seal_serialize(self, &user.creds.keys.master)?;
let rref = ct.unwrap_or(storage::RowRef::new(CAL_LIST_PK, CAL_LIST_SK));
let row_val = storage::RowVal::new(rref, list_blob);
user.storage.row_insert(vec![row_val]).await?;
Ok(())
}
// ----- Index manipulation functions
/// Ensure that a given calendar exists
/// (Don't forget to save if it returns CalendarExists::Created)
fn create(&mut self, name: &str) -> CalendarExists {
if let Some(CalendarListEntry {
id_lww: (_, Some(id)),
}) = self.0.get(name)
{
return CalendarExists::Existed(*id);
}
let id = gen_ident();
self.bind(name, Some(id)).unwrap();
CalendarExists::Created(id)
}
/// Get a list of all calendar names
fn names(&self) -> Vec<String> {
self.0
.iter()
.filter(|(_, v)| v.id_lww.1.is_some())
.map(|(k, _)| k.to_string())
.collect()
}
/// For a given calendar name, get its Unique Identifier
fn get(&self, name: &str) -> Option<UniqueIdent> {
self.0
.get(name)
.map(|CalendarListEntry { id_lww: (_, ident) }| *ident)
.flatten()
}
/// Check if a given calendar name exists
fn has(&self, name: &str) -> bool {
self.get(name).is_some()
}
/// Rename a calendar
fn rename(&mut self, old: &str, new: &str) -> Result<()> {
if self.has(new) {
bail!("Calendar {} already exists", new);
}
let ident = match self.get(old) {
None => bail!("Calendar {} does not exist", old),
Some(ident) => ident,
};
self.bind(old, None);
self.bind(new, Some(ident));
Ok(())
}
// ----- Internal logic
/// New is not publicly exposed, use `load` instead
fn new() -> Self {
Self(BTreeMap::new())
}
/// Low level index updating logic (used to add/rename/delete) an entry
fn bind(&mut self, name: &str, id: Option<UniqueIdent>) -> Option<()> {
let (ts, id) = match self.0.get_mut(name) {
None => {
if id.is_none() {
// User wants to delete entry with given name (passed id is None)
// Entry does not exist (get_mut is None)
// Nothing to do
return None;
} else {
// User wants entry with given name to be present (id is Some)
// Entry does not exist
// Initialize entry
(now_msec(), id)
}
}
Some(CalendarListEntry { id_lww }) => {
if id_lww.1 == id {
// Entry is already equals to the requested id (Option<UniqueIdent)
// Nothing to do
return None;
} else {
// Entry does not equal to what we know internally
// We update the Last Write Win CRDT here with the new id value
(std::cmp::max(id_lww.0 + 1, now_msec()), id)
}
}
};
// If we did not return here, that's because we have to update
// something in our internal index.
self.0
.insert(name.into(), CalendarListEntry { id_lww: (ts, id) });
Some(())
}
// Merge 2 calendar lists by applying a LWW logic on each element
fn merge(&mut self, list2: Self) {
for (k, v) in list2.0.into_iter() {
if let Some(e) = self.0.get_mut(&k) {
e.merge(&v);
} else {
self.0.insert(k, v);
}
}
}
}
impl CalendarListEntry {
fn merge(&mut self, other: &Self) {
// Simple CRDT merge rule
if other.id_lww.0 > self.id_lww.0
|| (other.id_lww.0 == self.id_lww.0 && other.id_lww.1 > self.id_lww.1)
{
self.id_lww = other.id_lww;
}
}
}
pub(crate) enum CalendarExists {
Created(UniqueIdent),
Existed(UniqueIdent),
}

View file

@ -0,0 +1,342 @@
use anyhow::{bail, Result};
use im::{ordset, OrdMap, OrdSet};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use aero_bayou::*;
use crate::unique_ident::{gen_ident, UniqueIdent};
/// Parents are only persisted in the event log,
/// not in the checkpoints.
pub type Token = UniqueIdent;
pub type Parents = Vec<Token>;
pub type SyncDesc = (Parents, Token);
pub type BlobId = UniqueIdent;
pub type Etag = String;
pub type FileName = String;
pub type IndexEntry = (BlobId, FileName, Etag);
#[derive(Clone, Default)]
pub struct DavDag {
/// Source of trust
pub table: OrdMap<BlobId, IndexEntry>,
/// Indexes optimized for queries
pub idx_by_filename: OrdMap<FileName, BlobId>,
// ------------ Below this line, data is ephemeral, ie. not checkpointed
/// Partial synchronization graph
pub ancestors: OrdMap<Token, OrdSet<Token>>,
/// All nodes
pub all_nodes: OrdSet<Token>,
/// Head nodes
pub heads: OrdSet<Token>,
/// Origin nodes
pub origins: OrdSet<Token>,
/// File change token by token
pub change: OrdMap<Token, SyncChange>,
}
#[derive(Clone, Debug)]
pub enum SyncChange {
Ok((FileName, BlobId)),
NotFound(FileName),
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub enum DavDagOp {
/// Merge is a virtual operation run when multiple heads are discovered
Merge(SyncDesc),
/// Add an item to the collection
Put(SyncDesc, IndexEntry),
/// Delete an item from the collection
Delete(SyncDesc, BlobId),
}
impl DavDagOp {
pub fn token(&self) -> Token {
match self {
Self::Merge((_, t)) => *t,
Self::Put((_, t), _) => *t,
Self::Delete((_, t), _) => *t,
}
}
}
impl DavDag {
pub fn op_merge(&self) -> DavDagOp {
DavDagOp::Merge(self.sync_desc())
}
pub fn op_put(&self, entry: IndexEntry) -> DavDagOp {
DavDagOp::Put(self.sync_desc(), entry)
}
pub fn op_delete(&self, blob_id: BlobId) -> DavDagOp {
DavDagOp::Delete(self.sync_desc(), blob_id)
}
// HELPER functions
pub fn heads_vec(&self) -> Vec<Token> {
self.heads.clone().into_iter().collect()
}
/// A sync descriptor
pub fn sync_desc(&self) -> SyncDesc {
(self.heads_vec(), gen_ident())
}
/// Resolve a sync token
pub fn resolve(&self, known: Token) -> Result<OrdSet<Token>> {
let already_known = self.all_ancestors(known);
// We can't capture all missing events if we are not connected
// to all sinks of the graph,
// ie. if we don't already know all the sinks,
// ie. if we are missing so much history that
// the event log has been transformed into a checkpoint
if !self.origins.is_subset(already_known.clone()) {
bail!("Not enough history to produce a correct diff, a full resync is needed");
}
// Missing items are *all existing graph items* from which
// we removed *all items known by the given node*.
// In other words, all values in `all_nodes` that are not in `already_known`.
Ok(self.all_nodes.clone().relative_complement(already_known))
}
/// Find all ancestors of a given node
fn all_ancestors(&self, known: Token) -> OrdSet<Token> {
let mut all_known: OrdSet<UniqueIdent> = OrdSet::new();
let mut to_collect = vec![known];
loop {
let cursor = match to_collect.pop() {
// Loop stops here
None => break,
Some(v) => v,
};
if all_known.insert(cursor).is_some() {
// Item already processed
continue;
}
// Collect parents
let parents = match self.ancestors.get(&cursor) {
None => continue,
Some(c) => c,
};
to_collect.extend(parents.iter());
}
all_known
}
// INTERNAL functions
/// Register a WebDAV item (put, copy, move)
fn register(&mut self, sync_token: Option<Token>, entry: IndexEntry) {
let (blob_id, filename, _etag) = entry.clone();
// Insert item in the source of trust
self.table.insert(blob_id, entry);
// Update the cache
self.idx_by_filename.insert(filename.to_string(), blob_id);
// Record the change in the ephemeral synchronization map
if let Some(sync_token) = sync_token {
self.change
.insert(sync_token, SyncChange::Ok((filename, blob_id)));
}
}
/// Unregister a WebDAV item (delete, move)
fn unregister(&mut self, sync_token: Token, blob_id: &BlobId) {
// Query the source of truth to get the information we
// need to clean the indexes
let (_blob_id, filename, _etag) = match self.table.get(blob_id) {
Some(v) => v,
// Element does not exist, return early
None => return,
};
self.idx_by_filename.remove(filename);
// Record the change in the ephemeral synchronization map
self.change
.insert(sync_token, SyncChange::NotFound(filename.to_string()));
// Finally clear item from the source of trust
self.table.remove(blob_id);
}
/// When an event is processed, update the synchronization DAG
fn sync_dag(&mut self, sync_desc: &SyncDesc) {
let (parents, child) = sync_desc;
// --- Update ANCESTORS
// We register ancestors as it is required for the sync algorithm
self.ancestors.insert(
*child,
parents.iter().fold(ordset![], |mut acc, p| {
acc.insert(*p);
acc
}),
);
// --- Update ORIGINS
// If this event has no parents, it's an origin
if parents.is_empty() {
self.origins.insert(*child);
}
// --- Update HEADS
// Remove from HEADS this event's parents
parents.iter().for_each(|par| {
self.heads.remove(par);
});
// This event becomes a new HEAD in turn
self.heads.insert(*child);
// --- Update ALL NODES
self.all_nodes.insert(*child);
}
}
impl std::fmt::Debug for DavDag {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("DavDag\n")?;
for elem in self.table.iter() {
f.write_fmt(format_args!("\t{:?} => {:?}", elem.0, elem.1))?;
}
Ok(())
}
}
impl BayouState for DavDag {
type Op = DavDagOp;
fn apply(&self, op: &Self::Op) -> Self {
let mut new = self.clone();
match op {
DavDagOp::Put(sync_desc, entry) => {
new.sync_dag(sync_desc);
new.register(Some(sync_desc.1), entry.clone());
}
DavDagOp::Delete(sync_desc, blob_id) => {
new.sync_dag(sync_desc);
new.unregister(sync_desc.1, blob_id);
}
DavDagOp::Merge(sync_desc) => {
new.sync_dag(sync_desc);
}
}
new
}
}
// CUSTOM SERIALIZATION & DESERIALIZATION
#[derive(Serialize, Deserialize)]
struct DavDagSerializedRepr {
items: Vec<IndexEntry>,
heads: Vec<UniqueIdent>,
}
impl<'de> Deserialize<'de> for DavDag {
fn deserialize<D>(d: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let val: DavDagSerializedRepr = DavDagSerializedRepr::deserialize(d)?;
let mut davdag = DavDag::default();
// Build the table + index
val.items
.into_iter()
.for_each(|entry| davdag.register(None, entry));
// Initialize the synchronization DAG with its roots
val.heads.into_iter().for_each(|ident| {
davdag.heads.insert(ident);
davdag.origins.insert(ident);
davdag.all_nodes.insert(ident);
});
Ok(davdag)
}
}
impl Serialize for DavDag {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// Indexes are rebuilt on the fly, we serialize only the core database
let items = self.table.iter().map(|(_, entry)| entry.clone()).collect();
// We keep only the head entries from the sync graph,
// these entries will be used to initialize it back when deserializing
let heads = self.heads_vec();
// Finale serialization object
let val = DavDagSerializedRepr { items, heads };
val.serialize(serializer)
}
}
// ---- TESTS ----
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn base() {
let mut state = DavDag::default();
// Add item 1
{
let m = UniqueIdent([0x01; 24]);
let ev = state.op_put((m, "cal.ics".into(), "321-321".into()));
state = state.apply(&ev);
assert_eq!(state.table.len(), 1);
assert_eq!(state.resolve(ev.token()).unwrap().len(), 0);
}
// Add 2 concurrent items
let (t1, t2) = {
let blob1 = UniqueIdent([0x02; 24]);
let ev1 = state.op_put((blob1, "cal2.ics".into(), "321-321".into()));
let blob2 = UniqueIdent([0x01; 24]);
let ev2 = state.op_delete(blob2);
state = state.apply(&ev1);
state = state.apply(&ev2);
assert_eq!(state.table.len(), 1);
assert_eq!(state.resolve(ev1.token()).unwrap(), ordset![ev2.token()]);
(ev1.token(), ev2.token())
};
// Add later a new item
{
let blob3 = UniqueIdent([0x03; 24]);
let ev = state.op_put((blob3, "cal3.ics".into(), "321-321".into()));
state = state.apply(&ev);
assert_eq!(state.table.len(), 2);
assert_eq!(state.resolve(ev.token()).unwrap().len(), 0);
assert_eq!(state.resolve(t1).unwrap(), ordset![t2, ev.token()]);
}
}
}

View file

@ -0,0 +1,5 @@
pub mod calendar;
pub mod davdag;
pub mod mail;
pub mod unique_ident;
pub mod user;

View file

@ -1,6 +1,3 @@
//use std::collections::HashMap;
use std::convert::TryFrom;
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
use std::time::Duration; use std::time::Duration;
@ -11,15 +8,16 @@ use futures::{future::BoxFuture, FutureExt};
use tokio::sync::watch; use tokio::sync::watch;
use tracing::{debug, error, info, warn}; use tracing::{debug, error, info, warn};
use crate::cryptoblob; use aero_bayou::timestamp::now_msec;
use crate::login::{Credentials, PublicCredentials}; use aero_user::cryptoblob;
use aero_user::login::{Credentials, PublicCredentials};
use aero_user::storage;
use crate::mail::mailbox::Mailbox; use crate::mail::mailbox::Mailbox;
use crate::mail::uidindex::ImapUidvalidity; use crate::mail::uidindex::ImapUidvalidity;
use crate::mail::unique_ident::*;
use crate::mail::user::User;
use crate::mail::IMF; use crate::mail::IMF;
use crate::storage; use crate::unique_ident::*;
use crate::timestamp::now_msec; use crate::user::User;
const INCOMING_PK: &str = "incoming"; const INCOMING_PK: &str = "incoming";
const INCOMING_LOCK_SK: &str = "lock"; const INCOMING_LOCK_SK: &str = "lock";

View file

@ -2,14 +2,15 @@ use anyhow::{anyhow, bail, Result};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::sync::RwLock; use tokio::sync::RwLock;
use crate::bayou::Bayou; use aero_bayou::timestamp::now_msec;
use crate::cryptoblob::{self, gen_key, open_deserialize, seal_serialize, Key}; use aero_bayou::Bayou;
use crate::login::Credentials; use aero_user::cryptoblob::{self, gen_key, open_deserialize, seal_serialize, Key};
use aero_user::login::Credentials;
use aero_user::storage::{self, BlobRef, BlobVal, RowRef, RowVal, Selector, Store};
use crate::mail::uidindex::*; use crate::mail::uidindex::*;
use crate::mail::unique_ident::*;
use crate::mail::IMF; use crate::mail::IMF;
use crate::storage::{self, BlobRef, BlobVal, RowRef, RowVal, Selector, Store}; use crate::unique_ident::*;
use crate::timestamp::now_msec;
pub struct Mailbox { pub struct Mailbox {
pub(super) id: UniqueIdent, pub(super) id: UniqueIdent,
@ -17,7 +18,7 @@ pub struct Mailbox {
} }
impl Mailbox { impl Mailbox {
pub(super) async fn open( pub(crate) async fn open(
creds: &Credentials, creds: &Credentials,
id: UniqueIdent, id: UniqueIdent,
min_uidvalidity: ImapUidvalidity, min_uidvalidity: ImapUidvalidity,
@ -67,6 +68,11 @@ impl Mailbox {
self.mbox.write().await.opportunistic_sync().await self.mbox.write().await.opportunistic_sync().await
} }
/// Block until a sync has been done (due to changes in the event log)
pub async fn notify(&self) -> std::sync::Weak<tokio::sync::Notify> {
self.mbox.read().await.notifier()
}
// ---- Functions for reading the mailbox ---- // ---- Functions for reading the mailbox ----
/// Get a clone of the current UID Index of this mailbox /// Get a clone of the current UID Index of this mailbox
@ -113,7 +119,7 @@ impl Mailbox {
msg: IMF<'a>, msg: IMF<'a>,
ident: Option<UniqueIdent>, ident: Option<UniqueIdent>,
flags: &[Flag], flags: &[Flag],
) -> Result<(ImapUidvalidity, ImapUid)> { ) -> Result<(ImapUidvalidity, ImapUid, ModSeq)> {
self.mbox.write().await.append(msg, ident, flags).await self.mbox.write().await.append(msg, ident, flags).await
} }
@ -199,6 +205,10 @@ impl MailboxInternal {
Ok(()) Ok(())
} }
fn notifier(&self) -> std::sync::Weak<tokio::sync::Notify> {
self.uid_index.notifier()
}
// ---- Functions for reading the mailbox ---- // ---- Functions for reading the mailbox ----
async fn fetch_meta(&self, ids: &[UniqueIdent]) -> Result<Vec<MailMeta>> { async fn fetch_meta(&self, ids: &[UniqueIdent]) -> Result<Vec<MailMeta>> {
@ -271,7 +281,7 @@ impl MailboxInternal {
mail: IMF<'_>, mail: IMF<'_>,
ident: Option<UniqueIdent>, ident: Option<UniqueIdent>,
flags: &[Flag], flags: &[Flag],
) -> Result<(ImapUidvalidity, ImapUid)> { ) -> Result<(ImapUidvalidity, ImapUid, ModSeq)> {
let ident = ident.unwrap_or_else(gen_ident); let ident = ident.unwrap_or_else(gen_ident);
let message_key = gen_key(); let message_key = gen_key();
@ -312,14 +322,14 @@ impl MailboxInternal {
let add_mail_op = uid_state.op_mail_add(ident, flags.to_vec()); let add_mail_op = uid_state.op_mail_add(ident, flags.to_vec());
let uidvalidity = uid_state.uidvalidity; let uidvalidity = uid_state.uidvalidity;
let uid = match add_mail_op { let (uid, modseq) = match add_mail_op {
UidIndexOp::MailAdd(_, uid, _) => uid, UidIndexOp::MailAdd(_, uid, modseq, _) => (uid, modseq),
_ => unreachable!(), _ => unreachable!(),
}; };
self.uid_index.push(add_mail_op).await?; self.uid_index.push(add_mail_op).await?;
Ok((uidvalidity, uid)) Ok((uidvalidity, uid, modseq))
} }
async fn append_from_s3<'a>( async fn append_from_s3<'a>(
@ -365,7 +375,7 @@ impl MailboxInternal {
async fn delete(&mut self, ident: UniqueIdent) -> Result<()> { async fn delete(&mut self, ident: UniqueIdent) -> Result<()> {
if !self.uid_index.state().table.contains_key(&ident) { if !self.uid_index.state().table.contains_key(&ident) {
bail!("Cannot delete mail that doesn't exit"); bail!("Cannot delete mail that doesn't exist");
} }
let del_mail_op = self.uid_index.state().op_mail_del(ident); let del_mail_op = self.uid_index.state().op_mail_del(ident);
@ -432,7 +442,7 @@ impl MailboxInternal {
.table .table
.get(&source_id) .get(&source_id)
.ok_or(anyhow!("Source mail not found"))? .ok_or(anyhow!("Source mail not found"))?
.1 .2
.clone(); .clone();
futures::try_join!( futures::try_join!(
@ -465,6 +475,9 @@ impl MailboxInternal {
} }
} }
// Can be useful to debug so we want this code
// to be available to developers
#[allow(dead_code)]
fn dump(uid_index: &Bayou<UidIndex>) { fn dump(uid_index: &Bayou<UidIndex>) {
let s = uid_index.state(); let s = uid_index.state();
println!("---- MAILBOX STATE ----"); println!("---- MAILBOX STATE ----");
@ -476,7 +489,7 @@ fn dump(uid_index: &Bayou<UidIndex>) {
"{} {} {}", "{} {} {}",
uid, uid,
hex::encode(ident.0), hex::encode(ident.0),
s.table.get(ident).cloned().unwrap().1.join(", ") s.table.get(ident).cloned().unwrap().2.join(", ")
); );
} }
println!(); println!();
@ -486,7 +499,7 @@ fn dump(uid_index: &Bayou<UidIndex>) {
/// The metadata of a message that is stored in K2V /// The metadata of a message that is stored in K2V
/// at pk = mail/<mailbox uuid>, sk = <message uuid> /// at pk = mail/<mailbox uuid>, sk = <message uuid>
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MailMeta { pub struct MailMeta {
/// INTERNALDATE field (milliseconds since epoch) /// INTERNALDATE field (milliseconds since epoch)
pub internaldate: u64, pub internaldate: u64,

View file

@ -1,12 +1,9 @@
use std::convert::TryFrom;
pub mod incoming; pub mod incoming;
pub mod mailbox; pub mod mailbox;
pub mod namespace;
pub mod query; pub mod query;
pub mod snapshot; pub mod snapshot;
pub mod uidindex; pub mod uidindex;
pub mod unique_ident;
pub mod user;
// Internet Message Format // Internet Message Format
// aka RFC 822 - RFC 2822 - RFC 5322 // aka RFC 822 - RFC 2822 - RFC 5322

View file

@ -0,0 +1,206 @@
use std::collections::BTreeMap;
use anyhow::{bail, Result};
use serde::{Deserialize, Serialize};
use aero_bayou::timestamp::now_msec;
use crate::mail::uidindex::ImapUidvalidity;
use crate::unique_ident::{gen_ident, UniqueIdent};
pub const MAILBOX_HIERARCHY_DELIMITER: char = '.';
/// INBOX is the only mailbox that must always exist.
/// It is created automatically when the account is created.
/// IMAP allows the user to rename INBOX to something else,
/// in this case all messages from INBOX are moved to a mailbox
/// with the new name and the INBOX mailbox still exists and is empty.
/// In our implementation, we indeed move the underlying mailbox
/// to the new name (i.e. the new name has the same id as the previous
/// INBOX), and we create a new empty mailbox for INBOX.
pub const INBOX: &str = "INBOX";
/// For convenience purpose, we also create some special mailbox
/// that are described in RFC6154 SPECIAL-USE
/// @FIXME maybe it should be a configuration parameter
/// @FIXME maybe we should have a per-mailbox flag mechanism, either an enum or a string, so we
/// track which mailbox is used for what.
/// @FIXME Junk could be useful but we don't have any antispam solution yet so...
/// @FIXME IMAP supports virtual mailbox. \All or \Flagged are intended to be virtual mailboxes.
/// \Trash might be one, or not one. I don't know what we should do there.
pub const DRAFTS: &str = "Drafts";
pub const ARCHIVE: &str = "Archive";
pub const SENT: &str = "Sent";
pub const TRASH: &str = "Trash";
pub(crate) const MAILBOX_LIST_PK: &str = "mailboxes";
pub(crate) const MAILBOX_LIST_SK: &str = "list";
// ---- User's mailbox list (serialized in K2V) ----
#[derive(Serialize, Deserialize)]
pub(crate) struct MailboxList(BTreeMap<String, MailboxListEntry>);
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
pub(crate) struct MailboxListEntry {
id_lww: (u64, Option<UniqueIdent>),
uidvalidity: ImapUidvalidity,
}
impl MailboxListEntry {
fn merge(&mut self, other: &Self) {
// Simple CRDT merge rule
if other.id_lww.0 > self.id_lww.0
|| (other.id_lww.0 == self.id_lww.0 && other.id_lww.1 > self.id_lww.1)
{
self.id_lww = other.id_lww;
}
self.uidvalidity = std::cmp::max(self.uidvalidity, other.uidvalidity);
}
}
impl MailboxList {
pub(crate) fn new() -> Self {
Self(BTreeMap::new())
}
pub(crate) fn merge(&mut self, list2: Self) {
for (k, v) in list2.0.into_iter() {
if let Some(e) = self.0.get_mut(&k) {
e.merge(&v);
} else {
self.0.insert(k, v);
}
}
}
pub(crate) fn existing_mailbox_names(&self) -> Vec<String> {
self.0
.iter()
.filter(|(_, v)| v.id_lww.1.is_some())
.map(|(k, _)| k.to_string())
.collect()
}
pub(crate) fn has_mailbox(&self, name: &str) -> bool {
matches!(
self.0.get(name),
Some(MailboxListEntry {
id_lww: (_, Some(_)),
..
})
)
}
pub(crate) fn get_mailbox(&self, name: &str) -> Option<(ImapUidvalidity, Option<UniqueIdent>)> {
self.0.get(name).map(
|MailboxListEntry {
id_lww: (_, mailbox_id),
uidvalidity,
}| (*uidvalidity, *mailbox_id),
)
}
/// Ensures mailbox `name` maps to id `id`.
/// If it already mapped to that, returns None.
/// If a change had to be done, returns Some(new uidvalidity in mailbox).
pub(crate) fn set_mailbox(
&mut self,
name: &str,
id: Option<UniqueIdent>,
) -> Option<ImapUidvalidity> {
let (ts, id, uidvalidity) = match self.0.get_mut(name) {
None => {
if id.is_none() {
return None;
} else {
(now_msec(), id, ImapUidvalidity::new(1).unwrap())
}
}
Some(MailboxListEntry {
id_lww,
uidvalidity,
}) => {
if id_lww.1 == id {
return None;
} else {
(
std::cmp::max(id_lww.0 + 1, now_msec()),
id,
ImapUidvalidity::new(uidvalidity.get() + 1).unwrap(),
)
}
}
};
self.0.insert(
name.into(),
MailboxListEntry {
id_lww: (ts, id),
uidvalidity,
},
);
Some(uidvalidity)
}
pub(crate) fn update_uidvalidity(&mut self, name: &str, new_uidvalidity: ImapUidvalidity) {
match self.0.get_mut(name) {
None => {
self.0.insert(
name.into(),
MailboxListEntry {
id_lww: (now_msec(), None),
uidvalidity: new_uidvalidity,
},
);
}
Some(MailboxListEntry { uidvalidity, .. }) => {
*uidvalidity = std::cmp::max(*uidvalidity, new_uidvalidity);
}
}
}
pub(crate) fn create_mailbox(&mut self, name: &str) -> CreatedMailbox {
if let Some(MailboxListEntry {
id_lww: (_, Some(id)),
uidvalidity,
}) = self.0.get(name)
{
return CreatedMailbox::Existed(*id, *uidvalidity);
}
let id = gen_ident();
let uidvalidity = self.set_mailbox(name, Some(id)).unwrap();
CreatedMailbox::Created(id, uidvalidity)
}
pub(crate) fn rename_mailbox(&mut self, old_name: &str, new_name: &str) -> Result<()> {
if let Some((uidvalidity, Some(mbid))) = self.get_mailbox(old_name) {
if self.has_mailbox(new_name) {
bail!(
"Cannot rename {} into {}: {} already exists",
old_name,
new_name,
new_name
);
}
self.set_mailbox(old_name, None);
self.set_mailbox(new_name, Some(mbid));
self.update_uidvalidity(new_name, uidvalidity);
Ok(())
} else {
bail!(
"Cannot rename {} into {}: {} doesn't exist",
old_name,
new_name,
old_name
);
}
}
}
pub(crate) enum CreatedMailbox {
Created(UniqueIdent, ImapUidvalidity),
Existed(UniqueIdent, ImapUidvalidity),
}

View file

@ -1,8 +1,9 @@
use super::mailbox::MailMeta; use super::mailbox::MailMeta;
use super::snapshot::FrozenMailbox; use super::snapshot::FrozenMailbox;
use super::unique_ident::UniqueIdent; use crate::unique_ident::UniqueIdent;
use anyhow::Result; use anyhow::Result;
use futures::stream::{FuturesOrdered, StreamExt}; use futures::future::FutureExt;
use futures::stream::{BoxStream, Stream, StreamExt};
/// Query is in charge of fetching efficiently /// Query is in charge of fetching efficiently
/// requested data for a list of emails /// requested data for a list of emails
@ -28,64 +29,62 @@ impl QueryScope {
} }
} }
//type QueryResultStream = Box<dyn Stream<Item = Result<QueryResult>>>;
impl<'a, 'b> Query<'a, 'b> { impl<'a, 'b> Query<'a, 'b> {
pub async fn fetch(&self) -> Result<Vec<QueryResult>> { pub fn fetch(&self) -> BoxStream<Result<QueryResult>> {
match self.scope { match self.scope {
QueryScope::Index => Ok(self QueryScope::Index => Box::pin(
.emails futures::stream::iter(self.emails)
.iter() .map(|&uuid| Ok(QueryResult::IndexResult { uuid })),
.map(|&uuid| QueryResult::IndexResult { uuid }) ),
.collect()), QueryScope::Partial => Box::pin(self.partial()),
QueryScope::Partial => self.partial().await, QueryScope::Full => Box::pin(self.full()),
QueryScope::Full => self.full().await,
} }
} }
// --- functions below are private *for reasons* // --- functions below are private *for reasons*
fn partial<'d>(&'d self) -> impl Stream<Item = Result<QueryResult>> + 'd + Send {
async move {
let maybe_meta_list: Result<Vec<MailMeta>> =
self.frozen.mailbox.fetch_meta(self.emails).await;
let list_res = maybe_meta_list
.map(|meta_list| {
meta_list
.into_iter()
.zip(self.emails)
.map(|(metadata, &uuid)| Ok(QueryResult::PartialResult { uuid, metadata }))
.collect()
})
.unwrap_or_else(|e| vec![Err(e)]);
async fn partial(&self) -> Result<Vec<QueryResult>> { futures::stream::iter(list_res)
let meta = self.frozen.mailbox.fetch_meta(self.emails).await?; }
let result = meta .flatten_stream()
.into_iter()
.zip(self.emails.iter())
.map(|(metadata, &uuid)| QueryResult::PartialResult { uuid, metadata })
.collect::<Vec<_>>();
Ok(result)
} }
/// @FIXME WARNING: THIS CAN ALLOCATE A LOT OF MEMORY fn full<'d>(&'d self) -> impl Stream<Item = Result<QueryResult>> + 'd + Send {
/// AND GENERATE SO MUCH NETWORK TRAFFIC. self.partial().then(move |maybe_meta| async move {
/// THIS FUNCTION SHOULD BE REWRITTEN, FOR EXAMPLE WITH let meta = maybe_meta?;
/// SOMETHING LIKE AN ITERATOR
async fn full(&self) -> Result<Vec<QueryResult>> {
let meta_list = self.partial().await?;
meta_list
.into_iter()
.map(|meta| async move {
let content = self
.frozen
.mailbox
.fetch_full(
*meta.uuid(),
&meta
.metadata()
.expect("meta to be PartialResult")
.message_key,
)
.await?;
Ok(meta.into_full(content).expect("meta to be PartialResult")) let content = self
}) .frozen
.collect::<FuturesOrdered<_>>() .mailbox
.collect::<Vec<_>>() .fetch_full(
.await *meta.uuid(),
.into_iter() &meta
.collect::<Result<Vec<_>, _>>() .metadata()
.expect("meta to be PartialResult")
.message_key,
)
.await?;
Ok(meta.into_full(content).expect("meta to be PartialResult"))
})
} }
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub enum QueryResult { pub enum QueryResult {
IndexResult { IndexResult {
uuid: UniqueIdent, uuid: UniqueIdent,
@ -125,13 +124,6 @@ impl QueryResult {
} }
} }
fn into_partial(self, metadata: MailMeta) -> Option<Self> {
match self {
Self::IndexResult { uuid } => Some(Self::PartialResult { uuid, metadata }),
_ => None,
}
}
fn into_full(self, content: Vec<u8>) -> Option<Self> { fn into_full(self, content: Vec<u8>) -> Option<Self> {
match self { match self {
Self::PartialResult { uuid, metadata } => Some(Self::FullResult { Self::PartialResult { uuid, metadata } => Some(Self::FullResult {

View file

@ -5,7 +5,7 @@ use anyhow::Result;
use super::mailbox::Mailbox; use super::mailbox::Mailbox;
use super::query::{Query, QueryScope}; use super::query::{Query, QueryScope};
use super::uidindex::UidIndex; use super::uidindex::UidIndex;
use super::unique_ident::UniqueIdent; use crate::unique_ident::UniqueIdent;
/// A Frozen Mailbox has a snapshot of the current mailbox /// A Frozen Mailbox has a snapshot of the current mailbox
/// state that is desynchronized with the real mailbox state. /// state that is desynchronized with the real mailbox state.

View file

@ -1,15 +1,16 @@
use std::num::NonZeroU32; use std::num::{NonZeroU32, NonZeroU64};
use im::{HashMap, OrdMap, OrdSet}; use im::{HashMap, OrdMap, OrdSet};
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::bayou::*; use crate::unique_ident::UniqueIdent;
use crate::mail::unique_ident::UniqueIdent; use aero_bayou::*;
pub type ModSeq = NonZeroU64;
pub type ImapUid = NonZeroU32; pub type ImapUid = NonZeroU32;
pub type ImapUidvalidity = NonZeroU32; pub type ImapUidvalidity = NonZeroU32;
pub type Flag = String; pub type Flag = String;
pub type IndexEntry = (ImapUid, Vec<Flag>); pub type IndexEntry = (ImapUid, ModSeq, Vec<Flag>);
/// A UidIndex handles the mutable part of a mailbox /// A UidIndex handles the mutable part of a mailbox
/// It is built by running the event log on it /// It is built by running the event log on it
@ -23,28 +24,33 @@ pub struct UidIndex {
// Indexes optimized for queries // Indexes optimized for queries
pub idx_by_uid: OrdMap<ImapUid, UniqueIdent>, pub idx_by_uid: OrdMap<ImapUid, UniqueIdent>,
pub idx_by_modseq: OrdMap<ModSeq, UniqueIdent>,
pub idx_by_flag: FlagIndex, pub idx_by_flag: FlagIndex,
// Counters // "Public" Counters
pub uidvalidity: ImapUidvalidity, pub uidvalidity: ImapUidvalidity,
pub uidnext: ImapUid, pub uidnext: ImapUid,
pub highestmodseq: ModSeq,
// "Internal" Counters
pub internalseq: ImapUid, pub internalseq: ImapUid,
pub internalmodseq: ModSeq,
} }
#[derive(Clone, Serialize, Deserialize, Debug)] #[derive(Clone, Serialize, Deserialize, Debug)]
pub enum UidIndexOp { pub enum UidIndexOp {
MailAdd(UniqueIdent, ImapUid, Vec<Flag>), MailAdd(UniqueIdent, ImapUid, ModSeq, Vec<Flag>),
MailDel(UniqueIdent), MailDel(UniqueIdent),
FlagAdd(UniqueIdent, Vec<Flag>), FlagAdd(UniqueIdent, ModSeq, Vec<Flag>),
FlagDel(UniqueIdent, Vec<Flag>), FlagDel(UniqueIdent, ModSeq, Vec<Flag>),
FlagSet(UniqueIdent, Vec<Flag>), FlagSet(UniqueIdent, ModSeq, Vec<Flag>),
BumpUidvalidity(u32), BumpUidvalidity(u32),
} }
impl UidIndex { impl UidIndex {
#[must_use] #[must_use]
pub fn op_mail_add(&self, ident: UniqueIdent, flags: Vec<Flag>) -> UidIndexOp { pub fn op_mail_add(&self, ident: UniqueIdent, flags: Vec<Flag>) -> UidIndexOp {
UidIndexOp::MailAdd(ident, self.internalseq, flags) UidIndexOp::MailAdd(ident, self.internalseq, self.internalmodseq, flags)
} }
#[must_use] #[must_use]
@ -54,17 +60,17 @@ impl UidIndex {
#[must_use] #[must_use]
pub fn op_flag_add(&self, ident: UniqueIdent, flags: Vec<Flag>) -> UidIndexOp { pub fn op_flag_add(&self, ident: UniqueIdent, flags: Vec<Flag>) -> UidIndexOp {
UidIndexOp::FlagAdd(ident, flags) UidIndexOp::FlagAdd(ident, self.internalmodseq, flags)
} }
#[must_use] #[must_use]
pub fn op_flag_del(&self, ident: UniqueIdent, flags: Vec<Flag>) -> UidIndexOp { pub fn op_flag_del(&self, ident: UniqueIdent, flags: Vec<Flag>) -> UidIndexOp {
UidIndexOp::FlagDel(ident, flags) UidIndexOp::FlagDel(ident, self.internalmodseq, flags)
} }
#[must_use] #[must_use]
pub fn op_flag_set(&self, ident: UniqueIdent, flags: Vec<Flag>) -> UidIndexOp { pub fn op_flag_set(&self, ident: UniqueIdent, flags: Vec<Flag>) -> UidIndexOp {
UidIndexOp::FlagSet(ident, flags) UidIndexOp::FlagSet(ident, self.internalmodseq, flags)
} }
#[must_use] #[must_use]
@ -74,18 +80,19 @@ impl UidIndex {
// INTERNAL functions to keep state consistent // INTERNAL functions to keep state consistent
fn reg_email(&mut self, ident: UniqueIdent, uid: ImapUid, flags: &[Flag]) { fn reg_email(&mut self, ident: UniqueIdent, uid: ImapUid, modseq: ModSeq, flags: &[Flag]) {
// Insert the email in our table // Insert the email in our table
self.table.insert(ident, (uid, flags.to_owned())); self.table.insert(ident, (uid, modseq, flags.to_owned()));
// Update the indexes/caches // Update the indexes/caches
self.idx_by_uid.insert(uid, ident); self.idx_by_uid.insert(uid, ident);
self.idx_by_flag.insert(uid, flags); self.idx_by_flag.insert(uid, flags);
self.idx_by_modseq.insert(modseq, ident);
} }
fn unreg_email(&mut self, ident: &UniqueIdent) { fn unreg_email(&mut self, ident: &UniqueIdent) {
// We do nothing if the mail does not exist // We do nothing if the mail does not exist
let (uid, flags) = match self.table.get(ident) { let (uid, modseq, flags) = match self.table.get(ident) {
Some(v) => v, Some(v) => v,
None => return, None => return,
}; };
@ -93,6 +100,7 @@ impl UidIndex {
// Delete all cache entries // Delete all cache entries
self.idx_by_uid.remove(uid); self.idx_by_uid.remove(uid);
self.idx_by_flag.remove(*uid, flags); self.idx_by_flag.remove(*uid, flags);
self.idx_by_modseq.remove(modseq);
// Remove from source of trust // Remove from source of trust
self.table.remove(ident); self.table.remove(ident);
@ -103,11 +111,17 @@ impl Default for UidIndex {
fn default() -> Self { fn default() -> Self {
Self { Self {
table: OrdMap::new(), table: OrdMap::new(),
idx_by_uid: OrdMap::new(), idx_by_uid: OrdMap::new(),
idx_by_modseq: OrdMap::new(),
idx_by_flag: FlagIndex::new(), idx_by_flag: FlagIndex::new(),
uidvalidity: NonZeroU32::new(1).unwrap(), uidvalidity: NonZeroU32::new(1).unwrap(),
uidnext: NonZeroU32::new(1).unwrap(), uidnext: NonZeroU32::new(1).unwrap(),
highestmodseq: NonZeroU64::new(1).unwrap(),
internalseq: NonZeroU32::new(1).unwrap(), internalseq: NonZeroU32::new(1).unwrap(),
internalmodseq: NonZeroU64::new(1).unwrap(),
} }
} }
} }
@ -118,17 +132,23 @@ impl BayouState for UidIndex {
fn apply(&self, op: &UidIndexOp) -> Self { fn apply(&self, op: &UidIndexOp) -> Self {
let mut new = self.clone(); let mut new = self.clone();
match op { match op {
UidIndexOp::MailAdd(ident, uid, flags) => { UidIndexOp::MailAdd(ident, uid, modseq, flags) => {
// Change UIDValidity if there is a conflict // Change UIDValidity if there is a UID conflict or a MODSEQ conflict
if *uid < new.internalseq { // @FIXME Need to prove that summing work
// The intuition: we increase the UIDValidity by the number of possible conflicts
if *uid < new.internalseq || *modseq < new.internalmodseq {
let bump_uid = new.internalseq.get() - uid.get();
let bump_modseq = (new.internalmodseq.get() - modseq.get()) as u32;
new.uidvalidity = new.uidvalidity =
NonZeroU32::new(new.uidvalidity.get() + new.internalseq.get() - uid.get()) NonZeroU32::new(new.uidvalidity.get() + bump_uid + bump_modseq).unwrap();
.unwrap();
} }
// Assign the real uid of the email // Assign the real uid of the email
let new_uid = new.internalseq; let new_uid = new.internalseq;
// Assign the real modseq of the email and its new flags
let new_modseq = new.internalmodseq;
// Delete the previous entry if any. // Delete the previous entry if any.
// Our proof has no assumption on `ident` uniqueness, // Our proof has no assumption on `ident` uniqueness,
// so we must handle this case even it is very unlikely // so we must handle this case even it is very unlikely
@ -137,10 +157,14 @@ impl BayouState for UidIndex {
new.unreg_email(ident); new.unreg_email(ident);
// We record our email and update ou caches // We record our email and update ou caches
new.reg_email(*ident, new_uid, flags); new.reg_email(*ident, new_uid, new_modseq, flags);
// Update counters // Update counters
new.highestmodseq = new.internalmodseq;
new.internalseq = NonZeroU32::new(new.internalseq.get() + 1).unwrap(); new.internalseq = NonZeroU32::new(new.internalseq.get() + 1).unwrap();
new.internalmodseq = NonZeroU64::new(new.internalmodseq.get() + 1).unwrap();
new.uidnext = new.internalseq; new.uidnext = new.internalseq;
} }
UidIndexOp::MailDel(ident) => { UidIndexOp::MailDel(ident) => {
@ -150,8 +174,16 @@ impl BayouState for UidIndex {
// We update the counter // We update the counter
new.internalseq = NonZeroU32::new(new.internalseq.get() + 1).unwrap(); new.internalseq = NonZeroU32::new(new.internalseq.get() + 1).unwrap();
} }
UidIndexOp::FlagAdd(ident, new_flags) => { UidIndexOp::FlagAdd(ident, candidate_modseq, new_flags) => {
if let Some((uid, existing_flags)) = new.table.get_mut(ident) { if let Some((uid, email_modseq, existing_flags)) = new.table.get_mut(ident) {
// Bump UIDValidity if required
if *candidate_modseq < new.internalmodseq {
let bump_modseq =
(new.internalmodseq.get() - candidate_modseq.get()) as u32;
new.uidvalidity =
NonZeroU32::new(new.uidvalidity.get() + bump_modseq).unwrap();
}
// Add flags to the source of trust and the cache // Add flags to the source of trust and the cache
let mut to_add: Vec<Flag> = new_flags let mut to_add: Vec<Flag> = new_flags
.iter() .iter()
@ -159,18 +191,48 @@ impl BayouState for UidIndex {
.cloned() .cloned()
.collect(); .collect();
new.idx_by_flag.insert(*uid, &to_add); new.idx_by_flag.insert(*uid, &to_add);
*email_modseq = new.internalmodseq;
new.idx_by_modseq.insert(new.internalmodseq, *ident);
existing_flags.append(&mut to_add); existing_flags.append(&mut to_add);
// Update counters
new.highestmodseq = new.internalmodseq;
new.internalmodseq = NonZeroU64::new(new.internalmodseq.get() + 1).unwrap();
} }
} }
UidIndexOp::FlagDel(ident, rm_flags) => { UidIndexOp::FlagDel(ident, candidate_modseq, rm_flags) => {
if let Some((uid, existing_flags)) = new.table.get_mut(ident) { if let Some((uid, email_modseq, existing_flags)) = new.table.get_mut(ident) {
// Bump UIDValidity if required
if *candidate_modseq < new.internalmodseq {
let bump_modseq =
(new.internalmodseq.get() - candidate_modseq.get()) as u32;
new.uidvalidity =
NonZeroU32::new(new.uidvalidity.get() + bump_modseq).unwrap();
}
// Remove flags from the source of trust and the cache // Remove flags from the source of trust and the cache
existing_flags.retain(|x| !rm_flags.contains(x)); existing_flags.retain(|x| !rm_flags.contains(x));
new.idx_by_flag.remove(*uid, rm_flags); new.idx_by_flag.remove(*uid, rm_flags);
// Register that email has been modified
new.idx_by_modseq.insert(new.internalmodseq, *ident);
*email_modseq = new.internalmodseq;
// Update counters
new.highestmodseq = new.internalmodseq;
new.internalmodseq = NonZeroU64::new(new.internalmodseq.get() + 1).unwrap();
} }
} }
UidIndexOp::FlagSet(ident, new_flags) => { UidIndexOp::FlagSet(ident, candidate_modseq, new_flags) => {
if let Some((uid, existing_flags)) = new.table.get_mut(ident) { if let Some((uid, email_modseq, existing_flags)) = new.table.get_mut(ident) {
// Bump UIDValidity if required
if *candidate_modseq < new.internalmodseq {
let bump_modseq =
(new.internalmodseq.get() - candidate_modseq.get()) as u32;
new.uidvalidity =
NonZeroU32::new(new.uidvalidity.get() + bump_modseq).unwrap();
}
// Remove flags from the source of trust and the cache // Remove flags from the source of trust and the cache
let (keep_flags, rm_flags): (Vec<String>, Vec<String>) = existing_flags let (keep_flags, rm_flags): (Vec<String>, Vec<String>) = existing_flags
.iter() .iter()
@ -185,6 +247,14 @@ impl BayouState for UidIndex {
existing_flags.append(&mut to_add); existing_flags.append(&mut to_add);
new.idx_by_flag.remove(*uid, &rm_flags); new.idx_by_flag.remove(*uid, &rm_flags);
new.idx_by_flag.insert(*uid, &to_add); new.idx_by_flag.insert(*uid, &to_add);
// Register that email has been modified
new.idx_by_modseq.insert(new.internalmodseq, *ident);
*email_modseq = new.internalmodseq;
// Update counters
new.highestmodseq = new.internalmodseq;
new.internalmodseq = NonZeroU64::new(new.internalmodseq.get() + 1).unwrap();
} }
} }
UidIndexOp::BumpUidvalidity(count) => { UidIndexOp::BumpUidvalidity(count) => {
@ -238,10 +308,14 @@ impl FlagIndex {
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
struct UidIndexSerializedRepr { struct UidIndexSerializedRepr {
mails: Vec<(ImapUid, UniqueIdent, Vec<Flag>)>, mails: Vec<(ImapUid, ModSeq, UniqueIdent, Vec<Flag>)>,
uidvalidity: ImapUidvalidity, uidvalidity: ImapUidvalidity,
uidnext: ImapUid, uidnext: ImapUid,
highestmodseq: ModSeq,
internalseq: ImapUid, internalseq: ImapUid,
internalmodseq: ModSeq,
} }
impl<'de> Deserialize<'de> for UidIndex { impl<'de> Deserialize<'de> for UidIndex {
@ -253,16 +327,22 @@ impl<'de> Deserialize<'de> for UidIndex {
let mut uidindex = UidIndex { let mut uidindex = UidIndex {
table: OrdMap::new(), table: OrdMap::new(),
idx_by_uid: OrdMap::new(), idx_by_uid: OrdMap::new(),
idx_by_modseq: OrdMap::new(),
idx_by_flag: FlagIndex::new(), idx_by_flag: FlagIndex::new(),
uidvalidity: val.uidvalidity, uidvalidity: val.uidvalidity,
uidnext: val.uidnext, uidnext: val.uidnext,
highestmodseq: val.highestmodseq,
internalseq: val.internalseq, internalseq: val.internalseq,
internalmodseq: val.internalmodseq,
}; };
val.mails val.mails
.iter() .iter()
.for_each(|(u, i, f)| uidindex.reg_email(*i, *u, f)); .for_each(|(uid, modseq, uuid, flags)| uidindex.reg_email(*uuid, *uid, *modseq, flags));
Ok(uidindex) Ok(uidindex)
} }
@ -274,15 +354,17 @@ impl Serialize for UidIndex {
S: Serializer, S: Serializer,
{ {
let mut mails = vec![]; let mut mails = vec![];
for (ident, (uid, flags)) in self.table.iter() { for (ident, (uid, modseq, flags)) in self.table.iter() {
mails.push((*uid, *ident, flags.clone())); mails.push((*uid, *modseq, *ident, flags.clone()));
} }
let val = UidIndexSerializedRepr { let val = UidIndexSerializedRepr {
mails, mails,
uidvalidity: self.uidvalidity, uidvalidity: self.uidvalidity,
uidnext: self.uidnext, uidnext: self.uidnext,
highestmodseq: self.highestmodseq,
internalseq: self.internalseq, internalseq: self.internalseq,
internalmodseq: self.internalmodseq,
}; };
val.serialize(serializer) val.serialize(serializer)
@ -308,8 +390,9 @@ mod tests {
// Early checks // Early checks
assert_eq!(state.table.len(), 1); assert_eq!(state.table.len(), 1);
let (uid, flags) = state.table.get(&m).unwrap(); let (uid, modseq, flags) = state.table.get(&m).unwrap();
assert_eq!(*uid, NonZeroU32::new(1).unwrap()); assert_eq!(*uid, NonZeroU32::new(1).unwrap());
assert_eq!(*modseq, NonZeroU64::new(1).unwrap());
assert_eq!(flags.len(), 2); assert_eq!(flags.len(), 2);
let ident = state.idx_by_uid.get(&NonZeroU32::new(1).unwrap()).unwrap(); let ident = state.idx_by_uid.get(&NonZeroU32::new(1).unwrap()).unwrap();
assert_eq!(&m, ident); assert_eq!(&m, ident);
@ -364,7 +447,12 @@ mod tests {
{ {
let m = UniqueIdent([0x03; 24]); let m = UniqueIdent([0x03; 24]);
let f = vec!["\\Archive".to_string(), "\\Recent".to_string()]; let f = vec!["\\Archive".to_string(), "\\Recent".to_string()];
let ev = UidIndexOp::MailAdd(m, NonZeroU32::new(1).unwrap(), f); let ev = UidIndexOp::MailAdd(
m,
NonZeroU32::new(1).unwrap(),
NonZeroU64::new(1).unwrap(),
f,
);
state = state.apply(&ev); state = state.apply(&ev);
} }

View file

@ -5,9 +5,9 @@ use lazy_static::lazy_static;
use rand::prelude::*; use rand::prelude::*;
use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer}; use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer};
use crate::timestamp::now_msec; use aero_bayou::timestamp::now_msec;
/// An internal Mail Identifier is composed of two components: /// An internal Aerogramme identifier is composed of two components:
/// - a process identifier, 128 bits, itself composed of: /// - a process identifier, 128 bits, itself composed of:
/// - the timestamp of when the process started, 64 bits /// - the timestamp of when the process started, 64 bits
/// - a 64-bit random number /// - a 64-bit random number
@ -15,7 +15,7 @@ use crate::timestamp::now_msec;
/// They are not part of the protocol but an internal representation /// They are not part of the protocol but an internal representation
/// required by Aerogramme. /// required by Aerogramme.
/// Their main property is to be unique without having to rely /// Their main property is to be unique without having to rely
/// on synchronization between IMAP processes. /// on synchronization between (IMAP) processes.
#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)] #[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct UniqueIdent(pub [u8; 24]); pub struct UniqueIdent(pub [u8; 24]);

View file

@ -1,41 +1,41 @@
use std::collections::{BTreeMap, HashMap}; use std::collections::HashMap;
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, bail, Result};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use tokio::sync::watch; use tokio::sync::watch;
use crate::cryptoblob::{open_deserialize, seal_serialize}; use aero_user::cryptoblob::{open_deserialize, seal_serialize};
use crate::login::Credentials; use aero_user::login::Credentials;
use aero_user::storage;
use crate::calendar::namespace::CalendarNs;
use crate::mail::incoming::incoming_mail_watch_process; use crate::mail::incoming::incoming_mail_watch_process;
use crate::mail::mailbox::Mailbox; use crate::mail::mailbox::Mailbox;
use crate::mail::namespace::{
CreatedMailbox, MailboxList, ARCHIVE, DRAFTS, INBOX, MAILBOX_HIERARCHY_DELIMITER,
MAILBOX_LIST_PK, MAILBOX_LIST_SK, SENT, TRASH,
};
use crate::mail::uidindex::ImapUidvalidity; use crate::mail::uidindex::ImapUidvalidity;
use crate::mail::unique_ident::{gen_ident, UniqueIdent}; use crate::unique_ident::UniqueIdent;
use crate::storage;
use crate::timestamp::now_msec;
pub const MAILBOX_HIERARCHY_DELIMITER: char = '.'; //@FIXME User should be totally rewriten
// to extract the local mailbox list
// to the mail/namespace.rs file (and mailbox list should be reworded as mail namespace)
/// INBOX is the only mailbox that must always exist. //@FIXME User should be run in a LocalSet
/// It is created automatically when the account is created. // to remove most - if not all - synchronizations types.
/// IMAP allows the user to rename INBOX to something else, // Especially RwLock & co.
/// in this case all messages from INBOX are moved to a mailbox
/// with the new name and the INBOX mailbox still exists and is empty.
/// In our implementation, we indeed move the underlying mailbox
/// to the new name (i.e. the new name has the same id as the previous
/// INBOX), and we create a new empty mailbox for INBOX.
pub const INBOX: &str = "INBOX";
const MAILBOX_LIST_PK: &str = "mailboxes";
const MAILBOX_LIST_SK: &str = "list";
pub struct User { pub struct User {
pub username: String, pub username: String,
pub creds: Credentials, pub creds: Credentials,
pub storage: storage::Store, pub storage: storage::Store,
pub mailboxes: std::sync::Mutex<HashMap<UniqueIdent, Weak<Mailbox>>>, pub mailboxes: std::sync::Mutex<HashMap<UniqueIdent, Weak<Mailbox>>>,
pub calendars: CalendarNs,
// Handle on worker processing received email
// (moving emails from the mailqueue to the user's INBOX)
tx_inbox_id: watch::Sender<Option<(UniqueIdent, ImapUidvalidity)>>, tx_inbox_id: watch::Sender<Option<(UniqueIdent, ImapUidvalidity)>>,
} }
@ -124,7 +124,7 @@ impl User {
let (mut list, ct) = self.load_mailbox_list().await?; let (mut list, ct) = self.load_mailbox_list().await?;
if list.has_mailbox(name) { if list.has_mailbox(name) {
// TODO: actually delete mailbox contents //@TODO: actually delete mailbox contents
list.set_mailbox(name, None); list.set_mailbox(name, None);
self.save_mailbox_list(&list, ct).await?; self.save_mailbox_list(&list, ct).await?;
Ok(()) Ok(())
@ -189,6 +189,7 @@ impl User {
storage, storage,
tx_inbox_id, tx_inbox_id,
mailboxes: std::sync::Mutex::new(HashMap::new()), mailboxes: std::sync::Mutex::new(HashMap::new()),
calendars: CalendarNs::new(),
}); });
// Ensure INBOX exists (done inside load_mailbox_list) // Ensure INBOX exists (done inside load_mailbox_list)
@ -215,6 +216,10 @@ impl User {
} }
} }
// The idea here is that:
// 1. Opening a mailbox that is not already opened takes a significant amount of time
// 2. We don't want to lock the whole HashMap that contain the mailboxes during this
// operation which is why we droppped the lock above but take it again below.
let mb = Arc::new(Mailbox::open(&self.creds, id, min_uidvalidity).await?); let mb = Arc::new(Mailbox::open(&self.creds, id, min_uidvalidity).await?);
let mut cache = self.mailboxes.lock().unwrap(); let mut cache = self.mailboxes.lock().unwrap();
@ -256,7 +261,18 @@ impl User {
} }
}; };
self.ensure_inbox_exists(&mut list, &row).await?; let is_default_mbx_missing = [DRAFTS, ARCHIVE, SENT, TRASH]
.iter()
.map(|mbx| list.create_mailbox(mbx))
.fold(false, |acc, r| {
acc || matches!(r, CreatedMailbox::Created(..))
});
let is_inbox_missing = self.ensure_inbox_exists(&mut list, &row).await?;
if is_default_mbx_missing && !is_inbox_missing {
// It's the only case where we created some mailboxes and not saved them
// So we save them!
self.save_mailbox_list(&list, row.clone()).await?;
}
Ok((list, row)) Ok((list, row))
} }
@ -303,171 +319,6 @@ impl User {
} }
} }
// ---- User's mailbox list (serialized in K2V) ----
#[derive(Serialize, Deserialize)]
struct MailboxList(BTreeMap<String, MailboxListEntry>);
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
struct MailboxListEntry {
id_lww: (u64, Option<UniqueIdent>),
uidvalidity: ImapUidvalidity,
}
impl MailboxListEntry {
fn merge(&mut self, other: &Self) {
// Simple CRDT merge rule
if other.id_lww.0 > self.id_lww.0
|| (other.id_lww.0 == self.id_lww.0 && other.id_lww.1 > self.id_lww.1)
{
self.id_lww = other.id_lww;
}
self.uidvalidity = std::cmp::max(self.uidvalidity, other.uidvalidity);
}
}
impl MailboxList {
fn new() -> Self {
Self(BTreeMap::new())
}
fn merge(&mut self, list2: Self) {
for (k, v) in list2.0.into_iter() {
if let Some(e) = self.0.get_mut(&k) {
e.merge(&v);
} else {
self.0.insert(k, v);
}
}
}
fn existing_mailbox_names(&self) -> Vec<String> {
self.0
.iter()
.filter(|(_, v)| v.id_lww.1.is_some())
.map(|(k, _)| k.to_string())
.collect()
}
fn has_mailbox(&self, name: &str) -> bool {
matches!(
self.0.get(name),
Some(MailboxListEntry {
id_lww: (_, Some(_)),
..
})
)
}
fn get_mailbox(&self, name: &str) -> Option<(ImapUidvalidity, Option<UniqueIdent>)> {
self.0.get(name).map(
|MailboxListEntry {
id_lww: (_, mailbox_id),
uidvalidity,
}| (*uidvalidity, *mailbox_id),
)
}
/// Ensures mailbox `name` maps to id `id`.
/// If it already mapped to that, returns None.
/// If a change had to be done, returns Some(new uidvalidity in mailbox).
fn set_mailbox(&mut self, name: &str, id: Option<UniqueIdent>) -> Option<ImapUidvalidity> {
let (ts, id, uidvalidity) = match self.0.get_mut(name) {
None => {
if id.is_none() {
return None;
} else {
(now_msec(), id, ImapUidvalidity::new(1).unwrap())
}
}
Some(MailboxListEntry {
id_lww,
uidvalidity,
}) => {
if id_lww.1 == id {
return None;
} else {
(
std::cmp::max(id_lww.0 + 1, now_msec()),
id,
ImapUidvalidity::new(uidvalidity.get() + 1).unwrap(),
)
}
}
};
self.0.insert(
name.into(),
MailboxListEntry {
id_lww: (ts, id),
uidvalidity,
},
);
Some(uidvalidity)
}
fn update_uidvalidity(&mut self, name: &str, new_uidvalidity: ImapUidvalidity) {
match self.0.get_mut(name) {
None => {
self.0.insert(
name.into(),
MailboxListEntry {
id_lww: (now_msec(), None),
uidvalidity: new_uidvalidity,
},
);
}
Some(MailboxListEntry { uidvalidity, .. }) => {
*uidvalidity = std::cmp::max(*uidvalidity, new_uidvalidity);
}
}
}
fn create_mailbox(&mut self, name: &str) -> CreatedMailbox {
if let Some(MailboxListEntry {
id_lww: (_, Some(id)),
uidvalidity,
}) = self.0.get(name)
{
return CreatedMailbox::Existed(*id, *uidvalidity);
}
let id = gen_ident();
let uidvalidity = self.set_mailbox(name, Some(id)).unwrap();
CreatedMailbox::Created(id, uidvalidity)
}
fn rename_mailbox(&mut self, old_name: &str, new_name: &str) -> Result<()> {
if let Some((uidvalidity, Some(mbid))) = self.get_mailbox(old_name) {
if self.has_mailbox(new_name) {
bail!(
"Cannot rename {} into {}: {} already exists",
old_name,
new_name,
new_name
);
}
self.set_mailbox(old_name, None);
self.set_mailbox(new_name, Some(mbid));
self.update_uidvalidity(new_name, uidvalidity);
Ok(())
} else {
bail!(
"Cannot rename {} into {}: {} doesn't exist",
old_name,
new_name,
old_name
);
}
}
}
enum CreatedMailbox {
Created(UniqueIdent, ImapUidvalidity),
Existed(UniqueIdent, ImapUidvalidity),
}
// ---- User cache ---- // ---- User cache ----
lazy_static! { lazy_static! {

1
aero-dav/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
target/

15
aero-dav/Cargo.toml Normal file
View file

@ -0,0 +1,15 @@
[package]
name = "aero-dav"
version = "0.3.0"
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
edition = "2021"
license = "EUPL-1.2"
description = "A partial and standalone implementation of the WebDAV protocol and its extensions (eg. CalDAV or CardDAV)"
[dependencies]
quick-xml.workspace = true
http.workspace = true
chrono.workspace = true
tokio.workspace = true
futures.workspace = true
tracing.workspace = true

4
aero-dav/fuzz/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
target
corpus
artifacts
coverage

4249
aero-dav/fuzz/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

24
aero-dav/fuzz/Cargo.toml Normal file
View file

@ -0,0 +1,24 @@
[package]
name = "aerogramme-fuzz"
version = "0.0.0"
publish = false
edition = "2021"
[package.metadata]
cargo-fuzz = true
[dependencies]
arbitrary = { version = "1", optional = true, features = ["derive"] }
libfuzzer-sys = { version = "0.4", features = ["arbitrary-derive"] }
tokio = { version = "1.18", default-features = false, features = ["rt", "rt-multi-thread", "io-util", "net", "time", "macros", "sync", "signal", "fs"] }
quick-xml = { version = "0.31", features = ["async-tokio"] }
[dependencies.aero-dav]
path = ".."
[[bin]]
name = "dav"
path = "fuzz_targets/dav.rs"
test = false
doc = false
bench = false

126
aero-dav/fuzz/dav.dict Normal file
View file

@ -0,0 +1,126 @@
#
# AFL dictionary for XML
# ----------------------
#
# Several basic syntax elements and attributes, modeled on libxml2.
#
# Created by Michal Zalewski
#
attr_encoding=" encoding=\"1\""
attr_generic=" a=\"1\""
attr_href=" href=\"1\""
attr_standalone=" standalone=\"no\""
attr_version=" version=\"1\""
attr_xml_base=" xml:base=\"1\""
attr_xml_id=" xml:id=\"1\""
attr_xml_lang=" xml:lang=\"1\""
attr_xml_space=" xml:space=\"1\""
attr_xmlns=" xmlns=\"1\""
entity_builtin="&lt;"
entity_decimal="&#1;"
entity_external="&a;"
entity_hex="&#x1;"
string_any="ANY"
string_brackets="[]"
string_cdata="CDATA"
string_col_fallback=":fallback"
string_col_generic=":a"
string_col_include=":include"
string_dashes="--"
string_empty="EMPTY"
string_empty_dblquotes="\"\""
string_empty_quotes="''"
string_entities="ENTITIES"
string_entity="ENTITY"
string_fixed="#FIXED"
string_id="ID"
string_idref="IDREF"
string_idrefs="IDREFS"
string_implied="#IMPLIED"
string_nmtoken="NMTOKEN"
string_nmtokens="NMTOKENS"
string_notation="NOTATION"
string_parentheses="()"
string_pcdata="#PCDATA"
string_percent="%a"
string_public="PUBLIC"
string_required="#REQUIRED"
string_schema=":schema"
string_system="SYSTEM"
string_ucs4="UCS-4"
string_utf16="UTF-16"
string_utf8="UTF-8"
string_xmlns="xmlns:"
tag_attlist="<!ATTLIST"
tag_cdata="<![CDATA["
tag_close="</a>"
tag_doctype="<!DOCTYPE"
tag_element="<!ELEMENT"
tag_entity="<!ENTITY"
tag_ignore="<![IGNORE["
tag_include="<![INCLUDE["
tag_notation="<!NOTATION"
tag_open="<a>"
tag_open_close="<a />"
tag_open_exclamation="<!"
tag_open_q="<?"
tag_sq2_close="]]>"
tag_xml_q="<?xml?>"
"0"
"1"
"activelock"
"allprop"
"cannot-modify-protected-property"
"collection"
"creationdate"
"DAV:"
"depth"
"displayname"
"error"
"exclusive"
"getcontentlanguage"
"getcontentlength"
"getcontenttype"
"getetag"
"getlastmodified"
"href"
"include"
"Infinite"
"infinity"
"location"
"lockdiscovery"
"lockentry"
"lockinfo"
"lockroot"
"lockscope"
"locktoken"
"lock-token-matches-request-uri"
"lock-token-submitted"
"locktype"
"multistatus"
"no-conflicting-lock"
"no-external-entities"
"owner"
"preserved-live-properties"
"prop"
"propertyupdate"
"propfind"
"propfind-finite-depth"
"propname"
"propstat"
"remove"
"resourcetype"
"response"
"responsedescription"
"set"
"shared"
"status"
"supportedlock"
"text/html"
"timeout"
"write"

View file

@ -0,0 +1,209 @@
#![no_main]
use libfuzzer_sys::arbitrary;
use libfuzzer_sys::arbitrary::Arbitrary;
use libfuzzer_sys::fuzz_target;
use aero_dav::{realization, types, xml};
use quick_xml::reader::NsReader;
use tokio::io::AsyncWriteExt;
use tokio::runtime::Runtime;
// Split this file
const tokens: [&str; 63] = [
"0",
"1",
"activelock",
"allprop",
"encoding",
"utf-8",
"http://ns.example.com/boxschema/",
"HTTP/1.1 200 OK",
"1997-12-01T18:27:21-08:00",
"Mon, 12 Jan 1998 09:25:56 GMT",
"\"abcdef\"",
"cannot-modify-protected-property",
"collection",
"creationdate",
"DAV:",
"D",
"C",
"xmlns:D",
"depth",
"displayname",
"error",
"exclusive",
"getcontentlanguage",
"getcontentlength",
"getcontenttype",
"getetag",
"getlastmodified",
"href",
"include",
"Infinite",
"infinity",
"location",
"lockdiscovery",
"lockentry",
"lockinfo",
"lockroot",
"lockscope",
"locktoken",
"lock-token-matches-request-uri",
"lock-token-submitted",
"locktype",
"multistatus",
"no-conflicting-lock",
"no-external-entities",
"owner",
"preserved-live-properties",
"prop",
"propertyupdate",
"propfind",
"propfind-finite-depth",
"propname",
"propstat",
"remove",
"resourcetype",
"response",
"responsedescription",
"set",
"shared",
"status",
"supportedlock",
"text/html",
"timeout",
"write",
];
#[derive(Arbitrary)]
enum Token {
Known(usize),
//Unknown(String),
}
impl Token {
fn serialize(&self) -> String {
match self {
Self::Known(i) => tokens[i % tokens.len()].to_string(),
//Self::Unknown(v) => v.to_string(),
}
}
}
#[derive(Arbitrary)]
struct Tag {
//prefix: Option<Token>,
name: Token,
attr: Option<(Token, Token)>,
}
impl Tag {
fn start(&self) -> String {
let mut acc = String::new();
/*if let Some(p) = &self.prefix {
acc.push_str(p.serialize().as_str());
acc.push_str(":");
}*/
acc.push_str("D:");
acc.push_str(self.name.serialize().as_str());
if let Some((k, v)) = &self.attr {
acc.push_str(" ");
acc.push_str(k.serialize().as_str());
acc.push_str("=\"");
acc.push_str(v.serialize().as_str());
acc.push_str("\"");
}
acc
}
fn end(&self) -> String {
let mut acc = String::new();
acc.push_str("D:");
acc.push_str(self.name.serialize().as_str());
acc
}
}
#[derive(Arbitrary)]
enum XmlNode {
//@FIXME: build RFC3339 and RFC822 Dates with chrono based on timestamps
//@FIXME: add small numbers
//@FIXME: add http status code
Node(Tag, Vec<Self>),
Number(u64),
Text(Token),
}
impl std::fmt::Debug for XmlNode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.serialize())
}
}
impl XmlNode {
fn serialize(&self) -> String {
match self {
Self::Node(tag, children) => {
let stag = tag.start();
match children.is_empty() {
true => format!("<{}/>", stag),
false => format!(
"<{}>{}</{}>",
stag,
children.iter().map(|v| v.serialize()).collect::<String>(),
tag.end()
),
}
}
Self::Number(v) => format!("{}", v),
Self::Text(v) => v.serialize(),
}
}
}
async fn serialize(elem: &impl xml::QWrite) -> Vec<u8> {
let mut buffer = Vec::new();
let mut tokio_buffer = tokio::io::BufWriter::new(&mut buffer);
let q = quick_xml::writer::Writer::new_with_indent(&mut tokio_buffer, b' ', 4);
let ns_to_apply = vec![("xmlns:D".into(), "DAV:".into())];
let mut writer = xml::Writer { q, ns_to_apply };
elem.qwrite(&mut writer).await.expect("xml serialization");
tokio_buffer.flush().await.expect("tokio buffer flush");
return buffer;
}
type Object = types::Multistatus<realization::Core, types::PropValue<realization::Core>>;
fuzz_target!(|nodes: XmlNode| {
let gen = format!(
"<D:multistatus xmlns:D=\"DAV:\">{}<D:/multistatus>",
nodes.serialize()
);
//println!("--------\n{}", gen);
let data = gen.as_bytes();
let rt = Runtime::new().expect("tokio runtime initialization");
rt.block_on(async {
// 1. Setup fuzzing by finding an input that seems correct, do not crash yet then.
let mut rdr = match xml::Reader::new(NsReader::from_reader(data)).await {
Err(_) => return,
Ok(r) => r,
};
let reference = match rdr.find::<Object>().await {
Err(_) => return,
Ok(m) => m,
};
// 2. Re-serialize the input
let my_serialization = serialize(&reference).await;
// 3. De-serialize my serialization
let mut rdr2 = xml::Reader::new(NsReader::from_reader(my_serialization.as_slice()))
.await
.expect("XML Reader init");
let comparison = rdr2.find::<Object>().await.expect("Deserialize again");
// 4. Both the first decoding and last decoding must be identical
assert_eq!(reference, comparison);
})
});

View file

@ -0,0 +1,84 @@
use super::acltypes::*;
use super::error::ParsingError;
use super::types as dav;
use super::xml::{IRead, QRead, Reader, DAV_URN};
impl QRead<Property> for Property {
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
if xml.maybe_open_start(DAV_URN, "owner").await?.is_some() {
let href = xml.find().await?;
xml.close().await?;
return Ok(Self::Owner(href));
}
if xml
.maybe_open_start(DAV_URN, "current-user-principal")
.await?
.is_some()
{
let user = xml.find().await?;
xml.close().await?;
return Ok(Self::CurrentUserPrincipal(user));
}
if xml
.maybe_open_start(DAV_URN, "current-user-privilege-set")
.await?
.is_some()
{
xml.close().await?;
return Ok(Self::CurrentUserPrivilegeSet(vec![]));
}
Err(ParsingError::Recoverable)
}
}
impl QRead<PropertyRequest> for PropertyRequest {
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
if xml.maybe_open(DAV_URN, "owner").await?.is_some() {
xml.close().await?;
return Ok(Self::Owner);
}
if xml
.maybe_open(DAV_URN, "current-user-principal")
.await?
.is_some()
{
xml.close().await?;
return Ok(Self::CurrentUserPrincipal);
}
if xml
.maybe_open(DAV_URN, "current-user-privilege-set")
.await?
.is_some()
{
xml.close().await?;
return Ok(Self::CurrentUserPrivilegeSet);
}
Err(ParsingError::Recoverable)
}
}
impl QRead<ResourceType> for ResourceType {
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
if xml.maybe_open(DAV_URN, "principal").await?.is_some() {
xml.close().await?;
return Ok(Self::Principal);
}
Err(ParsingError::Recoverable)
}
}
// -----
impl QRead<User> for User {
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
if xml.maybe_open(DAV_URN, "unauthenticated").await?.is_some() {
xml.close().await?;
return Ok(Self::Unauthenticated);
}
dav::Href::qread(xml).await.map(Self::Authenticated)
}
}

View file

@ -0,0 +1,71 @@
use quick_xml::events::Event;
use quick_xml::Error as QError;
use super::acltypes::*;
use super::error::ParsingError;
use super::xml::{IWrite, QWrite, Writer};
impl QWrite for Property {
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
match self {
Self::Owner(href) => {
let start = xml.create_dav_element("owner");
let end = start.to_end();
xml.q.write_event_async(Event::Start(start.clone())).await?;
href.qwrite(xml).await?;
xml.q.write_event_async(Event::End(end)).await
}
Self::CurrentUserPrincipal(user) => {
let start = xml.create_dav_element("current-user-principal");
let end = start.to_end();
xml.q.write_event_async(Event::Start(start.clone())).await?;
user.qwrite(xml).await?;
xml.q.write_event_async(Event::End(end)).await
}
Self::CurrentUserPrivilegeSet(_) => {
let empty_tag = xml.create_dav_element("current-user-privilege-set");
xml.q.write_event_async(Event::Empty(empty_tag)).await
}
}
}
}
impl QWrite for PropertyRequest {
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
let mut atom = async |c| {
let empty_tag = xml.create_dav_element(c);
xml.q.write_event_async(Event::Empty(empty_tag)).await
};
match self {
Self::Owner => atom("owner").await,
Self::CurrentUserPrincipal => atom("current-user-principal").await,
Self::CurrentUserPrivilegeSet => atom("current-user-privilege-set").await,
}
}
}
impl QWrite for ResourceType {
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
match self {
Self::Principal => {
let empty_tag = xml.create_dav_element("principal");
xml.q.write_event_async(Event::Empty(empty_tag)).await
}
}
}
}
// -----
impl QWrite for User {
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
match self {
Self::Unauthenticated => {
let tag = xml.create_dav_element("unauthenticated");
xml.q.write_event_async(Event::Empty(tag)).await
}
Self::Authenticated(href) => href.qwrite(xml).await,
}
}
}

38
aero-dav/src/acltypes.rs Normal file
View file

@ -0,0 +1,38 @@
use super::types as dav;
//RFC covered: RFC3744 (ACL core) + RFC5397 (ACL Current Principal Extension)
//@FIXME required for a full CalDAV implementation
// See section 6. of the CalDAV RFC
// It seems mainly required for free-busy that I will not implement now.
// It can also be used for discovering main calendar, not sure it is used.
// Note: it is used by Thunderbird
#[derive(Debug, PartialEq, Clone)]
pub enum PropertyRequest {
Owner,
CurrentUserPrincipal,
CurrentUserPrivilegeSet,
}
#[derive(Debug, PartialEq, Clone)]
pub enum Property {
Owner(dav::Href),
CurrentUserPrincipal(User),
CurrentUserPrivilegeSet(Vec<Privilege>),
}
#[derive(Debug, PartialEq, Clone)]
pub enum ResourceType {
Principal,
}
/// Not implemented, it's a placeholder
#[derive(Debug, PartialEq, Clone)]
pub struct Privilege(());
#[derive(Debug, PartialEq, Clone)]
pub enum User {
Unauthenticated,
Authenticated(dav::Href),
}

1421
aero-dav/src/caldecoder.rs Normal file

File diff suppressed because it is too large Load diff

1036
aero-dav/src/calencoder.rs Normal file

File diff suppressed because it is too large Load diff

1500
aero-dav/src/caltypes.rs Normal file

File diff suppressed because it is too large Load diff

1152
aero-dav/src/decoder.rs Normal file

File diff suppressed because it is too large Load diff

1262
aero-dav/src/encoder.rs Normal file

File diff suppressed because it is too large Load diff

62
aero-dav/src/error.rs Normal file
View file

@ -0,0 +1,62 @@
use quick_xml::events::attributes::AttrError;
#[derive(Debug)]
pub enum ParsingError {
Recoverable,
MissingChild,
MissingAttribute,
NamespacePrefixAlreadyUsed,
WrongToken,
TagNotFound,
InvalidValue,
Utf8Error(std::str::Utf8Error),
QuickXml(quick_xml::Error),
Chrono(chrono::format::ParseError),
Int(std::num::ParseIntError),
Eof,
}
impl std::fmt::Display for ParsingError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Recoverable => write!(f, "Recoverable"),
Self::MissingChild => write!(f, "Missing child"),
Self::MissingAttribute => write!(f, "Missing attribute"),
Self::NamespacePrefixAlreadyUsed => write!(f, "Namespace prefix already used"),
Self::WrongToken => write!(f, "Wrong token"),
Self::TagNotFound => write!(f, "Tag not found"),
Self::InvalidValue => write!(f, "Invalid value"),
Self::Utf8Error(_) => write!(f, "Utf8 Error"),
Self::QuickXml(_) => write!(f, "Quick XML error"),
Self::Chrono(_) => write!(f, "Chrono error"),
Self::Int(_) => write!(f, "Number parsing error"),
Self::Eof => write!(f, "Found EOF while expecting data"),
}
}
}
impl std::error::Error for ParsingError {}
impl From<AttrError> for ParsingError {
fn from(value: AttrError) -> Self {
Self::QuickXml(value.into())
}
}
impl From<quick_xml::Error> for ParsingError {
fn from(value: quick_xml::Error) -> Self {
Self::QuickXml(value)
}
}
impl From<std::str::Utf8Error> for ParsingError {
fn from(value: std::str::Utf8Error) -> Self {
Self::Utf8Error(value)
}
}
impl From<chrono::format::ParseError> for ParsingError {
fn from(value: chrono::format::ParseError) -> Self {
Self::Chrono(value)
}
}
impl From<std::num::ParseIntError> for ParsingError {
fn from(value: std::num::ParseIntError) -> Self {
Self::Int(value)
}
}

35
aero-dav/src/lib.rs Normal file
View file

@ -0,0 +1,35 @@
#![feature(type_alias_impl_trait)]
#![feature(async_closure)]
#![feature(trait_alias)]
// utils
pub mod error;
pub mod xml;
// webdav
pub mod decoder;
pub mod encoder;
pub mod types;
// calendar
pub mod caldecoder;
pub mod calencoder;
pub mod caltypes;
// acl (partial)
pub mod acldecoder;
pub mod aclencoder;
pub mod acltypes;
// versioning (partial)
pub mod versioningdecoder;
pub mod versioningencoder;
pub mod versioningtypes;
// sync
pub mod syncdecoder;
pub mod syncencoder;
pub mod synctypes;
// final type
pub mod realization;

260
aero-dav/src/realization.rs Normal file
View file

@ -0,0 +1,260 @@
use super::acltypes as acl;
use super::caltypes as cal;
use super::error;
use super::synctypes as sync;
use super::types as dav;
use super::versioningtypes as vers;
use super::xml;
#[derive(Debug, PartialEq, Clone)]
pub struct Disabled(());
impl xml::QRead<Disabled> for Disabled {
async fn qread(_xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> {
Err(error::ParsingError::Recoverable)
}
}
impl xml::QWrite for Disabled {
async fn qwrite(
&self,
_xml: &mut xml::Writer<impl xml::IWrite>,
) -> Result<(), quick_xml::Error> {
unreachable!()
}
}
/// The base WebDAV
///
/// Any extension is disabled through an object we can't build
/// due to a private inner element.
#[derive(Debug, PartialEq, Clone)]
pub struct Core {}
impl dav::Extension for Core {
type Error = Disabled;
type Property = Disabled;
type PropertyRequest = Disabled;
type ResourceType = Disabled;
type ReportType = Disabled;
type ReportTypeName = Disabled;
type Multistatus = Disabled;
}
// WebDAV with the base Calendar implementation (RFC4791)
#[derive(Debug, PartialEq, Clone)]
pub struct Calendar {}
impl dav::Extension for Calendar {
type Error = cal::Violation;
type Property = cal::Property;
type PropertyRequest = cal::PropertyRequest;
type ResourceType = cal::ResourceType;
type ReportType = cal::ReportType<Calendar>;
type ReportTypeName = cal::ReportTypeName;
type Multistatus = Disabled;
}
// ACL
#[derive(Debug, PartialEq, Clone)]
pub struct Acl {}
impl dav::Extension for Acl {
type Error = Disabled;
type Property = acl::Property;
type PropertyRequest = acl::PropertyRequest;
type ResourceType = acl::ResourceType;
type ReportType = Disabled;
type ReportTypeName = Disabled;
type Multistatus = Disabled;
}
// All merged
#[derive(Debug, PartialEq, Clone)]
pub struct All {}
impl dav::Extension for All {
type Error = cal::Violation;
type Property = Property<All>;
type PropertyRequest = PropertyRequest;
type ResourceType = ResourceType;
type ReportType = ReportType<All>;
type ReportTypeName = ReportTypeName;
type Multistatus = Multistatus;
}
#[derive(Debug, PartialEq, Clone)]
pub enum Property<E: dav::Extension> {
Cal(cal::Property),
Acl(acl::Property),
Sync(sync::Property),
Vers(vers::Property<E>),
}
impl<E: dav::Extension> xml::QRead<Property<E>> for Property<E> {
async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> {
match cal::Property::qread(xml).await {
Err(error::ParsingError::Recoverable) => (),
otherwise => return otherwise.map(Property::<E>::Cal),
}
match acl::Property::qread(xml).await {
Err(error::ParsingError::Recoverable) => (),
otherwise => return otherwise.map(Property::Acl),
}
match sync::Property::qread(xml).await {
Err(error::ParsingError::Recoverable) => (),
otherwise => return otherwise.map(Property::Sync),
}
vers::Property::qread(xml).await.map(Property::Vers)
}
}
impl<E: dav::Extension> xml::QWrite for Property<E> {
async fn qwrite(
&self,
xml: &mut xml::Writer<impl xml::IWrite>,
) -> Result<(), quick_xml::Error> {
match self {
Self::Cal(c) => c.qwrite(xml).await,
Self::Acl(a) => a.qwrite(xml).await,
Self::Sync(s) => s.qwrite(xml).await,
Self::Vers(v) => v.qwrite(xml).await,
}
}
}
#[derive(Debug, PartialEq, Clone)]
pub enum PropertyRequest {
Cal(cal::PropertyRequest),
Acl(acl::PropertyRequest),
Sync(sync::PropertyRequest),
Vers(vers::PropertyRequest),
}
impl xml::QRead<PropertyRequest> for PropertyRequest {
async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> {
match cal::PropertyRequest::qread(xml).await {
Err(error::ParsingError::Recoverable) => (),
otherwise => return otherwise.map(PropertyRequest::Cal),
}
match acl::PropertyRequest::qread(xml).await {
Err(error::ParsingError::Recoverable) => (),
otherwise => return otherwise.map(PropertyRequest::Acl),
}
match sync::PropertyRequest::qread(xml).await {
Err(error::ParsingError::Recoverable) => (),
otherwise => return otherwise.map(PropertyRequest::Sync),
}
vers::PropertyRequest::qread(xml)
.await
.map(PropertyRequest::Vers)
}
}
impl xml::QWrite for PropertyRequest {
async fn qwrite(
&self,
xml: &mut xml::Writer<impl xml::IWrite>,
) -> Result<(), quick_xml::Error> {
match self {
Self::Cal(c) => c.qwrite(xml).await,
Self::Acl(a) => a.qwrite(xml).await,
Self::Sync(s) => s.qwrite(xml).await,
Self::Vers(v) => v.qwrite(xml).await,
}
}
}
#[derive(Debug, PartialEq, Clone)]
pub enum ResourceType {
Cal(cal::ResourceType),
Acl(acl::ResourceType),
}
impl xml::QRead<ResourceType> for ResourceType {
async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> {
match cal::ResourceType::qread(xml).await {
Err(error::ParsingError::Recoverable) => (),
otherwise => return otherwise.map(ResourceType::Cal),
}
acl::ResourceType::qread(xml).await.map(ResourceType::Acl)
}
}
impl xml::QWrite for ResourceType {
async fn qwrite(
&self,
xml: &mut xml::Writer<impl xml::IWrite>,
) -> Result<(), quick_xml::Error> {
match self {
Self::Cal(c) => c.qwrite(xml).await,
Self::Acl(a) => a.qwrite(xml).await,
}
}
}
#[derive(Debug, PartialEq, Clone)]
pub enum ReportType<E: dav::Extension> {
Cal(cal::ReportType<E>),
Sync(sync::SyncCollection<E>),
}
impl<E: dav::Extension> xml::QRead<ReportType<E>> for ReportType<E> {
async fn qread(
xml: &mut xml::Reader<impl xml::IRead>,
) -> Result<ReportType<E>, error::ParsingError> {
match cal::ReportType::qread(xml).await {
Err(error::ParsingError::Recoverable) => (),
otherwise => return otherwise.map(ReportType::Cal),
}
sync::SyncCollection::qread(xml).await.map(ReportType::Sync)
}
}
impl<E: dav::Extension> xml::QWrite for ReportType<E> {
async fn qwrite(
&self,
xml: &mut xml::Writer<impl xml::IWrite>,
) -> Result<(), quick_xml::Error> {
match self {
Self::Cal(c) => c.qwrite(xml).await,
Self::Sync(s) => s.qwrite(xml).await,
}
}
}
#[derive(Debug, PartialEq, Clone)]
pub enum ReportTypeName {
Cal(cal::ReportTypeName),
Sync(sync::ReportTypeName),
}
impl xml::QRead<ReportTypeName> for ReportTypeName {
async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> {
match cal::ReportTypeName::qread(xml).await {
Err(error::ParsingError::Recoverable) => (),
otherwise => return otherwise.map(ReportTypeName::Cal),
}
sync::ReportTypeName::qread(xml)
.await
.map(ReportTypeName::Sync)
}
}
impl xml::QWrite for ReportTypeName {
async fn qwrite(
&self,
xml: &mut xml::Writer<impl xml::IWrite>,
) -> Result<(), quick_xml::Error> {
match self {
Self::Cal(c) => c.qwrite(xml).await,
Self::Sync(s) => s.qwrite(xml).await,
}
}
}
#[derive(Debug, PartialEq, Clone)]
pub enum Multistatus {
Sync(sync::Multistatus),
}
impl xml::QWrite for Multistatus {
async fn qwrite(
&self,
xml: &mut xml::Writer<impl xml::IWrite>,
) -> Result<(), quick_xml::Error> {
match self {
Self::Sync(s) => s.qwrite(xml).await,
}
}
}
impl xml::QRead<Multistatus> for Multistatus {
async fn qread(xml: &mut xml::Reader<impl xml::IRead>) -> Result<Self, error::ParsingError> {
sync::Multistatus::qread(xml).await.map(Self::Sync)
}
}

248
aero-dav/src/syncdecoder.rs Normal file
View file

@ -0,0 +1,248 @@
use quick_xml::events::Event;
use super::error::ParsingError;
use super::synctypes::*;
use super::types as dav;
use super::xml::{IRead, QRead, Reader, DAV_URN};
impl QRead<PropertyRequest> for PropertyRequest {
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
if xml.maybe_open(DAV_URN, "sync-token").await?.is_some() {
xml.close().await?;
return Ok(Self::SyncToken);
}
return Err(ParsingError::Recoverable);
}
}
impl QRead<Property> for Property {
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
let mut dirty = false;
let mut m_cdr = None;
xml.maybe_read(&mut m_cdr, &mut dirty).await?;
m_cdr.ok_or(ParsingError::Recoverable).map(Self::SyncToken)
}
}
impl QRead<ReportTypeName> for ReportTypeName {
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
if xml.maybe_open(DAV_URN, "sync-collection").await?.is_some() {
xml.close().await?;
return Ok(Self::SyncCollection);
}
Err(ParsingError::Recoverable)
}
}
impl QRead<Multistatus> for Multistatus {
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
SyncToken::qread(xml)
.await
.map(|sync_token| Multistatus { sync_token })
}
}
impl<E: dav::Extension> QRead<SyncCollection<E>> for SyncCollection<E> {
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
xml.open(DAV_URN, "sync-collection").await?;
let (mut sync_token, mut sync_level, mut limit, mut prop) = (None, None, None, None);
loop {
let mut dirty = false;
xml.maybe_read(&mut sync_token, &mut dirty).await?;
xml.maybe_read(&mut sync_level, &mut dirty).await?;
xml.maybe_read(&mut limit, &mut dirty).await?;
xml.maybe_read(&mut prop, &mut dirty).await?;
if !dirty {
match xml.peek() {
Event::End(_) => break,
_ => xml.skip().await?,
};
}
}
xml.close().await?;
match (sync_token, sync_level, prop) {
(Some(sync_token), Some(sync_level), Some(prop)) => Ok(SyncCollection {
sync_token,
sync_level,
limit,
prop,
}),
_ => Err(ParsingError::MissingChild),
}
}
}
impl QRead<SyncTokenRequest> for SyncTokenRequest {
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
xml.open(DAV_URN, "sync-token").await?;
let token = match xml.tag_string().await {
Ok(v) => SyncTokenRequest::IncrementalSync(v),
Err(ParsingError::Recoverable) => SyncTokenRequest::InitialSync,
Err(e) => return Err(e),
};
xml.close().await?;
Ok(token)
}
}
impl QRead<SyncToken> for SyncToken {
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
xml.open(DAV_URN, "sync-token").await?;
let token = xml.tag_string().await?;
xml.close().await?;
Ok(SyncToken(token))
}
}
impl QRead<SyncLevel> for SyncLevel {
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
xml.open(DAV_URN, "sync-level").await?;
let lvl = match xml.tag_string().await?.to_lowercase().as_str() {
"1" => SyncLevel::One,
"infinite" => SyncLevel::Infinite,
_ => return Err(ParsingError::InvalidValue),
};
xml.close().await?;
Ok(lvl)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::realization::{self, All};
use crate::types as dav;
use crate::versioningtypes as vers;
use crate::xml::Node;
async fn deserialize<T: Node<T>>(src: &str) -> T {
let mut rdr = Reader::new(quick_xml::NsReader::from_reader(src.as_bytes()))
.await
.unwrap();
rdr.find().await.unwrap()
}
#[tokio::test]
async fn sync_level() {
{
let expected = SyncLevel::One;
let src = r#"<D:sync-level xmlns:D="DAV:">1</D:sync-level>"#;
let got = deserialize::<SyncLevel>(src).await;
assert_eq!(got, expected);
}
{
let expected = SyncLevel::Infinite;
let src = r#"<D:sync-level xmlns:D="DAV:">infinite</D:sync-level>"#;
let got = deserialize::<SyncLevel>(src).await;
assert_eq!(got, expected);
}
}
#[tokio::test]
async fn sync_token_request() {
{
let expected = SyncTokenRequest::InitialSync;
let src = r#"<D:sync-token xmlns:D="DAV:"/>"#;
let got = deserialize::<SyncTokenRequest>(src).await;
assert_eq!(got, expected);
}
{
let expected =
SyncTokenRequest::IncrementalSync("http://example.com/ns/sync/1232".into());
let src =
r#"<D:sync-token xmlns:D="DAV:">http://example.com/ns/sync/1232</D:sync-token>"#;
let got = deserialize::<SyncTokenRequest>(src).await;
assert_eq!(got, expected);
}
}
#[tokio::test]
async fn sync_token() {
let expected = SyncToken("http://example.com/ns/sync/1232".into());
let src = r#"<D:sync-token xmlns:D="DAV:">http://example.com/ns/sync/1232</D:sync-token>"#;
let got = deserialize::<SyncToken>(src).await;
assert_eq!(got, expected);
}
#[tokio::test]
async fn sync_collection() {
{
let expected = SyncCollection::<All> {
sync_token: SyncTokenRequest::IncrementalSync(
"http://example.com/ns/sync/1232".into(),
),
sync_level: SyncLevel::One,
limit: Some(vers::Limit(vers::NResults(100))),
prop: dav::PropName(vec![dav::PropertyRequest::GetEtag]),
};
let src = r#"<D:sync-collection xmlns:D="DAV:">
<D:sync-token>http://example.com/ns/sync/1232</D:sync-token>
<D:sync-level>1</D:sync-level>
<D:limit>
<D:nresults>100</D:nresults>
</D:limit>
<D:prop>
<D:getetag/>
</D:prop>
</D:sync-collection>"#;
let got = deserialize::<SyncCollection<All>>(src).await;
assert_eq!(got, expected);
}
{
let expected = SyncCollection::<All> {
sync_token: SyncTokenRequest::InitialSync,
sync_level: SyncLevel::Infinite,
limit: None,
prop: dav::PropName(vec![dav::PropertyRequest::GetEtag]),
};
let src = r#"<D:sync-collection xmlns:D="DAV:">
<D:sync-token/>
<D:sync-level>infinite</D:sync-level>
<D:prop>
<D:getetag/>
</D:prop>
</D:sync-collection>"#;
let got = deserialize::<SyncCollection<All>>(src).await;
assert_eq!(got, expected);
}
}
#[tokio::test]
async fn prop_req() {
let expected = dav::PropName::<All>(vec![dav::PropertyRequest::Extension(
realization::PropertyRequest::Sync(PropertyRequest::SyncToken),
)]);
let src = r#"<prop xmlns="DAV:"><sync-token/></prop>"#;
let got = deserialize::<dav::PropName<All>>(src).await;
assert_eq!(got, expected);
}
#[tokio::test]
async fn prop_val() {
let expected = dav::PropValue::<All>(vec![
dav::Property::Extension(realization::Property::Sync(Property::SyncToken(SyncToken(
"http://example.com/ns/sync/1232".into(),
)))),
dav::Property::Extension(realization::Property::Vers(
vers::Property::SupportedReportSet(vec![vers::SupportedReport(
vers::ReportName::Extension(realization::ReportTypeName::Sync(
ReportTypeName::SyncCollection,
)),
)]),
)),
]);
let src = r#"<prop xmlns="DAV:">
<sync-token>http://example.com/ns/sync/1232</sync-token>
<supported-report-set>
<supported-report>
<report><sync-collection/></report>
</supported-report>
</supported-report-set>
</prop>"#;
let got = deserialize::<dav::PropValue<All>>(src).await;
assert_eq!(got, expected);
}
}

227
aero-dav/src/syncencoder.rs Normal file
View file

@ -0,0 +1,227 @@
use quick_xml::events::{BytesText, Event};
use quick_xml::Error as QError;
use super::synctypes::*;
use super::types::Extension;
use super::xml::{IWrite, QWrite, Writer};
impl QWrite for Property {
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
match self {
Self::SyncToken(token) => token.qwrite(xml).await,
}
}
}
impl QWrite for PropertyRequest {
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
match self {
Self::SyncToken => {
let start = xml.create_dav_element("sync-token");
xml.q.write_event_async(Event::Empty(start)).await
}
}
}
}
impl QWrite for ReportTypeName {
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
match self {
Self::SyncCollection => {
let start = xml.create_dav_element("sync-collection");
xml.q.write_event_async(Event::Empty(start)).await
}
}
}
}
impl QWrite for Multistatus {
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
self.sync_token.qwrite(xml).await
}
}
impl<E: Extension> QWrite for SyncCollection<E> {
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
let start = xml.create_dav_element("sync-collection");
let end = start.to_end();
xml.q.write_event_async(Event::Start(start.clone())).await?;
self.sync_token.qwrite(xml).await?;
self.sync_level.qwrite(xml).await?;
if let Some(limit) = &self.limit {
limit.qwrite(xml).await?;
}
self.prop.qwrite(xml).await?;
xml.q.write_event_async(Event::End(end)).await
}
}
impl QWrite for SyncTokenRequest {
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
let start = xml.create_dav_element("sync-token");
match self {
Self::InitialSync => xml.q.write_event_async(Event::Empty(start)).await,
Self::IncrementalSync(uri) => {
let end = start.to_end();
xml.q.write_event_async(Event::Start(start.clone())).await?;
xml.q
.write_event_async(Event::Text(BytesText::new(uri.as_str())))
.await?;
xml.q.write_event_async(Event::End(end)).await
}
}
}
}
impl QWrite for SyncToken {
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
let start = xml.create_dav_element("sync-token");
let end = start.to_end();
xml.q.write_event_async(Event::Start(start.clone())).await?;
xml.q
.write_event_async(Event::Text(BytesText::new(self.0.as_str())))
.await?;
xml.q.write_event_async(Event::End(end)).await
}
}
impl QWrite for SyncLevel {
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
let start = xml.create_dav_element("sync-level");
let end = start.to_end();
let text = match self {
Self::One => "1",
Self::Infinite => "infinite",
};
xml.q.write_event_async(Event::Start(start.clone())).await?;
xml.q
.write_event_async(Event::Text(BytesText::new(text)))
.await?;
xml.q.write_event_async(Event::End(end)).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::realization::{self, All};
use crate::types as dav;
use crate::versioningtypes as vers;
use crate::xml::Node;
use crate::xml::Reader;
use tokio::io::AsyncWriteExt;
async fn serialize_deserialize<T: Node<T>>(src: &T) {
let mut buffer = Vec::new();
let mut tokio_buffer = tokio::io::BufWriter::new(&mut buffer);
let q = quick_xml::writer::Writer::new_with_indent(&mut tokio_buffer, b' ', 4);
let ns_to_apply = vec![
("xmlns:D".into(), "DAV:".into()),
("xmlns:C".into(), "urn:ietf:params:xml:ns:caldav".into()),
];
let mut writer = Writer { q, ns_to_apply };
src.qwrite(&mut writer).await.expect("xml serialization");
tokio_buffer.flush().await.expect("tokio buffer flush");
let got = std::str::from_utf8(buffer.as_slice()).unwrap();
// deserialize
let mut rdr = Reader::new(quick_xml::NsReader::from_reader(got.as_bytes()))
.await
.unwrap();
let res = rdr.find().await.unwrap();
// check
assert_eq!(src, &res);
}
#[tokio::test]
async fn sync_level() {
serialize_deserialize(&SyncLevel::One).await;
serialize_deserialize(&SyncLevel::Infinite).await;
}
#[tokio::test]
async fn sync_token_request() {
serialize_deserialize(&SyncTokenRequest::InitialSync).await;
serialize_deserialize(&SyncTokenRequest::IncrementalSync(
"http://example.com/ns/sync/1232".into(),
))
.await;
}
#[tokio::test]
async fn sync_token() {
serialize_deserialize(&SyncToken("http://example.com/ns/sync/1232".into())).await;
}
#[tokio::test]
async fn sync_collection() {
serialize_deserialize(&SyncCollection::<All> {
sync_token: SyncTokenRequest::IncrementalSync("http://example.com/ns/sync/1232".into()),
sync_level: SyncLevel::One,
limit: Some(vers::Limit(vers::NResults(100))),
prop: dav::PropName(vec![dav::PropertyRequest::GetEtag]),
})
.await;
serialize_deserialize(&SyncCollection::<All> {
sync_token: SyncTokenRequest::InitialSync,
sync_level: SyncLevel::Infinite,
limit: None,
prop: dav::PropName(vec![dav::PropertyRequest::GetEtag]),
})
.await;
}
#[tokio::test]
async fn prop_req() {
serialize_deserialize(&dav::PropName::<All>(vec![
dav::PropertyRequest::Extension(realization::PropertyRequest::Sync(
PropertyRequest::SyncToken,
)),
]))
.await;
}
#[tokio::test]
async fn prop_val() {
serialize_deserialize(&dav::PropValue::<All>(vec![
dav::Property::Extension(realization::Property::Sync(Property::SyncToken(SyncToken(
"http://example.com/ns/sync/1232".into(),
)))),
dav::Property::Extension(realization::Property::Vers(
vers::Property::SupportedReportSet(vec![vers::SupportedReport(
vers::ReportName::Extension(realization::ReportTypeName::Sync(
ReportTypeName::SyncCollection,
)),
)]),
)),
]))
.await;
}
#[tokio::test]
async fn multistatus_ext() {
serialize_deserialize(&dav::Multistatus::<All> {
responses: vec![dav::Response {
status_or_propstat: dav::StatusOrPropstat::Status(
vec![dav::Href("/".into())],
dav::Status(http::status::StatusCode::OK),
),
error: None,
location: None,
responsedescription: None,
}],
responsedescription: None,
extension: Some(realization::Multistatus::Sync(Multistatus {
sync_token: SyncToken("http://example.com/ns/sync/1232".into()),
})),
})
.await;
}
}

86
aero-dav/src/synctypes.rs Normal file
View file

@ -0,0 +1,86 @@
use super::types as dav;
use super::versioningtypes as vers;
// RFC 6578
// https://datatracker.ietf.org/doc/html/rfc6578
#[derive(Debug, PartialEq, Clone)]
pub enum PropertyRequest {
SyncToken,
}
#[derive(Debug, PartialEq, Clone)]
pub enum Property {
SyncToken(SyncToken),
}
#[derive(Debug, PartialEq, Clone)]
pub enum ReportTypeName {
SyncCollection,
}
#[derive(Debug, PartialEq, Clone)]
pub struct Multistatus {
pub sync_token: SyncToken,
}
//@FIXME add SyncToken to Multistatus
/// Name: sync-collection
///
/// Namespace: DAV:
///
/// Purpose: WebDAV report used to synchronize data between client and
/// server.
///
/// Description: See Section 3.
///
/// <!ELEMENT sync-collection (sync-token, sync-level, limit?, prop)>
///
/// <!-- DAV:limit defined in RFC 5323, Section 5.17 -->
/// <!-- DAV:prop defined in RFC 4918, Section 14.18 -->
#[derive(Debug, PartialEq, Clone)]
pub struct SyncCollection<E: dav::Extension> {
pub sync_token: SyncTokenRequest,
pub sync_level: SyncLevel,
pub limit: Option<vers::Limit>,
pub prop: dav::PropName<E>,
}
/// Name: sync-token
///
/// Namespace: DAV:
///
/// Purpose: The synchronization token provided by the server and
/// returned by the client.
///
/// Description: See Section 3.
///
/// <!ELEMENT sync-token CDATA>
///
/// <!-- Text MUST be a URI -->
/// Used by multistatus
#[derive(Debug, PartialEq, Clone)]
pub struct SyncToken(pub String);
/// Used by propfind and report sync-collection
#[derive(Debug, PartialEq, Clone)]
pub enum SyncTokenRequest {
InitialSync,
IncrementalSync(String),
}
/// Name: sync-level
///
/// Namespace: DAV:
///
/// Purpose: Indicates the "scope" of the synchronization report
/// request.
///
/// Description: See Section 3.3.
#[derive(Debug, PartialEq, Clone)]
pub enum SyncLevel {
One,
Infinite,
}

964
aero-dav/src/types.rs Normal file
View file

@ -0,0 +1,964 @@
#![allow(dead_code)]
use std::fmt::Debug;
use super::xml;
use chrono::{DateTime, FixedOffset};
/// It's how we implement a DAV extension
/// (That's the dark magic part...)
pub trait Extension: std::fmt::Debug + PartialEq + Clone {
type Error: xml::Node<Self::Error>;
type Property: xml::Node<Self::Property>;
type PropertyRequest: xml::Node<Self::PropertyRequest>;
type ResourceType: xml::Node<Self::ResourceType>;
type ReportType: xml::Node<Self::ReportType>;
type ReportTypeName: xml::Node<Self::ReportTypeName>;
type Multistatus: xml::Node<Self::Multistatus>;
}
/// 14.1. activelock XML Element
///
/// Name: activelock
///
/// Purpose: Describes a lock on a resource.
/// <!ELEMENT activelock (lockscope, locktype, depth, owner?, timeout?,
/// locktoken?, lockroot)>
#[derive(Debug, PartialEq, Clone)]
pub struct ActiveLock {
pub lockscope: LockScope,
pub locktype: LockType,
pub depth: Depth,
pub owner: Option<Owner>,
pub timeout: Option<Timeout>,
pub locktoken: Option<LockToken>,
pub lockroot: LockRoot,
}
/// 14.3 collection XML Element
///
/// Name: collection
///
/// Purpose: Identifies the associated resource as a collection. The
/// DAV:resourcetype property of a collection resource MUST contain
/// this element. It is normally empty but extensions may add sub-
/// elements.
///
/// <!ELEMENT collection EMPTY >
#[derive(Debug, PartialEq)]
pub struct Collection {}
/// 14.4 depth XML Element
///
/// Name: depth
///
/// Purpose: Used for representing depth values in XML content (e.g.,
/// in lock information).
///
/// Value: "0" | "1" | "infinity"
///
/// <!ELEMENT depth (#PCDATA) >
#[derive(Debug, PartialEq, Clone)]
pub enum Depth {
Zero,
One,
Infinity,
}
/// 14.5 error XML Element
///
/// Name: error
///
/// Purpose: Error responses, particularly 403 Forbidden and 409
/// Conflict, sometimes need more information to indicate what went
/// wrong. In these cases, servers MAY return an XML response body
/// with a document element of 'error', containing child elements
/// identifying particular condition codes.
///
/// Description: Contains at least one XML element, and MUST NOT
/// contain text or mixed content. Any element that is a child of the
/// 'error' element is considered to be a precondition or
/// postcondition code. Unrecognized elements MUST be ignored.
///
/// <!ELEMENT error ANY >
#[derive(Debug, PartialEq, Clone)]
pub struct Error<E: Extension>(pub Vec<Violation<E>>);
#[derive(Debug, PartialEq, Clone)]
pub enum Violation<E: Extension> {
/// Name: lock-token-matches-request-uri
///
/// Use with: 409 Conflict
///
/// Purpose: (precondition) -- A request may include a Lock-Token header
/// to identify a lock for the UNLOCK method. However, if the
/// Request-URI does not fall within the scope of the lock identified
/// by the token, the server SHOULD use this error. The lock may have
/// a scope that does not include the Request-URI, or the lock could
/// have disappeared, or the token may be invalid.
LockTokenMatchesRequestUri,
/// Name: lock-token-submitted (precondition)
///
/// Use with: 423 Locked
///
/// Purpose: The request could not succeed because a lock token should
/// have been submitted. This element, if present, MUST contain at
/// least one URL of a locked resource that prevented the request. In
/// cases of MOVE, COPY, and DELETE where collection locks are
/// involved, it can be difficult for the client to find out which
/// locked resource made the request fail -- but the server is only
/// responsible for returning one such locked resource. The server
/// MAY return every locked resource that prevented the request from
/// succeeding if it knows them all.
///
/// <!ELEMENT lock-token-submitted (href+) >
LockTokenSubmitted(Vec<Href>),
/// Name: no-conflicting-lock (precondition)
///
/// Use with: Typically 423 Locked
///
/// Purpose: A LOCK request failed due the presence of an already
/// existing conflicting lock. Note that a lock can be in conflict
/// although the resource to which the request was directed is only
/// indirectly locked. In this case, the precondition code can be
/// used to inform the client about the resource that is the root of
/// the conflicting lock, avoiding a separate lookup of the
/// "lockdiscovery" property.
///
/// <!ELEMENT no-conflicting-lock (href)* >
NoConflictingLock(Vec<Href>),
/// Name: no-external-entities
///
/// Use with: 403 Forbidden
///
/// Purpose: (precondition) -- If the server rejects a client request
/// because the request body contains an external entity, the server
/// SHOULD use this error.
NoExternalEntities,
/// Name: preserved-live-properties
///
/// Use with: 409 Conflict
///
/// Purpose: (postcondition) -- The server received an otherwise-valid
/// MOVE or COPY request, but cannot maintain the live properties with
/// the same behavior at the destination. It may be that the server
/// only supports some live properties in some parts of the
/// repository, or simply has an internal error.
PreservedLiveProperties,
/// Name: propfind-finite-depth
///
/// Use with: 403 Forbidden
///
/// Purpose: (precondition) -- This server does not allow infinite-depth
/// PROPFIND requests on collections.
PropfindFiniteDepth,
/// Name: cannot-modify-protected-property
///
/// Use with: 403 Forbidden
///
/// Purpose: (precondition) -- The client attempted to set a protected
/// property in a PROPPATCH (such as DAV:getetag). See also
/// [RFC3253], Section 3.12.
CannotModifyProtectedProperty,
/// Specific errors
Extension(E::Error),
}
/// 14.6. exclusive XML Element
///
/// Name: exclusive
///
/// Purpose: Specifies an exclusive lock.
///
/// <!ELEMENT exclusive EMPTY >
#[derive(Debug, PartialEq)]
pub struct Exclusive {}
/// 14.7. href XML Element
///
/// Name: href
///
/// Purpose: MUST contain a URI or a relative reference.
///
/// Description: There may be limits on the value of 'href' depending
/// on the context of its use. Refer to the specification text where
/// 'href' is used to see what limitations apply in each case.
///
/// Value: Simple-ref
///
/// <!ELEMENT href (#PCDATA)>
#[derive(Debug, PartialEq, Clone)]
pub struct Href(pub String);
/// 14.8. include XML Element
///
/// Name: include
///
/// Purpose: Any child element represents the name of a property to be
/// included in the PROPFIND response. All elements inside an
/// 'include' XML element MUST define properties related to the
/// resource, although possible property names are in no way limited
/// to those property names defined in this document or other
/// standards. This element MUST NOT contain text or mixed content.
///
/// <!ELEMENT include ANY >
#[derive(Debug, PartialEq, Clone)]
pub struct Include<E: Extension>(pub Vec<PropertyRequest<E>>);
/// 14.9. location XML Element
///
/// Name: location
///
/// Purpose: HTTP defines the "Location" header (see [RFC2616], Section
/// 14.30) for use with some status codes (such as 201 and the 300
/// series codes). When these codes are used inside a 'multistatus'
/// element, the 'location' element can be used to provide the
/// accompanying Location header value.
///
/// Description: Contains a single href element with the same value
/// that would be used in a Location header.
///
/// <!ELEMENT location (href)>
#[derive(Debug, PartialEq, Clone)]
pub struct Location(pub Href);
/// 14.10. lockentry XML Element
///
/// Name: lockentry
///
/// Purpose: Defines the types of locks that can be used with the
/// resource.
///
/// <!ELEMENT lockentry (lockscope, locktype) >
#[derive(Debug, PartialEq, Clone)]
pub struct LockEntry {
pub lockscope: LockScope,
pub locktype: LockType,
}
/// 14.11. lockinfo XML Element
///
/// Name: lockinfo
///
/// Purpose: The 'lockinfo' XML element is used with a LOCK method to
/// specify the type of lock the client wishes to have created.
///
/// <!ELEMENT lockinfo (lockscope, locktype, owner?) >
#[derive(Debug, PartialEq, Clone)]
pub struct LockInfo {
pub lockscope: LockScope,
pub locktype: LockType,
pub owner: Option<Owner>,
}
/// 14.12. lockroot XML Element
///
/// Name: lockroot
///
/// Purpose: Contains the root URL of the lock, which is the URL
/// through which the resource was addressed in the LOCK request.
///
/// Description: The href element contains the root of the lock. The
/// server SHOULD include this in all DAV:lockdiscovery property
/// values and the response to LOCK requests.
///
/// <!ELEMENT lockroot (href) >
#[derive(Debug, PartialEq, Clone)]
pub struct LockRoot(pub Href);
/// 14.13. lockscope XML Element
///
/// Name: lockscope
///
/// Purpose: Specifies whether a lock is an exclusive lock, or a shared
/// lock.
/// <!ELEMENT lockscope (exclusive | shared) >
#[derive(Debug, PartialEq, Clone)]
pub enum LockScope {
Exclusive,
Shared,
}
/// 14.14. locktoken XML Element
///
/// Name: locktoken
///
/// Purpose: The lock token associated with a lock.
///
/// Description: The href contains a single lock token URI, which
/// refers to the lock.
///
/// <!ELEMENT locktoken (href) >
#[derive(Debug, PartialEq, Clone)]
pub struct LockToken(pub Href);
/// 14.15. locktype XML Element
///
/// Name: locktype
///
/// Purpose: Specifies the access type of a lock. At present, this
/// specification only defines one lock type, the write lock.
///
/// <!ELEMENT locktype (write) >
#[derive(Debug, PartialEq, Clone)]
pub enum LockType {
/// 14.30. write XML Element
///
/// Name: write
///
/// Purpose: Specifies a write lock.
///
///
/// <!ELEMENT write EMPTY >
Write,
}
/// 14.16. multistatus XML Element
///
/// Name: multistatus
///
/// Purpose: Contains multiple response messages.
///
/// Description: The 'responsedescription' element at the top level is
/// used to provide a general message describing the overarching
/// nature of the response. If this value is available, an
/// application may use it instead of presenting the individual
/// response descriptions contained within the responses.
///
/// <!ELEMENT multistatus (response*, responsedescription?) >
///
/// In WebDAV sync (rfc6578), multistatus is extended:
///
/// <!ELEMENT multistatus (response*, responsedescription?, sync-token?) >
#[derive(Debug, PartialEq, Clone)]
pub struct Multistatus<E: Extension> {
pub responses: Vec<Response<E>>,
pub responsedescription: Option<ResponseDescription>,
pub extension: Option<E::Multistatus>,
}
/// 14.17. owner XML Element
///
/// Name: owner
///
/// Purpose: Holds client-supplied information about the creator of a
/// lock.
///
/// Description: Allows a client to provide information sufficient for
/// either directly contacting a principal (such as a telephone number
/// or Email URI), or for discovering the principal (such as the URL
/// of a homepage) who created a lock. The value provided MUST be
/// treated as a dead property in terms of XML Information Item
/// preservation. The server MUST NOT alter the value unless the
/// owner value provided by the client is empty. For a certain amount
/// of interoperability between different client implementations, if
/// clients have URI-formatted contact information for the lock
/// creator suitable for user display, then clients SHOULD put those
/// URIs in 'href' child elements of the 'owner' element.
///
/// Extensibility: MAY be extended with child elements, mixed content,
/// text content or attributes.
///
/// <!ELEMENT owner ANY >
//@FIXME might need support for an extension
#[derive(Debug, PartialEq, Clone)]
pub enum Owner {
Txt(String),
Href(Href),
Unknown,
}
/// 14.18. prop XML Element
///
/// Name: prop
///
/// Purpose: Contains properties related to a resource.
///
/// Description: A generic container for properties defined on
/// resources. All elements inside a 'prop' XML element MUST define
/// properties related to the resource, although possible property
/// names are in no way limited to those property names defined in
/// this document or other standards. This element MUST NOT contain
/// text or mixed content.
///
/// <!ELEMENT prop ANY >
#[derive(Debug, PartialEq, Clone)]
pub struct PropName<E: Extension>(pub Vec<PropertyRequest<E>>);
#[derive(Debug, PartialEq, Clone)]
pub struct PropValue<E: Extension>(pub Vec<Property<E>>);
#[derive(Debug, PartialEq, Clone)]
pub struct AnyProp<E: Extension>(pub Vec<AnyProperty<E>>);
/// 14.19. propertyupdate XML Element
///
/// Name: propertyupdate
///
/// Purpose: Contains a request to alter the properties on a resource.
///
/// Description: This XML element is a container for the information
/// required to modify the properties on the resource.
///
/// <!ELEMENT propertyupdate (remove | set)+ >
#[derive(Debug, PartialEq, Clone)]
pub struct PropertyUpdate<E: Extension>(pub Vec<PropertyUpdateItem<E>>);
#[derive(Debug, PartialEq, Clone)]
pub enum PropertyUpdateItem<E: Extension> {
Remove(Remove<E>),
Set(Set<E>),
}
/// 14.2 allprop XML Element
///
/// Name: allprop
///
/// Purpose: Specifies that all names and values of dead properties and
/// the live properties defined by this document existing on the
/// resource are to be returned.
///
/// <!ELEMENT allprop EMPTY >
///
/// ---
///
/// 14.21. propname XML Element
///
/// Name: propname
///
/// Purpose: Specifies that only a list of property names on the
/// resource is to be returned.
///
/// <!ELEMENT propname EMPTY >
///
/// ---
///
/// 14.20. propfind XML Element
///
/// Name: propfind
///
/// Purpose: Specifies the properties to be returned from a PROPFIND
/// method. Four special elements are specified for use with
/// 'propfind': 'prop', 'allprop', 'include', and 'propname'. If
/// 'prop' is used inside 'propfind', it MUST NOT contain property
/// values.
///
/// <!ELEMENT propfind ( propname | (allprop, include?) | prop ) >
#[derive(Debug, PartialEq, Clone)]
pub enum PropFind<E: Extension> {
PropName,
AllProp(Option<Include<E>>),
Prop(PropName<E>),
}
/// 14.22 propstat XML Element
///
/// Name: propstat
///
/// Purpose: Groups together a prop and status element that is
/// associated with a particular 'href' element.
///
/// Description: The propstat XML element MUST contain one prop XML
/// element and one status XML element. The contents of the prop XML
/// element MUST only list the names of properties to which the result
/// in the status element applies. The optional precondition/
/// postcondition element and 'responsedescription' text also apply to
/// the properties named in 'prop'.
///
/// <!ELEMENT propstat (prop, status, error?, responsedescription?) >
///
/// ---
///
///
#[derive(Debug, PartialEq, Clone)]
pub struct PropStat<E: Extension> {
pub prop: AnyProp<E>,
pub status: Status,
pub error: Option<Error<E>>,
pub responsedescription: Option<ResponseDescription>,
}
/// 14.23. remove XML Element
///
/// Name: remove
///
/// Purpose: Lists the properties to be removed from a resource.
///
/// Description: Remove instructs that the properties specified in prop
/// should be removed. Specifying the removal of a property that does
/// not exist is not an error. All the XML elements in a 'prop' XML
/// element inside of a 'remove' XML element MUST be empty, as only
/// the names of properties to be removed are required.
///
/// <!ELEMENT remove (prop) >
#[derive(Debug, PartialEq, Clone)]
pub struct Remove<E: Extension>(pub PropName<E>);
/// 14.24. response XML Element
///
/// Name: response
///
/// Purpose: Holds a single response describing the effect of a method
/// on resource and/or its properties.
///
/// Description: The 'href' element contains an HTTP URL pointing to a
/// WebDAV resource when used in the 'response' container. A
/// particular 'href' value MUST NOT appear more than once as the
/// child of a 'response' XML element under a 'multistatus' XML
/// element. This requirement is necessary in order to keep
/// processing costs for a response to linear time. Essentially, this
/// prevents having to search in order to group together all the
/// responses by 'href'. There are, however, no requirements
/// regarding ordering based on 'href' values. The optional
/// precondition/postcondition element and 'responsedescription' text
/// can provide additional information about this resource relative to
/// the request or result.
///
/// <!ELEMENT response (href, ((href*, status)|(propstat+)),
/// error?, responsedescription? , location?) >
///
/// --- rewritten as ---
/// <!ELEMENT response ((href+, status)|(href, propstat+), error?, responsedescription?, location?>
#[derive(Debug, PartialEq, Clone)]
pub enum StatusOrPropstat<E: Extension> {
// One status, multiple hrefs...
Status(Vec<Href>, Status),
// A single href, multiple properties...
PropStat(Href, Vec<PropStat<E>>),
}
#[derive(Debug, PartialEq, Clone)]
pub struct Response<E: Extension> {
pub status_or_propstat: StatusOrPropstat<E>,
pub error: Option<Error<E>>,
pub responsedescription: Option<ResponseDescription>,
pub location: Option<Location>,
}
/// 14.25. responsedescription XML Element
///
/// Name: responsedescription
///
/// Purpose: Contains information about a status response within a
/// Multi-Status.
///
/// Description: Provides information suitable to be presented to a
/// user.
///
/// <!ELEMENT responsedescription (#PCDATA) >
#[derive(Debug, PartialEq, Clone)]
pub struct ResponseDescription(pub String);
/// 14.26. set XML Element
///
/// Name: set
///
/// Purpose: Lists the property values to be set for a resource.
///
/// Description: The 'set' element MUST contain only a 'prop' element.
/// The elements contained by the 'prop' element inside the 'set'
/// element MUST specify the name and value of properties that are set
/// on the resource identified by Request-URI. If a property already
/// exists, then its value is replaced. Language tagging information
/// appearing in the scope of the 'prop' element (in the "xml:lang"
/// attribute, if present) MUST be persistently stored along with the
/// property, and MUST be subsequently retrievable using PROPFIND.
///
/// <!ELEMENT set (prop) >
#[derive(Debug, PartialEq, Clone)]
pub struct Set<E: Extension>(pub PropValue<E>);
/// 14.27. shared XML Element
///
/// Name: shared
///
/// Purpose: Specifies a shared lock.
///
///
/// <!ELEMENT shared EMPTY >
#[derive(Debug, PartialEq, Clone)]
pub struct Shared {}
/// 14.28. status XML Element
///
/// Name: status
///
/// Purpose: Holds a single HTTP status-line.
///
/// Value: status-line (defined in Section 6.1 of [RFC2616])
///
/// <!ELEMENT status (#PCDATA) >
//@FIXME: Better typing is possible with an enum for example
#[derive(Debug, PartialEq, Clone)]
pub struct Status(pub http::status::StatusCode);
/// 14.29. timeout XML Element
///
/// Name: timeout
///
/// Purpose: The number of seconds remaining before a lock expires.
///
/// Value: TimeType (defined in Section 10.7)
///
///
/// <!ELEMENT timeout (#PCDATA) >
///
/// TimeOut = "Timeout" ":" 1#TimeType
/// TimeType = ("Second-" DAVTimeOutVal | "Infinite")
/// ; No LWS allowed within TimeType
/// DAVTimeOutVal = 1*DIGIT
///
/// Clients MAY include Timeout request headers in their LOCK requests.
/// However, the server is not required to honor or even consider these
/// requests. Clients MUST NOT submit a Timeout request header with any
/// method other than a LOCK method.
///
/// The "Second" TimeType specifies the number of seconds that will
/// elapse between granting of the lock at the server, and the automatic
/// removal of the lock. The timeout value for TimeType "Second" MUST
/// NOT be greater than 2^32-1.
#[derive(Debug, PartialEq, Clone)]
pub enum Timeout {
Seconds(u32),
Infinite,
}
/// 15. DAV Properties
///
/// For DAV properties, the name of the property is also the same as the
/// name of the XML element that contains its value. In the section
/// below, the final line of each section gives the element type
/// declaration using the format defined in [REC-XML]. The "Value"
/// field, where present, specifies further restrictions on the allowable
/// contents of the XML element using BNF (i.e., to further restrict the
/// values of a PCDATA element).
///
/// A protected property is one that cannot be changed with a PROPPATCH
/// request. There may be other requests that would result in a change
/// to a protected property (as when a LOCK request affects the value of
/// DAV:lockdiscovery). Note that a given property could be protected on
/// one type of resource, but not protected on another type of resource.
///
/// A computed property is one with a value defined in terms of a
/// computation (based on the content and other properties of that
/// resource, or even of some other resource). A computed property is
/// always a protected property.
///
/// COPY and MOVE behavior refers to local COPY and MOVE operations.
///
/// For properties defined based on HTTP GET response headers (DAV:get*),
/// the header value could include LWS as defined in [RFC2616], Section
/// 4.2. Server implementors SHOULD strip LWS from these values before
/// using as WebDAV property values.
#[derive(Debug, PartialEq, Clone)]
pub enum AnyProperty<E: Extension> {
Request(PropertyRequest<E>),
Value(Property<E>),
}
#[derive(Debug, PartialEq, Clone)]
pub enum PropertyRequest<E: Extension> {
CreationDate,
DisplayName,
GetContentLanguage,
GetContentLength,
GetContentType,
GetEtag,
GetLastModified,
LockDiscovery,
ResourceType,
SupportedLock,
Extension(E::PropertyRequest),
}
#[derive(Debug, PartialEq, Clone)]
pub enum Property<E: Extension> {
/// 15.1. creationdate Property
///
/// Name: creationdate
///
/// Purpose: Records the time and date the resource was created.
///
/// Value: date-time (defined in [RFC3339], see the ABNF in Section
/// 5.6.)
///
/// Protected: MAY be protected. Some servers allow DAV:creationdate
/// to be changed to reflect the time the document was created if that
/// is more meaningful to the user (rather than the time it was
/// uploaded). Thus, clients SHOULD NOT use this property in
/// synchronization logic (use DAV:getetag instead).
///
/// COPY/MOVE behavior: This property value SHOULD be kept during a
/// MOVE operation, but is normally re-initialized when a resource is
/// created with a COPY. It should not be set in a COPY.
///
/// Description: The DAV:creationdate property SHOULD be defined on all
/// DAV compliant resources. If present, it contains a timestamp of
/// the moment when the resource was created. Servers that are
/// incapable of persistently recording the creation date SHOULD
/// instead leave it undefined (i.e. report "Not Found").
///
/// <!ELEMENT creationdate (#PCDATA) >
CreationDate(DateTime<FixedOffset>),
/// 15.2. displayname Property
///
/// Name: displayname
///
/// Purpose: Provides a name for the resource that is suitable for
/// presentation to a user.
///
/// Value: Any text.
///
/// Protected: SHOULD NOT be protected. Note that servers implementing
/// [RFC2518] might have made this a protected property as this is a
/// new requirement.
///
/// COPY/MOVE behavior: This property value SHOULD be preserved in COPY
/// and MOVE operations.
///
/// Description: Contains a description of the resource that is
/// suitable for presentation to a user. This property is defined on
/// the resource, and hence SHOULD have the same value independent of
/// the Request-URI used to retrieve it (thus, computing this property
/// based on the Request-URI is deprecated). While generic clients
/// might display the property value to end users, client UI designers
/// must understand that the method for identifying resources is still
/// the URL. Changes to DAV:displayname do not issue moves or copies
/// to the server, but simply change a piece of meta-data on the
/// individual resource. Two resources can have the same DAV:
/// displayname value even within the same collection.
///
/// <!ELEMENT displayname (#PCDATA) >
DisplayName(String),
/// 15.3. getcontentlanguage Property
///
/// Name: getcontentlanguage
///
/// Purpose: Contains the Content-Language header value (from Section
/// 14.12 of [RFC2616]) as it would be returned by a GET without
/// accept headers.
///
/// Value: language-tag (language-tag is defined in Section 3.10 of
/// [RFC2616])
///
/// Protected: SHOULD NOT be protected, so that clients can reset the
/// language. Note that servers implementing [RFC2518] might have
/// made this a protected property as this is a new requirement.
///
/// COPY/MOVE behavior: This property value SHOULD be preserved in COPY
/// and MOVE operations.
///
/// Description: The DAV:getcontentlanguage property MUST be defined on
/// any DAV-compliant resource that returns the Content-Language
/// header on a GET.
///
/// <!ELEMENT getcontentlanguage (#PCDATA) >
GetContentLanguage(String),
/// 15.4. getcontentlength Property
///
/// Name: getcontentlength
///
/// Purpose: Contains the Content-Length header returned by a GET
/// without accept headers.
///
/// Value: See Section 14.13 of [RFC2616].
///
/// Protected: This property is computed, therefore protected.
///
/// Description: The DAV:getcontentlength property MUST be defined on
/// any DAV-compliant resource that returns the Content-Length header
/// in response to a GET.
///
/// COPY/MOVE behavior: This property value is dependent on the size of
/// the destination resource, not the value of the property on the
/// source resource.
///
/// <!ELEMENT getcontentlength (#PCDATA) >
GetContentLength(u64),
/// 15.5. getcontenttype Property
///
/// Name: getcontenttype
///
/// Purpose: Contains the Content-Type header value (from Section 14.17
/// of [RFC2616]) as it would be returned by a GET without accept
/// headers.
///
/// Value: media-type (defined in Section 3.7 of [RFC2616])
///
/// Protected: Potentially protected if the server prefers to assign
/// content types on its own (see also discussion in Section 9.7.1).
///
/// COPY/MOVE behavior: This property value SHOULD be preserved in COPY
/// and MOVE operations.
///
/// Description: This property MUST be defined on any DAV-compliant
/// resource that returns the Content-Type header in response to a
/// GET.
///
/// <!ELEMENT getcontenttype (#PCDATA) >
GetContentType(String),
/// 15.6. getetag Property
///
/// Name: getetag
///
/// Purpose: Contains the ETag header value (from Section 14.19 of
/// [RFC2616]) as it would be returned by a GET without accept
/// headers.
///
/// Value: entity-tag (defined in Section 3.11 of [RFC2616])
///
/// Protected: MUST be protected because this value is created and
/// controlled by the server.
///
/// COPY/MOVE behavior: This property value is dependent on the final
/// state of the destination resource, not the value of the property
/// on the source resource. Also note the considerations in
/// Section 8.8.
///
/// Description: The getetag property MUST be defined on any DAV-
/// compliant resource that returns the Etag header. Refer to Section
/// 3.11 of RFC 2616 for a complete definition of the semantics of an
/// ETag, and to Section 8.6 for a discussion of ETags in WebDAV.
///
/// <!ELEMENT getetag (#PCDATA) >
GetEtag(String),
/// 15.7. getlastmodified Property
///
/// Name: getlastmodified
///
/// Purpose: Contains the Last-Modified header value (from Section
/// 14.29 of [RFC2616]) as it would be returned by a GET method
/// without accept headers.
///
/// Value: rfc1123-date (defined in Section 3.3.1 of [RFC2616])
///
/// Protected: SHOULD be protected because some clients may rely on the
/// value for appropriate caching behavior, or on the value of the
/// Last-Modified header to which this property is linked.
///
/// COPY/MOVE behavior: This property value is dependent on the last
/// modified date of the destination resource, not the value of the
/// property on the source resource. Note that some server
/// implementations use the file system date modified value for the
/// DAV:getlastmodified value, and this can be preserved in a MOVE
/// even when the HTTP Last-Modified value SHOULD change. Note that
/// since [RFC2616] requires clients to use ETags where provided, a
/// server implementing ETags can count on clients using a much better
/// mechanism than modification dates for offline synchronization or
/// cache control. Also note the considerations in Section 8.8.
///
/// Description: The last-modified date on a resource SHOULD only
/// reflect changes in the body (the GET responses) of the resource.
/// A change in a property only SHOULD NOT cause the last-modified
/// date to change, because clients MAY rely on the last-modified date
/// to know when to overwrite the existing body. The DAV:
/// getlastmodified property MUST be defined on any DAV-compliant
/// resource that returns the Last-Modified header in response to a
/// GET.
///
/// <!ELEMENT getlastmodified (#PCDATA) >
GetLastModified(DateTime<FixedOffset>),
/// 15.8. lockdiscovery Property
///
/// Name: lockdiscovery
///
/// Purpose: Describes the active locks on a resource
///
/// Protected: MUST be protected. Clients change the list of locks
/// through LOCK and UNLOCK, not through PROPPATCH.
///
/// COPY/MOVE behavior: The value of this property depends on the lock
/// state of the destination, not on the locks of the source resource.
/// Recall that locks are not moved in a MOVE operation.
///
/// Description: Returns a listing of who has a lock, what type of lock
/// he has, the timeout type and the time remaining on the timeout,
/// and the associated lock token. Owner information MAY be omitted
/// if it is considered sensitive. If there are no locks, but the
/// server supports locks, the property will be present but contain
/// zero 'activelock' elements. If there are one or more locks, an
/// 'activelock' element appears for each lock on the resource. This
/// property is NOT lockable with respect to write locks (Section 7).
///
/// <!ELEMENT lockdiscovery (activelock)* >
LockDiscovery(Vec<ActiveLock>),
/// 15.9. resourcetype Property
///
/// Name: resourcetype
///
/// Purpose: Specifies the nature of the resource.
///
/// Protected: SHOULD be protected. Resource type is generally decided
/// through the operation creating the resource (MKCOL vs PUT), not by
/// PROPPATCH.
///
/// COPY/MOVE behavior: Generally a COPY/MOVE of a resource results in
/// the same type of resource at the destination.
///
/// Description: MUST be defined on all DAV-compliant resources. Each
/// child element identifies a specific type the resource belongs to,
/// such as 'collection', which is the only resource type defined by
/// this specification (see Section 14.3). If the element contains
/// the 'collection' child element plus additional unrecognized
/// elements, it should generally be treated as a collection. If the
/// element contains no recognized child elements, it should be
/// treated as a non-collection resource. The default value is empty.
/// This element MUST NOT contain text or mixed content. Any custom
/// child element is considered to be an identifier for a resource
/// type.
///
/// Example: (fictional example to show extensibility)
///
/// <x:resourcetype xmlns:x="DAV:">
/// <x:collection/>
/// <f:search-results xmlns:f="http://www.example.com/ns"/>
/// </x:resourcetype>
ResourceType(Vec<ResourceType<E>>),
/// 15.10. supportedlock Property
///
/// Name: supportedlock
///
/// Purpose: To provide a listing of the lock capabilities supported by
/// the resource.
///
/// Protected: MUST be protected. Servers, not clients, determine what
/// lock mechanisms are supported.
/// COPY/MOVE behavior: This property value is dependent on the kind of
/// locks supported at the destination, not on the value of the
/// property at the source resource. Servers attempting to COPY to a
/// destination should not attempt to set this property at the
/// destination.
///
/// Description: Returns a listing of the combinations of scope and
/// access types that may be specified in a lock request on the
/// resource. Note that the actual contents are themselves controlled
/// by access controls, so a server is not required to provide
/// information the client is not authorized to see. This property is
/// NOT lockable with respect to write locks (Section 7).
///
/// <!ELEMENT supportedlock (lockentry)* >
SupportedLock(Vec<LockEntry>),
/// Any extension
Extension(E::Property),
}
#[derive(Debug, PartialEq, Clone)]
pub enum ResourceType<E: Extension> {
Collection,
Extension(E::ResourceType),
}

View file

@ -0,0 +1,132 @@
use super::error::ParsingError;
use super::types as dav;
use super::versioningtypes::*;
use super::xml::{IRead, QRead, Reader, DAV_URN};
// -- extensions ---
impl QRead<PropertyRequest> for PropertyRequest {
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
if xml
.maybe_open(DAV_URN, "supported-report-set")
.await?
.is_some()
{
xml.close().await?;
return Ok(Self::SupportedReportSet);
}
return Err(ParsingError::Recoverable);
}
}
impl<E: dav::Extension> QRead<Property<E>> for Property<E> {
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
if xml
.maybe_open_start(DAV_URN, "supported-report-set")
.await?
.is_some()
{
let supported_reports = xml.collect().await?;
xml.close().await?;
return Ok(Property::SupportedReportSet(supported_reports));
}
Err(ParsingError::Recoverable)
}
}
impl<E: dav::Extension> QRead<SupportedReport<E>> for SupportedReport<E> {
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
xml.open(DAV_URN, "supported-report").await?;
let r = xml.find().await?;
xml.close().await?;
Ok(SupportedReport(r))
}
}
impl<E: dav::Extension> QRead<ReportName<E>> for ReportName<E> {
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
xml.open(DAV_URN, "report").await?;
let final_result = if xml.maybe_open(DAV_URN, "version-tree").await?.is_some() {
xml.close().await?;
Ok(ReportName::VersionTree)
} else if xml.maybe_open(DAV_URN, "expand-property").await?.is_some() {
xml.close().await?;
Ok(ReportName::ExpandProperty)
} else {
let x = match xml.maybe_find().await? {
Some(v) => v,
None => return Err(ParsingError::MissingChild),
};
Ok(ReportName::Extension(x))
//E::ReportTypeName::qread(xml).await.map(ReportName::Extension)
};
xml.close().await?;
final_result
}
}
impl<E: dav::Extension> QRead<Report<E>> for Report<E> {
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
if xml.maybe_open(DAV_URN, "version-tree").await?.is_some() {
xml.close().await?;
tracing::warn!("version-tree is not implemented, skipping");
Ok(Report::VersionTree)
} else if xml.maybe_open(DAV_URN, "expand-property").await?.is_some() {
xml.close().await?;
tracing::warn!("expand-property is not implemented, skipping");
Ok(Report::ExpandProperty)
} else {
E::ReportType::qread(xml).await.map(Report::Extension)
}
}
}
impl QRead<Limit> for Limit {
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
xml.open(DAV_URN, "limit").await?;
let nres = xml.find().await?;
xml.close().await?;
Ok(Limit(nres))
}
}
impl QRead<NResults> for NResults {
async fn qread(xml: &mut Reader<impl IRead>) -> Result<Self, ParsingError> {
xml.open(DAV_URN, "nresults").await?;
let sz = xml.tag_string().await?.parse::<u64>()?;
xml.close().await?;
Ok(NResults(sz))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::xml::Node;
async fn deserialize<T: Node<T>>(src: &str) -> T {
let mut rdr = Reader::new(quick_xml::NsReader::from_reader(src.as_bytes()))
.await
.unwrap();
rdr.find().await.unwrap()
}
#[tokio::test]
async fn nresults() {
let expected = NResults(100);
let src = r#"<D:nresults xmlns:D="DAV:">100</D:nresults>"#;
let got = deserialize::<NResults>(src).await;
assert_eq!(got, expected);
}
#[tokio::test]
async fn limit() {
let expected = Limit(NResults(1024));
let src = r#"<D:limit xmlns:D="DAV:">
<D:nresults>1024</D:nresults>
</D:limit>"#;
let got = deserialize::<Limit>(src).await;
assert_eq!(got, expected);
}
}

View file

@ -0,0 +1,143 @@
use quick_xml::events::{BytesText, Event};
use quick_xml::Error as QError;
use super::types::Extension;
use super::versioningtypes::*;
use super::xml::{IWrite, QWrite, Writer};
// --- extensions to PROP
impl QWrite for PropertyRequest {
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
match self {
Self::SupportedReportSet => {
let start = xml.create_dav_element("supported-report-set");
xml.q.write_event_async(Event::Empty(start)).await
}
}
}
}
impl<E: Extension> QWrite for Property<E> {
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
match self {
Self::SupportedReportSet(set) => {
let start = xml.create_dav_element("supported-report-set");
let end = start.to_end();
xml.q.write_event_async(Event::Start(start.clone())).await?;
for v in set.iter() {
v.qwrite(xml).await?;
}
xml.q.write_event_async(Event::End(end)).await
}
}
}
}
impl<E: Extension> QWrite for SupportedReport<E> {
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
let start = xml.create_dav_element("supported-report");
let end = start.to_end();
xml.q.write_event_async(Event::Start(start.clone())).await?;
self.0.qwrite(xml).await?;
xml.q.write_event_async(Event::End(end)).await
}
}
impl<E: Extension> QWrite for ReportName<E> {
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
let start = xml.create_dav_element("report");
let end = start.to_end();
xml.q.write_event_async(Event::Start(start.clone())).await?;
match self {
Self::VersionTree => {
let start = xml.create_dav_element("version-tree");
xml.q.write_event_async(Event::Empty(start)).await?;
}
Self::ExpandProperty => {
let start = xml.create_dav_element("expand-property");
xml.q.write_event_async(Event::Empty(start)).await?;
}
Self::Extension(ext) => ext.qwrite(xml).await?,
};
xml.q.write_event_async(Event::End(end)).await
}
}
// --- root REPORT object ---
impl<E: Extension> QWrite for Report<E> {
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
match self {
Report::VersionTree => unimplemented!(),
Report::ExpandProperty => unimplemented!(),
Report::Extension(inner) => inner.qwrite(xml).await,
}
}
}
// --- limit REPORT parameter ---
impl QWrite for Limit {
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
let start = xml.create_dav_element("limit");
let end = start.to_end();
xml.q.write_event_async(Event::Start(start.clone())).await?;
self.0.qwrite(xml).await?;
xml.q.write_event_async(Event::End(end)).await
}
}
impl QWrite for NResults {
async fn qwrite(&self, xml: &mut Writer<impl IWrite>) -> Result<(), QError> {
let start = xml.create_dav_element("nresults");
let end = start.to_end();
xml.q.write_event_async(Event::Start(start.clone())).await?;
xml.q
.write_event_async(Event::Text(BytesText::new(&format!("{}", self.0))))
.await?;
xml.q.write_event_async(Event::End(end)).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::xml::Node;
use crate::xml::Reader;
use tokio::io::AsyncWriteExt;
async fn serialize_deserialize<T: Node<T>>(src: &T) -> T {
let mut buffer = Vec::new();
let mut tokio_buffer = tokio::io::BufWriter::new(&mut buffer);
let q = quick_xml::writer::Writer::new_with_indent(&mut tokio_buffer, b' ', 4);
let ns_to_apply = vec![
("xmlns:D".into(), "DAV:".into()),
("xmlns:C".into(), "urn:ietf:params:xml:ns:caldav".into()),
];
let mut writer = Writer { q, ns_to_apply };
src.qwrite(&mut writer).await.expect("xml serialization");
tokio_buffer.flush().await.expect("tokio buffer flush");
let got = std::str::from_utf8(buffer.as_slice()).unwrap();
// deserialize
let mut rdr = Reader::new(quick_xml::NsReader::from_reader(got.as_bytes()))
.await
.unwrap();
rdr.find().await.unwrap()
}
#[tokio::test]
async fn nresults() {
let orig = NResults(100);
assert_eq!(orig, serialize_deserialize(&orig).await);
}
#[tokio::test]
async fn limit() {
let orig = Limit(NResults(1024));
assert_eq!(orig, serialize_deserialize(&orig).await);
}
}

View file

@ -0,0 +1,59 @@
use super::types as dav;
//@FIXME required for a full DAV implementation
// See section 7.1 of the CalDAV RFC
// It seems it's mainly due to the fact that the REPORT method is re-used.
// https://datatracker.ietf.org/doc/html/rfc4791#section-7.1
//
// Defines (required by CalDAV):
// - REPORT method
// - expand-property root report method
//
// Defines (required by Sync):
// - limit, nresults
// - supported-report-set
// This property identifies the reports that are supported by the
// resource.
//
// <!ELEMENT supported-report-set (supported-report*)>
// <!ELEMENT supported-report report>
// <!ELEMENT report ANY>
// ANY value: a report element type
#[derive(Debug, PartialEq, Clone)]
pub enum PropertyRequest {
SupportedReportSet,
}
#[derive(Debug, PartialEq, Clone)]
pub enum Property<E: dav::Extension> {
SupportedReportSet(Vec<SupportedReport<E>>),
}
#[derive(Debug, PartialEq, Clone)]
pub struct SupportedReport<E: dav::Extension>(pub ReportName<E>);
#[derive(Debug, PartialEq, Clone)]
pub enum ReportName<E: dav::Extension> {
VersionTree,
ExpandProperty,
Extension(E::ReportTypeName),
}
#[derive(Debug, PartialEq, Clone)]
pub enum Report<E: dav::Extension> {
VersionTree, // Not yet implemented
ExpandProperty, // Not yet implemented
Extension(E::ReportType),
}
/// Limit
/// <!ELEMENT limit (nresults) >
#[derive(Debug, PartialEq, Clone)]
pub struct Limit(pub NResults);
/// NResults
/// <!ELEMENT nresults (#PCDATA) >
#[derive(Debug, PartialEq, Clone)]
pub struct NResults(pub u64);

367
aero-dav/src/xml.rs Normal file
View file

@ -0,0 +1,367 @@
use futures::Future;
use quick_xml::events::{BytesStart, Event};
use quick_xml::name::ResolveResult;
use quick_xml::reader::NsReader;
use tokio::io::{AsyncBufRead, AsyncWrite};
use super::error::ParsingError;
// Constants
pub const DAV_URN: &[u8] = b"DAV:";
pub const CAL_URN: &[u8] = b"urn:ietf:params:xml:ns:caldav";
pub const CARD_URN: &[u8] = b"urn:ietf:params:xml:ns:carddav";
// Async traits
pub trait IWrite = AsyncWrite + Unpin + Send;
pub trait IRead = AsyncBufRead + Unpin;
// Serialization/Deserialization traits
pub trait QWrite {
fn qwrite(
&self,
xml: &mut Writer<impl IWrite>,
) -> impl Future<Output = Result<(), quick_xml::Error>> + Send;
}
pub trait QRead<T> {
fn qread(xml: &mut Reader<impl IRead>) -> impl Future<Output = Result<T, ParsingError>>;
}
// The representation of an XML node in Rust
pub trait Node<T> = QRead<T> + QWrite + std::fmt::Debug + PartialEq + Clone + Sync;
// ---------------
/// Transform a Rust object into an XML stream of characters
pub struct Writer<T: IWrite> {
pub q: quick_xml::writer::Writer<T>,
pub ns_to_apply: Vec<(String, String)>,
}
impl<T: IWrite> Writer<T> {
pub fn create_dav_element(&mut self, name: &str) -> BytesStart<'static> {
self.create_ns_element("D", name)
}
pub fn create_cal_element(&mut self, name: &str) -> BytesStart<'static> {
self.create_ns_element("C", name)
}
fn create_ns_element(&mut self, ns: &str, name: &str) -> BytesStart<'static> {
let mut start = BytesStart::new(format!("{}:{}", ns, name));
if !self.ns_to_apply.is_empty() {
start.extend_attributes(
self.ns_to_apply
.iter()
.map(|(k, n)| (k.as_str(), n.as_str())),
);
self.ns_to_apply.clear()
}
start
}
}
/// Transform an XML stream of characters into a Rust object
pub struct Reader<T: IRead> {
pub rdr: NsReader<T>,
cur: Event<'static>,
prev: Event<'static>,
parents: Vec<Event<'static>>,
buf: Vec<u8>,
}
impl<T: IRead> Reader<T> {
pub async fn new(mut rdr: NsReader<T>) -> Result<Self, ParsingError> {
let mut buf: Vec<u8> = vec![];
let cur = rdr.read_event_into_async(&mut buf).await?.into_owned();
let parents = vec![];
let prev = Event::Eof;
buf.clear();
Ok(Self {
cur,
prev,
parents,
rdr,
buf,
})
}
/// read one more tag
/// do not expose it publicly
async fn next(&mut self) -> Result<Event<'static>, ParsingError> {
let evt = self
.rdr
.read_event_into_async(&mut self.buf)
.await?
.into_owned();
self.buf.clear();
self.prev = std::mem::replace(&mut self.cur, evt);
Ok(self.prev.clone())
}
/// skip a node at current level
/// I would like to make this one private but not ready
pub async fn skip(&mut self) -> Result<Event<'static>, ParsingError> {
//println!("skipping inside node {:?} value {:?}", self.parents.last(), self.cur);
match &self.cur {
Event::Start(b) => {
let _span = self
.rdr
.read_to_end_into_async(b.to_end().name(), &mut self.buf)
.await?;
self.next().await
}
Event::End(_) => Err(ParsingError::WrongToken),
Event::Eof => Err(ParsingError::Eof),
_ => self.next().await,
}
}
/// check if this is the desired tag
fn is_tag(&self, ns: &[u8], key: &str) -> bool {
let qname = match self.peek() {
Event::Start(bs) | Event::Empty(bs) => bs.name(),
Event::End(be) => be.name(),
_ => return false,
};
let (extr_ns, local) = self.rdr.resolve_element(qname);
if local.into_inner() != key.as_bytes() {
return false;
}
match extr_ns {
ResolveResult::Bound(v) => v.into_inner() == ns,
_ => false,
}
}
pub fn parent_has_child(&self) -> bool {
matches!(self.parents.last(), Some(Event::Start(_)) | None)
}
fn ensure_parent_has_child(&self) -> Result<(), ParsingError> {
match self.parent_has_child() {
true => Ok(()),
false => Err(ParsingError::Recoverable),
}
}
pub fn peek(&self) -> &Event<'static> {
&self.cur
}
pub fn previous(&self) -> &Event<'static> {
&self.prev
}
// NEW API
pub async fn tag_string(&mut self) -> Result<String, ParsingError> {
self.ensure_parent_has_child()?;
let mut acc = String::new();
loop {
match self.peek() {
Event::CData(unescaped) => {
acc.push_str(std::str::from_utf8(unescaped.as_ref())?);
self.next().await?
}
Event::Text(escaped) => {
acc.push_str(escaped.unescape()?.as_ref());
self.next().await?
}
Event::End(_) | Event::Start(_) | Event::Empty(_) => return Ok(acc),
_ => self.next().await?,
};
}
}
pub async fn maybe_read<N: Node<N>>(
&mut self,
t: &mut Option<N>,
dirty: &mut bool,
) -> Result<(), ParsingError> {
if !self.parent_has_child() {
return Ok(());
}
match N::qread(self).await {
Ok(v) => {
*t = Some(v);
*dirty = true;
Ok(())
}
Err(ParsingError::Recoverable) => Ok(()),
Err(e) => Err(e),
}
}
pub async fn maybe_push<N: Node<N>>(
&mut self,
t: &mut Vec<N>,
dirty: &mut bool,
) -> Result<(), ParsingError> {
if !self.parent_has_child() {
return Ok(());
}
match N::qread(self).await {
Ok(v) => {
t.push(v);
*dirty = true;
Ok(())
}
Err(ParsingError::Recoverable) => Ok(()),
Err(e) => Err(e),
}
}
pub async fn find<N: Node<N>>(&mut self) -> Result<N, ParsingError> {
self.ensure_parent_has_child()?;
loop {
// Try parse
match N::qread(self).await {
Err(ParsingError::Recoverable) => (),
otherwise => return otherwise,
}
// If recovered, skip the element
self.skip().await?;
}
}
pub async fn maybe_find<N: Node<N>>(&mut self) -> Result<Option<N>, ParsingError> {
// We can't find anything inside a self-closed tag
if !self.parent_has_child() {
return Ok(None);
}
loop {
// Try parse
match N::qread(self).await {
Err(ParsingError::Recoverable) => (),
otherwise => return otherwise.map(Some),
}
// Skip or stop
match self.peek() {
Event::End(_) => return Ok(None),
_ => self.skip().await?,
};
}
}
pub async fn collect<N: Node<N>>(&mut self) -> Result<Vec<N>, ParsingError> {
let mut acc = Vec::new();
if !self.parent_has_child() {
return Ok(acc);
}
loop {
match N::qread(self).await {
Err(ParsingError::Recoverable) => match self.peek() {
Event::End(_) => return Ok(acc),
_ => {
self.skip().await?;
}
},
Ok(v) => acc.push(v),
Err(e) => return Err(e),
}
}
}
pub async fn open(&mut self, ns: &[u8], key: &str) -> Result<Event<'static>, ParsingError> {
//println!("try open tag {:?}, on {:?}", key, self.peek());
let evt = match self.peek() {
Event::Empty(_) if self.is_tag(ns, key) => {
// hack to make `prev_attr` works
// here we duplicate the current tag
// as in other words, we virtually moved one token
// which is useful for prev_attr and any logic based on
// self.prev + self.open() on empty nodes
self.prev = self.cur.clone();
self.cur.clone()
}
Event::Start(_) if self.is_tag(ns, key) => self.next().await?,
_ => return Err(ParsingError::Recoverable),
};
//println!("open tag {:?}", evt);
self.parents.push(evt.clone());
Ok(evt)
}
pub async fn open_start(
&mut self,
ns: &[u8],
key: &str,
) -> Result<Event<'static>, ParsingError> {
//println!("try open start tag {:?}, on {:?}", key, self.peek());
let evt = match self.peek() {
Event::Start(_) if self.is_tag(ns, key) => self.next().await?,
_ => return Err(ParsingError::Recoverable),
};
//println!("open start tag {:?}", evt);
self.parents.push(evt.clone());
Ok(evt)
}
pub async fn maybe_open(
&mut self,
ns: &[u8],
key: &str,
) -> Result<Option<Event<'static>>, ParsingError> {
match self.open(ns, key).await {
Ok(v) => Ok(Some(v)),
Err(ParsingError::Recoverable) => Ok(None),
Err(e) => Err(e),
}
}
pub async fn maybe_open_start(
&mut self,
ns: &[u8],
key: &str,
) -> Result<Option<Event<'static>>, ParsingError> {
match self.open_start(ns, key).await {
Ok(v) => Ok(Some(v)),
Err(ParsingError::Recoverable) => Ok(None),
Err(e) => Err(e),
}
}
pub fn prev_attr(&self, attr: &str) -> Option<String> {
match &self.prev {
Event::Start(bs) | Event::Empty(bs) => match bs.try_get_attribute(attr) {
Ok(Some(attr)) => attr
.decode_and_unescape_value(&self.rdr)
.ok()
.map(|v| v.into_owned()),
_ => None,
},
_ => None,
}
}
// find stop tag
pub async fn close(&mut self) -> Result<Event<'static>, ParsingError> {
//println!("close tag {:?}", self.parents.last());
// Handle the empty case
if !self.parent_has_child() {
self.parents.pop();
return self.next().await;
}
// Handle the start/end case
loop {
match self.peek() {
Event::End(_) => {
self.parents.pop();
return self.next().await;
}
_ => self.skip().await?,
};
}
}
}

15
aero-ical/Cargo.toml Normal file
View file

@ -0,0 +1,15 @@
[package]
name = "aero-ical"
version = "0.3.0"
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
edition = "2021"
license = "EUPL-1.2"
description = "An iCalendar parser"
[dependencies]
aero-dav.workspace = true
icalendar.workspace = true
nom.workspace = true
chrono.workspace = true
tracing.workspace = true

8
aero-ical/src/lib.rs Normal file
View file

@ -0,0 +1,8 @@
/// The iCalendar module is not yet properly rewritten
/// Instead we heavily rely on the icalendar library
/// However, for many reason, it's not satisfying:
/// the goal will be to rewrite it in the end so it better
/// integrates into Aerogramme
pub mod parser;
pub mod prune;
pub mod query;

146
aero-ical/src/parser.rs Normal file
View file

@ -0,0 +1,146 @@
use chrono::TimeDelta;
use nom::branch::alt;
use nom::bytes::complete::{tag, tag_no_case};
use nom::character::complete as nomchar;
use nom::combinator::{map, map_opt, opt, value};
use nom::sequence::{pair, tuple};
use nom::IResult;
use aero_dav::caltypes as cal;
//@FIXME too simple, we have 4 cases in practices:
// - floating datetime
// - floating datetime with a tzid as param so convertible to tz datetime
// - utc datetime
// - floating(?) date (without time)
pub fn date_time(dt: &str) -> Option<chrono::DateTime<chrono::Utc>> {
tracing::trace!(raw_time = dt, "VEVENT raw time");
let tmpl = match dt.chars().last() {
Some('Z') => cal::UTC_DATETIME_FMT,
Some(_) => {
tracing::warn!(
raw_time = dt,
"floating datetime is not properly supported yet"
);
cal::FLOATING_DATETIME_FMT
}
None => return None,
};
chrono::NaiveDateTime::parse_from_str(dt, tmpl)
.ok()
.map(|v| v.and_utc())
}
/// RFC3389 Duration Value
///
/// ```abnf
/// dur-value = (["+"] / "-") "P" (dur-date / dur-time / dur-week)
/// dur-date = dur-day [dur-time]
/// dur-time = "T" (dur-hour / dur-minute / dur-second)
/// dur-week = 1*DIGIT "W"
/// dur-hour = 1*DIGIT "H" [dur-minute]
/// dur-minute = 1*DIGIT "M" [dur-second]
/// dur-second = 1*DIGIT "S"
/// dur-day = 1*DIGIT "D"
/// ```
pub fn dur_value(text: &str) -> IResult<&str, TimeDelta> {
map_opt(
tuple((
dur_sign,
tag_no_case("P"),
alt((dur_date, dur_time, dur_week)),
)),
|(sign, _, delta)| delta.checked_mul(sign),
)(text)
}
fn dur_sign(text: &str) -> IResult<&str, i32> {
map(opt(alt((value(1, tag("+")), value(-1, tag("-"))))), |x| {
x.unwrap_or(1)
})(text)
}
fn dur_date(text: &str) -> IResult<&str, TimeDelta> {
map(pair(dur_day, opt(dur_time)), |(day, time)| {
day + time.unwrap_or(TimeDelta::zero())
})(text)
}
fn dur_time(text: &str) -> IResult<&str, TimeDelta> {
map(
pair(tag_no_case("T"), alt((dur_hour, dur_minute, dur_second))),
|(_, x)| x,
)(text)
}
fn dur_week(text: &str) -> IResult<&str, TimeDelta> {
map_opt(pair(nomchar::i64, tag_no_case("W")), |(i, _)| {
TimeDelta::try_weeks(i)
})(text)
}
fn dur_day(text: &str) -> IResult<&str, TimeDelta> {
map_opt(pair(nomchar::i64, tag_no_case("D")), |(i, _)| {
TimeDelta::try_days(i)
})(text)
}
fn dur_hour(text: &str) -> IResult<&str, TimeDelta> {
map_opt(
tuple((nomchar::i64, tag_no_case("H"), opt(dur_minute))),
|(i, _, mm)| TimeDelta::try_hours(i).map(|hours| hours + mm.unwrap_or(TimeDelta::zero())),
)(text)
}
fn dur_minute(text: &str) -> IResult<&str, TimeDelta> {
map_opt(
tuple((nomchar::i64, tag_no_case("M"), opt(dur_second))),
|(i, _, ms)| TimeDelta::try_minutes(i).map(|min| min + ms.unwrap_or(TimeDelta::zero())),
)(text)
}
fn dur_second(text: &str) -> IResult<&str, TimeDelta> {
map_opt(pair(nomchar::i64, tag_no_case("S")), |(i, _)| {
TimeDelta::try_seconds(i)
})(text)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rfc5545_example1() {
// A duration of 15 days, 5 hours, and 20 seconds would be:
let to_parse = "P15DT5H0M20S";
let (_, time_delta) = dur_value(to_parse).unwrap();
assert_eq!(
time_delta,
TimeDelta::try_days(15).unwrap()
+ TimeDelta::try_hours(5).unwrap()
+ TimeDelta::try_seconds(20).unwrap()
);
}
#[test]
fn rfc5545_example2() {
// A duration of 7 weeks would be:
let to_parse = "P7W";
let (_, time_delta) = dur_value(to_parse).unwrap();
assert_eq!(time_delta, TimeDelta::try_weeks(7).unwrap());
}
#[test]
fn rfc4791_example1() {
// 10 minutes before
let to_parse = "-PT10M";
let (_, time_delta) = dur_value(to_parse).unwrap();
assert_eq!(time_delta, TimeDelta::try_minutes(-10).unwrap());
}
#[test]
fn ical_org_example1() {
// The following example is for a "VALARM" calendar component that specifies an email alarm
// that will trigger 2 days before the scheduled due DATE-TIME of a to-do with which it is associated.
let to_parse = "-P2D";
let (_, time_delta) = dur_value(to_parse).unwrap();
assert_eq!(time_delta, TimeDelta::try_days(-2).unwrap());
}
}

55
aero-ical/src/prune.rs Normal file
View file

@ -0,0 +1,55 @@
use aero_dav::caltypes as cal;
use icalendar::parser::{Component, Property};
pub fn component<'a>(src: &'a Component<'a>, prune: &cal::Comp) -> Option<Component<'a>> {
if src.name.as_str() != prune.name.as_str() {
return None;
}
let name = src.name.clone();
let properties = match &prune.prop_kind {
Some(cal::PropKind::AllProp) | None => src.properties.clone(),
Some(cal::PropKind::Prop(l)) => src
.properties
.iter()
.filter_map(|prop| {
let sel_filt = match l
.iter()
.find(|filt| filt.name.0.as_str() == prop.name.as_str())
{
Some(v) => v,
None => return None,
};
match sel_filt.novalue {
None | Some(false) => Some(prop.clone()),
Some(true) => Some(Property {
name: prop.name.clone(),
params: prop.params.clone(),
val: "".into(),
}),
}
})
.collect::<Vec<_>>(),
};
let components = match &prune.comp_kind {
Some(cal::CompKind::AllComp) | None => src.components.clone(),
Some(cal::CompKind::Comp(many_inner_prune)) => src
.components
.iter()
.filter_map(|src_component| {
many_inner_prune
.iter()
.find_map(|inner_prune| component(src_component, inner_prune))
})
.collect::<Vec<_>>(),
};
Some(Component {
name,
properties,
components,
})
}

338
aero-ical/src/query.rs Normal file
View file

@ -0,0 +1,338 @@
use crate::parser;
use aero_dav::caltypes as cal;
pub fn is_component_match(
parent: &icalendar::parser::Component,
components: &[icalendar::parser::Component],
filter: &cal::CompFilter,
) -> bool {
// Find the component among the list
let maybe_comps = components
.iter()
.filter(|candidate| candidate.name.as_str() == filter.name.as_str())
.collect::<Vec<_>>();
// Filter according to rules
match (&maybe_comps[..], &filter.additional_rules) {
([_, ..], None) => true,
([], Some(cal::CompFilterRules::IsNotDefined)) => true,
([], None) => false,
([_, ..], Some(cal::CompFilterRules::IsNotDefined)) => false,
(comps, Some(cal::CompFilterRules::Matches(matcher))) => comps.iter().any(|component| {
// check time range
if let Some(time_range) = &matcher.time_range {
if !is_in_time_range(
&filter.name,
parent,
component.properties.as_ref(),
time_range,
) {
return false;
}
}
// check properties
if !is_properties_match(component.properties.as_ref(), matcher.prop_filter.as_ref()) {
return false;
}
// check inner components
matcher.comp_filter.iter().all(|inner_filter| {
is_component_match(component, component.components.as_ref(), &inner_filter)
})
}),
}
}
fn prop_date(
properties: &[icalendar::parser::Property],
name: &str,
) -> Option<chrono::DateTime<chrono::Utc>> {
properties
.iter()
.find(|candidate| candidate.name.as_str() == name)
.map(|p| p.val.as_str())
.map(parser::date_time)
.flatten()
}
fn prop_parse<T: std::str::FromStr>(
properties: &[icalendar::parser::Property],
name: &str,
) -> Option<T> {
properties
.iter()
.find(|candidate| candidate.name.as_str() == name)
.map(|p| p.val.as_str().parse::<T>().ok())
.flatten()
}
fn is_properties_match(props: &[icalendar::parser::Property], filters: &[cal::PropFilter]) -> bool {
filters.iter().all(|single_filter| {
// Find the property
let candidate_props = props
.iter()
.filter(|candidate| candidate.name.as_str() == single_filter.name.0.as_str())
.collect::<Vec<_>>();
match (&single_filter.additional_rules, &candidate_props[..]) {
(None, [_, ..]) | (Some(cal::PropFilterRules::IsNotDefined), []) => true,
(None, []) | (Some(cal::PropFilterRules::IsNotDefined), [_, ..]) => false,
(Some(cal::PropFilterRules::Match(pattern)), multi_props) => {
multi_props.iter().any(|prop| {
// check value
match &pattern.time_or_text {
Some(cal::TimeOrText::Time(time_range)) => {
let maybe_parsed_date = parser::date_time(prop.val.as_str());
let parsed_date = match maybe_parsed_date {
None => return false,
Some(v) => v,
};
// see if entry is in range
let is_in_range = match time_range {
cal::TimeRange::OnlyStart(after) => &parsed_date >= after,
cal::TimeRange::OnlyEnd(before) => &parsed_date <= before,
cal::TimeRange::FullRange(after, before) => {
&parsed_date >= after && &parsed_date <= before
}
};
if !is_in_range {
return false;
}
// if you are here, this subcondition is valid
}
Some(cal::TimeOrText::Text(txt_match)) => {
//@FIXME ignoring collation
let is_match = match txt_match.negate_condition {
None | Some(false) => {
prop.val.as_str().contains(txt_match.text.as_str())
}
Some(true) => !prop.val.as_str().contains(txt_match.text.as_str()),
};
if !is_match {
return false;
}
}
None => (), // if not filter on value is set, continue
};
// check parameters
pattern.param_filter.iter().all(|single_param_filter| {
let multi_param = prop
.params
.iter()
.filter(|candidate| {
candidate.key.as_str() == single_param_filter.name.as_str()
})
.collect::<Vec<_>>();
match (&multi_param[..], &single_param_filter.additional_rules) {
([.., _], None) => true,
([], None) => false,
([.., _], Some(cal::ParamFilterMatch::IsNotDefined)) => false,
([], Some(cal::ParamFilterMatch::IsNotDefined)) => true,
(many_params, Some(cal::ParamFilterMatch::Match(txt_match))) => {
many_params.iter().any(|param| {
let param_val = match &param.val {
Some(v) => v,
None => return false,
};
match txt_match.negate_condition {
None | Some(false) => {
param_val.as_str().contains(txt_match.text.as_str())
}
Some(true) => {
!param_val.as_str().contains(txt_match.text.as_str())
}
}
})
}
}
})
})
}
}
})
}
fn resolve_trigger(
parent: &icalendar::parser::Component,
properties: &[icalendar::parser::Property],
) -> Option<chrono::DateTime<chrono::Utc>> {
// A. Do we have a TRIGGER property? If not, returns early
let maybe_trigger_prop = properties
.iter()
.find(|candidate| candidate.name.as_str() == "TRIGGER");
let trigger_prop = match maybe_trigger_prop {
None => return None,
Some(v) => v,
};
// B.1 Is it an absolute datetime? If so, returns early
let maybe_absolute = trigger_prop
.params
.iter()
.find(|param| param.key.as_str() == "VALUE")
.map(|param| param.val.as_ref())
.flatten()
.map(|v| v.as_str() == "DATE-TIME");
if maybe_absolute.is_some() {
let final_date = prop_date(properties, "TRIGGER");
tracing::trace!(trigger=?final_date, "resolved absolute trigger");
return final_date;
}
// B.2 Otherwise it's a timedelta relative to a parent field.
// C.1 Parse the timedelta value, returns early if invalid
let (_, time_delta) = parser::dur_value(trigger_prop.val.as_str()).ok()?;
// C.2 Get the parent reference absolute datetime, returns early if invalid
let maybe_bound = trigger_prop
.params
.iter()
.find(|param| param.key.as_str() == "RELATED")
.map(|param| param.val.as_ref())
.flatten();
// If the trigger is set relative to START, then the "DTSTART" property MUST be present in the associated
// "VEVENT" or "VTODO" calendar component.
//
// If an alarm is specified for an event with the trigger set relative to the END,
// then the "DTEND" property or the "DTSTART" and "DURATION " properties MUST be present
// in the associated "VEVENT" calendar component.
//
// If the alarm is specified for a to-do with a trigger set relative to the END,
// then either the "DUE" property or the "DTSTART" and "DURATION " properties
// MUST be present in the associated "VTODO" calendar component.
let related_field = match maybe_bound.as_ref().map(|v| v.as_str()) {
Some("START") => "DTSTART",
Some("END") => "DTEND", //@FIXME must add support for DUE, DTSTART, and DURATION
_ => "DTSTART", // by default use DTSTART
};
let parent_date = match prop_date(parent.properties.as_ref(), related_field) {
Some(v) => v,
_ => return None,
};
// C.3 Compute the final date from the base date + timedelta
let final_date = parent_date + time_delta;
tracing::trace!(trigger=?final_date, "resolved relative trigger");
Some(final_date)
}
fn is_in_time_range(
component: &cal::Component,
parent: &icalendar::parser::Component,
properties: &[icalendar::parser::Property],
time_range: &cal::TimeRange,
) -> bool {
//@FIXME timezones are not properly handled currently (everything is UTC)
//@FIXME does not support repeat
//ref: https://datatracker.ietf.org/doc/html/rfc4791#section-9.9
let (start, end) = match time_range {
cal::TimeRange::OnlyStart(start) => (start, &chrono::DateTime::<chrono::Utc>::MAX_UTC),
cal::TimeRange::OnlyEnd(end) => (&chrono::DateTime::<chrono::Utc>::MIN_UTC, end),
cal::TimeRange::FullRange(start, end) => (start, end),
};
match component {
cal::Component::VEvent => {
let dtstart = match prop_date(properties, "DTSTART") {
Some(v) => v,
_ => return false,
};
let maybe_dtend = prop_date(properties, "DTEND");
let maybe_duration = prop_parse::<i64>(properties, "DURATION")
.map(|d| chrono::TimeDelta::new(std::cmp::max(d, 0), 0))
.flatten();
//@FIXME missing "date" management (only support "datetime")
match (&maybe_dtend, &maybe_duration) {
// | Y | N | N | * | (start < DTEND AND end > DTSTART) |
(Some(dtend), _) => start < dtend && end > &dtstart,
// | N | Y | Y | * | (start < DTSTART+DURATION AND end > DTSTART) |
(_, Some(duration)) => *start <= dtstart + *duration && end > &dtstart,
// | N | N | N | Y | (start <= DTSTART AND end > DTSTART) |
_ => start <= &dtstart && end > &dtstart,
}
}
cal::Component::VTodo => {
let maybe_dtstart = prop_date(properties, "DTSTART");
let maybe_due = prop_date(properties, "DUE");
let maybe_completed = prop_date(properties, "COMPLETED");
let maybe_created = prop_date(properties, "CREATED");
let maybe_duration = prop_parse::<i64>(properties, "DURATION")
.map(|d| chrono::TimeDelta::new(d, 0))
.flatten();
match (
maybe_dtstart,
maybe_duration,
maybe_due,
maybe_completed,
maybe_created,
) {
// | Y | Y | N | * | * | (start <= DTSTART+DURATION) AND |
// | | | | | | ((end > DTSTART) OR |
// | | | | | | (end >= DTSTART+DURATION)) |
(Some(dtstart), Some(duration), None, _, _) => {
*start <= dtstart + duration && (*end > dtstart || *end >= dtstart + duration)
}
// | Y | N | Y | * | * | ((start < DUE) OR (start <= DTSTART)) |
// | | | | | | AND |
// | | | | | | ((end > DTSTART) OR (end >= DUE)) |
(Some(dtstart), None, Some(due), _, _) => {
(*start < due || *start <= dtstart) && (*end > dtstart || *end >= due)
}
// | Y | N | N | * | * | (start <= DTSTART) AND (end > DTSTART) |
(Some(dtstart), None, None, _, _) => *start <= dtstart && *end > dtstart,
// | N | N | Y | * | * | (start < DUE) AND (end >= DUE) |
(None, None, Some(due), _, _) => *start < due && *end >= due,
// | N | N | N | Y | Y | ((start <= CREATED) OR (start <= COMPLETED))|
// | | | | | | AND |
// | | | | | | ((end >= CREATED) OR (end >= COMPLETED))|
(None, None, None, Some(completed), Some(created)) => {
(*start <= created || *start <= completed)
&& (*end >= created || *end >= completed)
}
// | N | N | N | Y | N | (start <= COMPLETED) AND (end >= COMPLETED) |
(None, None, None, Some(completed), None) => {
*start <= completed && *end >= completed
}
// | N | N | N | N | Y | (end > CREATED) |
(None, None, None, None, Some(created)) => *end > created,
// | N | N | N | N | N | TRUE |
_ => true,
}
}
cal::Component::VJournal => {
let maybe_dtstart = prop_date(properties, "DTSTART");
match maybe_dtstart {
// | Y | Y | (start <= DTSTART) AND (end > DTSTART) |
Some(dtstart) => *start <= dtstart && *end > dtstart,
// | N | * | FALSE |
None => false,
}
}
cal::Component::VFreeBusy => {
//@FIXME freebusy is not supported yet
false
}
cal::Component::VAlarm => {
//@FIXME does not support REPEAT
let maybe_trigger = resolve_trigger(parent, properties);
match maybe_trigger {
// (start <= trigger-time) AND (end > trigger-time)
Some(trigger_time) => *start <= trigger_time && *end > trigger_time,
_ => false,
}
}
_ => false,
}
}

39
aero-proto/Cargo.toml Normal file
View file

@ -0,0 +1,39 @@
[package]
name = "aero-proto"
version = "0.3.0"
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
edition = "2021"
license = "EUPL-1.2"
description = "Binding between Aerogramme's internal components and well-known protocols"
[dependencies]
aero-ical.workspace = true
aero-sasl.workspace = true
aero-dav.workspace = true
aero-user.workspace = true
aero-collections.workspace = true
async-trait.workspace = true
anyhow.workspace = true
hyper.workspace = true
base64.workspace = true
hyper-util.workspace = true
http-body-util.workspace = true
futures.workspace = true
tokio.workspace = true
tokio-util.workspace = true
tokio-rustls.workspace = true
tokio-stream.workspace = true
rustls.workspace = true
rustls-pemfile.workspace = true
imap-codec.workspace = true
imap-flow.workspace = true
chrono.workspace = true
eml-codec.workspace = true
thiserror.workspace = true
duplexify.workspace = true
smtp-message.workspace = true
smtp-server.workspace = true
tracing.workspace = true
quick-xml.workspace = true
icalendar.workspace = true

135
aero-proto/src/dav/codec.rs Normal file
View file

@ -0,0 +1,135 @@
use anyhow::{bail, Result};
use futures::sink::SinkExt;
use futures::stream::StreamExt;
use futures::stream::TryStreamExt;
use http_body_util::combinators::UnsyncBoxBody;
use http_body_util::BodyExt;
use http_body_util::BodyStream;
use http_body_util::Full;
use http_body_util::StreamBody;
use hyper::body::Frame;
use hyper::body::Incoming;
use hyper::{body::Bytes, Request, Response};
use std::io::{Error, ErrorKind};
use tokio_util::io::{CopyToBytes, SinkWriter};
use tokio_util::sync::PollSender;
use super::controller::HttpResponse;
use super::node::PutPolicy;
use aero_dav::types as dav;
use aero_dav::xml as dxml;
pub(crate) fn depth(req: &Request<impl hyper::body::Body>) -> dav::Depth {
match req
.headers()
.get("Depth")
.map(hyper::header::HeaderValue::to_str)
{
Some(Ok("0")) => dav::Depth::Zero,
Some(Ok("1")) => dav::Depth::One,
Some(Ok("Infinity")) => dav::Depth::Infinity,
_ => dav::Depth::Zero,
}
}
pub(crate) fn put_policy(req: &Request<impl hyper::body::Body>) -> Result<PutPolicy> {
if let Some(maybe_txt_etag) = req
.headers()
.get("If-Match")
.map(hyper::header::HeaderValue::to_str)
{
let etag = maybe_txt_etag?;
let dquote_count = etag.chars().filter(|c| *c == '"').count();
if dquote_count != 2 {
bail!("Either If-Match value is invalid or it's not supported (only single etag is supported)");
}
return Ok(PutPolicy::ReplaceEtag(etag.into()));
}
if let Some(maybe_txt_etag) = req
.headers()
.get("If-None-Match")
.map(hyper::header::HeaderValue::to_str)
{
let etag = maybe_txt_etag?;
if etag == "*" {
return Ok(PutPolicy::CreateOnly);
}
bail!("Either If-None-Match value is invalid or it's not supported (only asterisk is supported)")
}
Ok(PutPolicy::OverwriteAll)
}
pub(crate) fn text_body(txt: &'static str) -> UnsyncBoxBody<Bytes, std::io::Error> {
UnsyncBoxBody::new(Full::new(Bytes::from(txt)).map_err(|e| match e {}))
}
pub(crate) fn serialize<T: dxml::QWrite + Send + 'static>(
status_ok: hyper::StatusCode,
elem: T,
) -> Result<HttpResponse> {
let (tx, rx) = tokio::sync::mpsc::channel::<Bytes>(1);
// Build the writer
tokio::task::spawn(async move {
let sink = PollSender::new(tx).sink_map_err(|_| Error::from(ErrorKind::BrokenPipe));
let mut writer = SinkWriter::new(CopyToBytes::new(sink));
let q = quick_xml::writer::Writer::new_with_indent(&mut writer, b' ', 4);
let ns_to_apply = vec![
("xmlns:D".into(), "DAV:".into()),
("xmlns:C".into(), "urn:ietf:params:xml:ns:caldav".into()),
];
let mut qwriter = dxml::Writer { q, ns_to_apply };
let decl =
quick_xml::events::BytesDecl::from_start(quick_xml::events::BytesStart::from_content(
"xml version=\"1.0\" encoding=\"utf-8\"",
0,
));
match qwriter
.q
.write_event_async(quick_xml::events::Event::Decl(decl))
.await
{
Ok(_) => (),
Err(e) => tracing::error!(err=?e, "unable to write XML declaration <?xml ... >"),
}
match elem.qwrite(&mut qwriter).await {
Ok(_) => tracing::debug!("fully serialized object"),
Err(e) => tracing::error!(err=?e, "failed to serialize object"),
}
});
// Build the reader
let recv = tokio_stream::wrappers::ReceiverStream::new(rx);
let stream = StreamBody::new(recv.map(|v| Ok(Frame::data(v))));
let boxed_body = UnsyncBoxBody::new(stream);
let response = Response::builder()
.status(status_ok)
.header("content-type", "application/xml; charset=\"utf-8\"")
.body(boxed_body)?;
Ok(response)
}
/// Deserialize a request body to an XML request
pub(crate) async fn deserialize<T: dxml::Node<T>>(req: Request<Incoming>) -> Result<T> {
let stream_of_frames = BodyStream::new(req.into_body());
let stream_of_bytes = stream_of_frames
.map_ok(|frame| frame.into_data())
.map(|obj| match obj {
Ok(Ok(v)) => Ok(v),
Ok(Err(_)) => Err(std::io::Error::new(
std::io::ErrorKind::Other,
"conversion error",
)),
Err(err) => Err(std::io::Error::new(std::io::ErrorKind::Other, err)),
});
let async_read = tokio_util::io::StreamReader::new(stream_of_bytes);
let async_read = std::pin::pin!(async_read);
let mut rdr = dxml::Reader::new(quick_xml::reader::NsReader::from_reader(async_read)).await?;
let parsed = rdr.find::<T>().await?;
Ok(parsed)
}

View file

@ -0,0 +1,436 @@
use anyhow::Result;
use futures::stream::{StreamExt, TryStreamExt};
use http_body_util::combinators::UnsyncBoxBody;
use http_body_util::BodyStream;
use http_body_util::StreamBody;
use hyper::body::Frame;
use hyper::body::Incoming;
use hyper::{body::Bytes, Request, Response};
use aero_collections::{davdag::Token, user::User};
use aero_dav::caltypes as cal;
use aero_dav::realization::{self, All};
use aero_dav::synctypes as sync;
use aero_dav::types as dav;
use aero_dav::versioningtypes as vers;
use aero_ical::query::is_component_match;
use crate::dav::codec;
use crate::dav::codec::{depth, deserialize, serialize, text_body};
use crate::dav::node::DavNode;
use crate::dav::resource::{RootNode, BASE_TOKEN_URI};
pub(super) type ArcUser = std::sync::Arc<User>;
pub(super) type HttpResponse = Response<UnsyncBoxBody<Bytes, std::io::Error>>;
const ALLPROP: [dav::PropertyRequest<All>; 10] = [
dav::PropertyRequest::CreationDate,
dav::PropertyRequest::DisplayName,
dav::PropertyRequest::GetContentLanguage,
dav::PropertyRequest::GetContentLength,
dav::PropertyRequest::GetContentType,
dav::PropertyRequest::GetEtag,
dav::PropertyRequest::GetLastModified,
dav::PropertyRequest::LockDiscovery,
dav::PropertyRequest::ResourceType,
dav::PropertyRequest::SupportedLock,
];
pub(crate) struct Controller {
node: Box<dyn DavNode>,
user: std::sync::Arc<User>,
req: Request<Incoming>,
}
impl Controller {
pub(crate) async fn route(
user: std::sync::Arc<User>,
req: Request<Incoming>,
) -> Result<HttpResponse> {
let path = req.uri().path().to_string();
let path_segments: Vec<_> = path.split("/").filter(|s| *s != "").collect();
let method = req.method().as_str().to_uppercase();
let can_create = matches!(method.as_str(), "PUT" | "MKCOL" | "MKCALENDAR");
let node = match (RootNode {}).fetch(&user, &path_segments, can_create).await {
Ok(v) => v,
Err(e) => {
tracing::warn!(err=?e, "dav node fetch failed");
return Ok(Response::builder()
.status(404)
.body(codec::text_body("Resource not found"))?);
}
};
let dav_hdrs = node.dav_header();
let ctrl = Self { node, user, req };
match method.as_str() {
"OPTIONS" => Ok(Response::builder()
.status(200)
.header("DAV", dav_hdrs)
.header("Allow", "HEAD,GET,PUT,OPTIONS,DELETE,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE,LOCK,UNLOCK,MKCALENDAR,REPORT")
.body(codec::text_body(""))?),
"HEAD" => {
tracing::warn!("HEAD might not correctly implemented: should return ETags & co");
Ok(Response::builder()
.status(200)
.body(codec::text_body(""))?)
},
"GET" => ctrl.get().await,
"PUT" => ctrl.put().await,
"DELETE" => ctrl.delete().await,
"PROPFIND" => ctrl.propfind().await,
"REPORT" => ctrl.report().await,
_ => Ok(Response::builder()
.status(501)
.body(codec::text_body("HTTP Method not implemented"))?),
}
}
// --- Per-method functions ---
/// REPORT has been first described in the "Versioning Extension" of WebDAV
/// It allows more complex queries compared to PROPFIND
///
/// Note: current implementation is not generic at all, it is heavily tied to CalDAV.
/// A rewrite would be required to make it more generic (with the extension system that has
/// been introduced in aero-dav)
async fn report(self) -> Result<HttpResponse> {
let status = hyper::StatusCode::from_u16(207)?;
let cal_report = match deserialize::<vers::Report<All>>(self.req).await {
Ok(v) => v,
Err(e) => {
tracing::error!(err=?e, "unable to decode REPORT body");
return Ok(Response::builder()
.status(400)
.body(text_body("Bad request"))?);
}
};
// Internal representation that will handle processed request
let (mut ok_node, mut not_found) = (Vec::new(), Vec::new());
let calprop: Option<cal::CalendarSelector<All>>;
let extension: Option<realization::Multistatus>;
// Extracting request information
match cal_report {
vers::Report::Extension(realization::ReportType::Cal(cal::ReportType::Multiget(m))) => {
// Multiget is really like a propfind where Depth: 0|1|Infinity is replaced by an arbitrary
// list of URLs
// Getting the list of nodes
for h in m.href.into_iter() {
let maybe_collected_node = match Path::new(h.0.as_str()) {
Ok(Path::Abs(p)) => RootNode {}
.fetch(&self.user, p.as_slice(), false)
.await
.or(Err(h)),
Ok(Path::Rel(p)) => self
.node
.fetch(&self.user, p.as_slice(), false)
.await
.or(Err(h)),
Err(_) => Err(h),
};
match maybe_collected_node {
Ok(v) => ok_node.push(v),
Err(h) => not_found.push(h),
};
}
calprop = m.selector;
extension = None;
}
vers::Report::Extension(realization::ReportType::Cal(cal::ReportType::Query(q))) => {
calprop = q.selector;
extension = None;
ok_node = apply_filter(self.node.children(&self.user).await, &q.filter)
.try_collect()
.await?;
}
vers::Report::Extension(realization::ReportType::Sync(sync_col)) => {
calprop = Some(cal::CalendarSelector::Prop(sync_col.prop));
if sync_col.limit.is_some() {
tracing::warn!("limit is not supported, ignoring");
}
if matches!(sync_col.sync_level, sync::SyncLevel::Infinite) {
tracing::debug!("aerogramme calendar collections are not nested");
}
let token = match sync_col.sync_token {
sync::SyncTokenRequest::InitialSync => None,
sync::SyncTokenRequest::IncrementalSync(token_raw) => {
// parse token
if token_raw.len() != BASE_TOKEN_URI.len() + 48 {
anyhow::bail!("invalid token length")
}
let token = token_raw[BASE_TOKEN_URI.len()..]
.parse()
.or(Err(anyhow::anyhow!("can't parse token")))?;
Some(token)
}
};
// do the diff
let new_token: Token;
(new_token, ok_node, not_found) = match self.node.diff(token).await {
Ok(t) => t,
Err(e) => match e.kind() {
std::io::ErrorKind::NotFound => return Ok(Response::builder()
.status(410)
.body(text_body("Diff failed, token might be expired"))?),
_ => return Ok(Response::builder()
.status(500)
.body(text_body("Server error, maybe this operation is not supported on this collection"))?),
},
};
extension = Some(realization::Multistatus::Sync(sync::Multistatus {
sync_token: sync::SyncToken(format!("{}{}", BASE_TOKEN_URI, new_token)),
}));
}
_ => {
return Ok(Response::builder()
.status(501)
.body(text_body("Not implemented"))?)
}
};
// Getting props
let props = match calprop {
None | Some(cal::CalendarSelector::AllProp) => Some(dav::PropName(ALLPROP.to_vec())),
Some(cal::CalendarSelector::PropName) => None,
Some(cal::CalendarSelector::Prop(inner)) => Some(inner),
};
serialize(
status,
Self::multistatus(&self.user, ok_node, not_found, props, extension).await,
)
}
/// PROPFIND is the standard way to fetch WebDAV properties
async fn propfind(self) -> Result<HttpResponse> {
let depth = depth(&self.req);
if matches!(depth, dav::Depth::Infinity) {
return Ok(Response::builder()
.status(501)
.body(text_body("Depth: Infinity not implemented"))?);
}
let status = hyper::StatusCode::from_u16(207)?;
// A client may choose not to submit a request body. An empty PROPFIND
// request body MUST be treated as if it were an 'allprop' request.
// @FIXME here we handle any invalid data as an allprop, an empty request is thus correctly
// handled, but corrupted requests are also silently handled as allprop.
let propfind = deserialize::<dav::PropFind<All>>(self.req)
.await
.unwrap_or_else(|_| dav::PropFind::<All>::AllProp(None));
tracing::debug!(recv=?propfind, "inferred propfind request");
// Collect nodes as PROPFIND is not limited to the targeted node
let mut nodes = vec![];
if matches!(depth, dav::Depth::One | dav::Depth::Infinity) {
nodes.extend(self.node.children(&self.user).await);
}
nodes.push(self.node);
// Expand properties request
let propname = match propfind {
dav::PropFind::PropName => None,
dav::PropFind::AllProp(None) => Some(dav::PropName(ALLPROP.to_vec())),
dav::PropFind::AllProp(Some(dav::Include(mut include))) => {
include.extend_from_slice(&ALLPROP);
Some(dav::PropName(include))
}
dav::PropFind::Prop(inner) => Some(inner),
};
// Not Found is currently impossible considering the way we designed this function
let not_found = vec![];
serialize(
status,
Self::multistatus(&self.user, nodes, not_found, propname, None).await,
)
}
async fn put(self) -> Result<HttpResponse> {
let put_policy = codec::put_policy(&self.req)?;
let stream_of_frames = BodyStream::new(self.req.into_body());
let stream_of_bytes = stream_of_frames
.map_ok(|frame| frame.into_data())
.map(|obj| match obj {
Ok(Ok(v)) => Ok(v),
Ok(Err(_)) => Err(std::io::Error::new(
std::io::ErrorKind::Other,
"conversion error",
)),
Err(err) => Err(std::io::Error::new(std::io::ErrorKind::Other, err)),
})
.boxed();
let etag = match self.node.put(put_policy, stream_of_bytes).await {
Ok(etag) => etag,
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
tracing::warn!("put pre-condition failed");
let response = Response::builder().status(412).body(text_body(""))?;
return Ok(response);
}
Err(e) => Err(e)?,
};
let response = Response::builder()
.status(201)
.header("ETag", etag)
//.header("content-type", "application/xml; charset=\"utf-8\"")
.body(text_body(""))?;
Ok(response)
}
async fn get(self) -> Result<HttpResponse> {
let stream_body = StreamBody::new(self.node.content().map_ok(|v| Frame::data(v)));
let boxed_body = UnsyncBoxBody::new(stream_body);
let mut builder = Response::builder().status(200);
builder = builder.header("content-type", self.node.content_type());
if let Some(etag) = self.node.etag().await {
builder = builder.header("etag", etag);
}
let response = builder.body(boxed_body)?;
Ok(response)
}
async fn delete(self) -> Result<HttpResponse> {
self.node.delete().await?;
let response = Response::builder()
.status(204)
//.header("content-type", "application/xml; charset=\"utf-8\"")
.body(text_body(""))?;
Ok(response)
}
// --- Common utility functions ---
/// Build a multistatus response from a list of DavNodes
async fn multistatus(
user: &ArcUser,
nodes: Vec<Box<dyn DavNode>>,
not_found: Vec<dav::Href>,
props: Option<dav::PropName<All>>,
extension: Option<realization::Multistatus>,
) -> dav::Multistatus<All> {
// Collect properties on existing objects
let mut responses: Vec<dav::Response<All>> = match props {
Some(props) => {
futures::stream::iter(nodes)
.then(|n| n.response_props(user, props.clone()))
.collect()
.await
}
None => nodes
.into_iter()
.map(|n| n.response_propname(user))
.collect(),
};
// Register not found objects only if relevant
if !not_found.is_empty() {
responses.push(dav::Response {
status_or_propstat: dav::StatusOrPropstat::Status(
not_found,
dav::Status(hyper::StatusCode::NOT_FOUND),
),
error: None,
location: None,
responsedescription: None,
});
}
// Build response
let multistatus = dav::Multistatus::<All> {
responses,
responsedescription: None,
extension,
};
tracing::debug!(multistatus=?multistatus, "multistatus response");
multistatus
}
}
/// Path is a voluntarily feature limited
/// compared to the expressiveness of a UNIX path
/// For example getting parent with ../ is not supported, scheme is not supported, etc.
/// More complex support could be added later if needed by clients
enum Path<'a> {
Abs(Vec<&'a str>),
Rel(Vec<&'a str>),
}
impl<'a> Path<'a> {
fn new(path: &'a str) -> Result<Self> {
// This check is naive, it does not aim at detecting all fully qualified
// URL or protect from any attack, its only goal is to help debugging.
if path.starts_with("http://") || path.starts_with("https://") {
anyhow::bail!("Full URL are not supported")
}
let path_segments: Vec<_> = path.split("/").filter(|s| *s != "" && *s != ".").collect();
if path.starts_with("/") {
return Ok(Path::Abs(path_segments));
}
Ok(Path::Rel(path_segments))
}
}
//@FIXME naive implementation, must be refactored later
use futures::stream::Stream;
fn apply_filter<'a>(
nodes: Vec<Box<dyn DavNode>>,
filter: &'a cal::Filter,
) -> impl Stream<Item = std::result::Result<Box<dyn DavNode>, std::io::Error>> + 'a {
futures::stream::iter(nodes).filter_map(move |single_node| async move {
// Get ICS
let chunks: Vec<_> = match single_node.content().try_collect().await {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
let raw_ics = chunks.iter().fold(String::new(), |mut acc, single_chunk| {
let str_fragment = std::str::from_utf8(single_chunk.as_ref());
acc.extend(str_fragment);
acc
});
// Parse ICS
let ics = match icalendar::parser::read_calendar(&raw_ics) {
Ok(v) => v,
Err(e) => {
tracing::warn!(err=?e, "Unable to parse ICS in calendar-query");
return Some(Err(std::io::Error::from(std::io::ErrorKind::InvalidData)));
}
};
// Do checks
// @FIXME: icalendar does not consider VCALENDAR as a component
// but WebDAV does...
// Build a fake VCALENDAR component for icalendar compatibility, it's a hack
let root_filter = &filter.0;
let fake_vcal_component = icalendar::parser::Component {
name: cal::Component::VCalendar.as_str().into(),
properties: ics.properties,
components: ics.components,
};
tracing::debug!(filter=?root_filter, "calendar-query filter");
// Adjust return value according to filter
match is_component_match(
&fake_vcal_component,
&[fake_vcal_component.clone()],
root_filter,
) {
true => Some(Ok(single_node)),
_ => None,
}
})
}

View file

@ -0,0 +1,70 @@
use anyhow::{anyhow, Result};
use base64::Engine;
use hyper::body::Incoming;
use hyper::{Request, Response};
use aero_collections::user::User;
use aero_user::login::ArcLoginProvider;
use super::codec::text_body;
use super::controller::HttpResponse;
type ArcUser = std::sync::Arc<User>;
pub(super) async fn auth<'a>(
login: ArcLoginProvider,
req: Request<Incoming>,
next: impl Fn(ArcUser, Request<Incoming>) -> futures::future::BoxFuture<'a, Result<HttpResponse>>,
) -> Result<HttpResponse> {
let auth_val = match req.headers().get(hyper::header::AUTHORIZATION) {
Some(hv) => hv.to_str()?,
None => {
tracing::info!("Missing authorization field");
return Ok(Response::builder()
.status(401)
.header("WWW-Authenticate", "Basic realm=\"Aerogramme\"")
.body(text_body("Missing Authorization field"))?);
}
};
let b64_creds_maybe_padded = match auth_val.split_once(" ") {
Some(("Basic", b64)) => b64,
_ => {
tracing::info!("Unsupported authorization field");
return Ok(Response::builder()
.status(400)
.body(text_body("Unsupported Authorization field"))?);
}
};
// base64urlencoded may have trailing equals, base64urlsafe has not
// theoretically authorization is padded but "be liberal in what you accept"
let b64_creds_clean = b64_creds_maybe_padded.trim_end_matches('=');
// Decode base64
let creds = base64::engine::general_purpose::STANDARD_NO_PAD.decode(b64_creds_clean)?;
let str_creds = std::str::from_utf8(&creds)?;
// Split username and password
let (username, password) = str_creds.split_once(':').ok_or(anyhow!(
"Missing colon in Authorization, can't split decoded value into a username/password pair"
))?;
// Call login provider
let creds = match login.login(username, password).await {
Ok(c) => c,
Err(_) => {
tracing::info!(user = username, "Wrong credentials");
return Ok(Response::builder()
.status(401)
.header("WWW-Authenticate", "Basic realm=\"Aerogramme\"")
.body(text_body("Wrong credentials"))?);
}
};
// Build a user
let user = User::new(username.into(), creds).await?;
// Call router with user
next(user, req).await
}

195
aero-proto/src/dav/mod.rs Normal file
View file

@ -0,0 +1,195 @@
mod codec;
mod controller;
mod middleware;
mod node;
mod resource;
use std::net::SocketAddr;
use std::sync::Arc;
use anyhow::Result;
use futures::future::FutureExt;
use futures::stream::{FuturesUnordered, StreamExt};
use hyper::rt::{Read, Write};
use hyper::server::conn::http1 as http;
use hyper::service::service_fn;
use hyper::{Request, Response};
use hyper_util::rt::TokioIo;
use rustls_pemfile::{certs, private_key};
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::net::TcpListener;
use tokio::net::TcpStream;
use tokio::sync::watch;
use tokio_rustls::TlsAcceptor;
use aero_user::config::{DavConfig, DavUnsecureConfig};
use aero_user::login::ArcLoginProvider;
use crate::dav::controller::Controller;
pub struct Server {
bind_addr: SocketAddr,
login_provider: ArcLoginProvider,
tls: Option<TlsAcceptor>,
}
pub fn new_unsecure(config: DavUnsecureConfig, login: ArcLoginProvider) -> Server {
Server {
bind_addr: config.bind_addr,
login_provider: login,
tls: None,
}
}
pub fn new(config: DavConfig, login: ArcLoginProvider) -> Result<Server> {
let loaded_certs = certs(&mut std::io::BufReader::new(std::fs::File::open(
config.certs,
)?))
.collect::<Result<Vec<_>, _>>()?;
let loaded_key = private_key(&mut std::io::BufReader::new(std::fs::File::open(
config.key,
)?))?
.unwrap();
let tls_config = rustls::ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(loaded_certs, loaded_key)?;
let acceptor = TlsAcceptor::from(Arc::new(tls_config));
Ok(Server {
bind_addr: config.bind_addr,
login_provider: login,
tls: Some(acceptor),
})
}
trait Stream: Read + Write + Send + Unpin {}
impl<T: Unpin + AsyncRead + AsyncWrite + Send> Stream for TokioIo<T> {}
impl Server {
pub async fn run(self: Self, mut must_exit: watch::Receiver<bool>) -> Result<()> {
let tcp = TcpListener::bind(self.bind_addr).await?;
tracing::info!("DAV server listening on {:#}", self.bind_addr);
let mut connections = FuturesUnordered::new();
while !*must_exit.borrow() {
let wait_conn_finished = async {
if connections.is_empty() {
futures::future::pending().await
} else {
connections.next().await
}
};
let (socket, remote_addr) = tokio::select! {
a = tcp.accept() => a?,
_ = wait_conn_finished => continue,
_ = must_exit.changed() => continue,
};
tracing::info!("Accepted connection from {}", remote_addr);
let stream = match self.build_stream(socket).await {
Ok(v) => v,
Err(e) => {
tracing::error!(err=?e, "TLS acceptor failed");
continue;
}
};
let login = self.login_provider.clone();
let conn = tokio::spawn(async move {
//@FIXME should create a generic "public web" server on which "routers" could be
//abitrarily bound
//@FIXME replace with a handler supporting http2
match http::Builder::new()
.serve_connection(
stream,
service_fn(|req: Request<hyper::body::Incoming>| {
let login = login.clone();
tracing::info!("{:?} {:?}", req.method(), req.uri());
tracing::debug!(req=?req, "full request");
async {
let response = match middleware::auth(login, req, |user, request| {
async { Controller::route(user, request).await }.boxed()
})
.await
{
Ok(v) => Ok(v),
Err(e) => {
tracing::error!(err=?e, "internal error");
Response::builder()
.status(500)
.body(codec::text_body("Internal error"))
}
};
tracing::debug!(resp=?response, "full response");
response
}
}),
)
.await
{
Err(e) => tracing::warn!(err=?e, "connection failed"),
Ok(()) => tracing::trace!("connection terminated with success"),
}
});
connections.push(conn);
}
drop(tcp);
tracing::info!("Server shutting down, draining remaining connections...");
while connections.next().await.is_some() {}
Ok(())
}
async fn build_stream(&self, socket: TcpStream) -> Result<Box<dyn Stream>> {
match self.tls.clone() {
Some(acceptor) => {
let stream = acceptor.accept(socket).await?;
Ok(Box::new(TokioIo::new(stream)))
}
None => Ok(Box::new(TokioIo::new(socket))),
}
}
}
// <D:propfind xmlns:D='DAV:' xmlns:A='http://apple.com/ns/ical/'>
// <D:prop>
// <D:getcontenttype/>
// <D:resourcetype/>
// <D:displayname/>
// <A:calendar-color/>
// </D:prop>
// </D:propfind>
// <D:propfind xmlns:D='DAV:' xmlns:A='http://apple.com/ns/ical/' xmlns:C='urn:ietf:params:xml:ns:caldav'>
// <D:prop>
// <D:resourcetype/>
// <D:owner/>
// <D:displayname/>
// <D:current-user-principal/>
// <D:current-user-privilege-set/>
// <A:calendar-color/>
// <C:calendar-home-set/>
// </D:prop>
// </D:propfind>
// <D:propfind xmlns:D='DAV:' xmlns:C='urn:ietf:params:xml:ns:caldav' xmlns:CS='http://calendarserver.org/ns/'>
// <D:prop>
// <D:resourcetype/>
// <D:owner/>
// <D:current-user-principal/>
// <D:current-user-privilege-set/>
// <D:supported-report-set/>
// <C:supported-calendar-component-set/>
// <CS:getctag/>
// </D:prop>
// </D:propfind>
// <C:calendar-multiget xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
// <D:prop>
// <D:getetag/>
// <C:calendar-data/>
// </D:prop>
// <D:href>/alice/calendar/personal/something.ics</D:href>
// </C:calendar-multiget>

145
aero-proto/src/dav/node.rs Normal file
View file

@ -0,0 +1,145 @@
use anyhow::Result;
use futures::future::{BoxFuture, FutureExt};
use futures::stream::{BoxStream, StreamExt};
use hyper::body::Bytes;
use aero_collections::davdag::{Etag, Token};
use aero_dav::realization::All;
use aero_dav::types as dav;
use super::controller::ArcUser;
pub(crate) type Content<'a> = BoxStream<'a, std::result::Result<Bytes, std::io::Error>>;
pub(crate) type PropertyStream<'a> =
BoxStream<'a, std::result::Result<dav::Property<All>, dav::PropertyRequest<All>>>;
pub(crate) enum PutPolicy {
OverwriteAll,
CreateOnly,
ReplaceEtag(String),
}
/// A DAV node should implement the following methods
/// @FIXME not satisfied by BoxFutures but I have no better idea currently
pub(crate) trait DavNode: Send {
// recurence, filesystem hierarchy
/// This node direct children
fn children<'a>(&self, user: &'a ArcUser) -> BoxFuture<'a, Vec<Box<dyn DavNode>>>;
/// Recursively fetch a child (progress inside the filesystem hierarchy)
fn fetch<'a>(
&self,
user: &'a ArcUser,
path: &'a [&str],
create: bool,
) -> BoxFuture<'a, Result<Box<dyn DavNode>>>;
// node properties
/// Get the path
fn path(&self, user: &ArcUser) -> String;
/// Get the supported WebDAV properties
fn supported_properties(&self, user: &ArcUser) -> dav::PropName<All>;
/// Get the values for the given properties
fn properties(&self, user: &ArcUser, prop: dav::PropName<All>) -> PropertyStream<'static>;
/// Get the value of the DAV header to return
fn dav_header(&self) -> String;
/// Put an element (create or update)
fn put<'a>(
&'a self,
policy: PutPolicy,
stream: Content<'a>,
) -> BoxFuture<'a, std::result::Result<Etag, std::io::Error>>;
/// Content type of the element
fn content_type(&self) -> &str;
/// Get ETag
fn etag(&self) -> BoxFuture<Option<Etag>>;
/// Get content
fn content<'a>(&self) -> Content<'a>;
/// Delete
fn delete(&self) -> BoxFuture<std::result::Result<(), std::io::Error>>;
/// Sync
fn diff<'a>(
&self,
sync_token: Option<Token>,
) -> BoxFuture<
'a,
std::result::Result<(Token, Vec<Box<dyn DavNode>>, Vec<dav::Href>), std::io::Error>,
>;
/// Utility function to get a propname response from a node
fn response_propname(&self, user: &ArcUser) -> dav::Response<All> {
dav::Response {
status_or_propstat: dav::StatusOrPropstat::PropStat(
dav::Href(self.path(user)),
vec![dav::PropStat {
status: dav::Status(hyper::StatusCode::OK),
prop: dav::AnyProp(
self.supported_properties(user)
.0
.into_iter()
.map(dav::AnyProperty::Request)
.collect(),
),
error: None,
responsedescription: None,
}],
),
error: None,
location: None,
responsedescription: None,
}
}
/// Utility function to get a prop response from a node & a list of propname
fn response_props(
&self,
user: &ArcUser,
props: dav::PropName<All>,
) -> BoxFuture<'static, dav::Response<All>> {
//@FIXME we should make the DAV parsed object a stream...
let mut result_stream = self.properties(user, props);
let path = self.path(user);
async move {
let mut prop_desc = vec![];
let (mut found, mut not_found) = (vec![], vec![]);
while let Some(maybe_prop) = result_stream.next().await {
match maybe_prop {
Ok(v) => found.push(dav::AnyProperty::Value(v)),
Err(v) => not_found.push(dav::AnyProperty::Request(v)),
}
}
// If at least one property has been found on this object, adding a HTTP 200 propstat to
// the response
if !found.is_empty() {
prop_desc.push(dav::PropStat {
status: dav::Status(hyper::StatusCode::OK),
prop: dav::AnyProp(found),
error: None,
responsedescription: None,
});
}
// If at least one property can't be found on this object, adding a HTTP 404 propstat to
// the response
if !not_found.is_empty() {
prop_desc.push(dav::PropStat {
status: dav::Status(hyper::StatusCode::NOT_FOUND),
prop: dav::AnyProp(not_found),
error: None,
responsedescription: None,
})
}
// Build the finale response
dav::Response {
status_or_propstat: dav::StatusOrPropstat::PropStat(dav::Href(path), prop_desc),
error: None,
location: None,
responsedescription: None,
}
}
.boxed()
}
}

View file

@ -0,0 +1,999 @@
use std::sync::Arc;
type ArcUser = std::sync::Arc<User>;
use anyhow::{anyhow, Result};
use futures::io::AsyncReadExt;
use futures::stream::{StreamExt, TryStreamExt};
use futures::{future::BoxFuture, future::FutureExt};
use aero_collections::{
calendar::Calendar,
davdag::{BlobId, Etag, SyncChange, Token},
user::User,
};
use aero_dav::acltypes as acl;
use aero_dav::caltypes as cal;
use aero_dav::realization::{self as all, All};
use aero_dav::synctypes as sync;
use aero_dav::types as dav;
use aero_dav::versioningtypes as vers;
use super::node::PropertyStream;
use crate::dav::node::{Content, DavNode, PutPolicy};
/// Why "https://aerogramme.0"?
/// Because tokens must be valid URI.
/// And numeric TLD are ~mostly valid in URI (check the .42 TLD experience)
/// and at the same time, they are not used sold by the ICANN and there is no plan to use them.
/// So I am sure that the URL remains invalid, avoiding leaking requests to an hardcoded URL in the
/// future.
/// The best option would be to make it configurable ofc, so someone can put a domain name
/// that they control, it would probably improve compatibility (maybe some WebDAV spec tells us
/// how to handle/resolve this URI but I am not aware of that...). But that's not the plan for
/// now. So here we are: https://aerogramme.0.
pub const BASE_TOKEN_URI: &str = "https://aerogramme.0/sync/";
#[derive(Clone)]
pub(crate) struct RootNode {}
impl DavNode for RootNode {
fn fetch<'a>(
&self,
user: &'a ArcUser,
path: &'a [&str],
create: bool,
) -> BoxFuture<'a, Result<Box<dyn DavNode>>> {
if path.len() == 0 {
let this = self.clone();
return async { Ok(Box::new(this) as Box<dyn DavNode>) }.boxed();
}
if path[0] == user.username {
let child = Box::new(HomeNode {});
return child.fetch(user, &path[1..], create);
}
//@NOTE: We can't create a node at this level
async { Err(anyhow!("Not found")) }.boxed()
}
fn children<'a>(&self, user: &'a ArcUser) -> BoxFuture<'a, Vec<Box<dyn DavNode>>> {
async { vec![Box::new(HomeNode {}) as Box<dyn DavNode>] }.boxed()
}
fn path(&self, user: &ArcUser) -> String {
"/".into()
}
fn supported_properties(&self, user: &ArcUser) -> dav::PropName<All> {
dav::PropName(vec![
dav::PropertyRequest::DisplayName,
dav::PropertyRequest::ResourceType,
dav::PropertyRequest::GetContentType,
dav::PropertyRequest::Extension(all::PropertyRequest::Acl(
acl::PropertyRequest::CurrentUserPrincipal,
)),
])
}
fn properties(&self, user: &ArcUser, prop: dav::PropName<All>) -> PropertyStream<'static> {
let user = user.clone();
futures::stream::iter(prop.0)
.map(move |n| {
let prop = match n {
dav::PropertyRequest::DisplayName => {
dav::Property::DisplayName("DAV Root".to_string())
}
dav::PropertyRequest::ResourceType => {
dav::Property::ResourceType(vec![dav::ResourceType::Collection])
}
dav::PropertyRequest::GetContentType => {
dav::Property::GetContentType("httpd/unix-directory".into())
}
dav::PropertyRequest::Extension(all::PropertyRequest::Acl(
acl::PropertyRequest::CurrentUserPrincipal,
)) => dav::Property::Extension(all::Property::Acl(
acl::Property::CurrentUserPrincipal(acl::User::Authenticated(dav::Href(
HomeNode {}.path(&user),
))),
)),
v => return Err(v),
};
Ok(prop)
})
.boxed()
}
fn put<'a>(
&'a self,
_policy: PutPolicy,
stream: Content<'a>,
) -> BoxFuture<'a, std::result::Result<Etag, std::io::Error>> {
futures::future::err(std::io::Error::from(std::io::ErrorKind::Unsupported)).boxed()
}
fn content<'a>(&self) -> Content<'a> {
futures::stream::once(futures::future::err(std::io::Error::from(
std::io::ErrorKind::Unsupported,
)))
.boxed()
}
fn content_type(&self) -> &str {
"text/plain"
}
fn etag(&self) -> BoxFuture<Option<Etag>> {
async { None }.boxed()
}
fn delete(&self) -> BoxFuture<std::result::Result<(), std::io::Error>> {
async { Err(std::io::Error::from(std::io::ErrorKind::PermissionDenied)) }.boxed()
}
fn diff<'a>(
&self,
_sync_token: Option<Token>,
) -> BoxFuture<
'a,
std::result::Result<(Token, Vec<Box<dyn DavNode>>, Vec<dav::Href>), std::io::Error>,
> {
async { Err(std::io::Error::from(std::io::ErrorKind::Unsupported)) }.boxed()
}
fn dav_header(&self) -> String {
"1".into()
}
}
#[derive(Clone)]
pub(crate) struct HomeNode {}
impl DavNode for HomeNode {
fn fetch<'a>(
&self,
user: &'a ArcUser,
path: &'a [&str],
create: bool,
) -> BoxFuture<'a, Result<Box<dyn DavNode>>> {
if path.len() == 0 {
let node = Box::new(self.clone()) as Box<dyn DavNode>;
return async { Ok(node) }.boxed();
}
if path[0] == "calendar" {
return async move {
let child = Box::new(CalendarListNode::new(user).await?);
child.fetch(user, &path[1..], create).await
}
.boxed();
}
//@NOTE: we can't create a node at this level
async { Err(anyhow!("Not found")) }.boxed()
}
fn children<'a>(&self, user: &'a ArcUser) -> BoxFuture<'a, Vec<Box<dyn DavNode>>> {
async {
CalendarListNode::new(user)
.await
.map(|c| vec![Box::new(c) as Box<dyn DavNode>])
.unwrap_or(vec![])
}
.boxed()
}
fn path(&self, user: &ArcUser) -> String {
format!("/{}/", user.username)
}
fn supported_properties(&self, user: &ArcUser) -> dav::PropName<All> {
dav::PropName(vec![
dav::PropertyRequest::DisplayName,
dav::PropertyRequest::ResourceType,
dav::PropertyRequest::GetContentType,
dav::PropertyRequest::Extension(all::PropertyRequest::Cal(
cal::PropertyRequest::CalendarHomeSet,
)),
])
}
fn properties(&self, user: &ArcUser, prop: dav::PropName<All>) -> PropertyStream<'static> {
let user = user.clone();
futures::stream::iter(prop.0)
.map(move |n| {
let prop = match n {
dav::PropertyRequest::DisplayName => {
dav::Property::DisplayName(format!("{} home", user.username))
}
dav::PropertyRequest::ResourceType => dav::Property::ResourceType(vec![
dav::ResourceType::Collection,
dav::ResourceType::Extension(all::ResourceType::Acl(
acl::ResourceType::Principal,
)),
]),
dav::PropertyRequest::GetContentType => {
dav::Property::GetContentType("httpd/unix-directory".into())
}
dav::PropertyRequest::Extension(all::PropertyRequest::Cal(
cal::PropertyRequest::CalendarHomeSet,
)) => dav::Property::Extension(all::Property::Cal(
cal::Property::CalendarHomeSet(dav::Href(
//@FIXME we are hardcoding the calendar path, instead we would want to use
//objects
format!("/{}/calendar/", user.username),
)),
)),
v => return Err(v),
};
Ok(prop)
})
.boxed()
}
fn put<'a>(
&'a self,
_policy: PutPolicy,
stream: Content<'a>,
) -> BoxFuture<'a, std::result::Result<Etag, std::io::Error>> {
futures::future::err(std::io::Error::from(std::io::ErrorKind::Unsupported)).boxed()
}
fn content<'a>(&self) -> Content<'a> {
futures::stream::once(futures::future::err(std::io::Error::from(
std::io::ErrorKind::Unsupported,
)))
.boxed()
}
fn content_type(&self) -> &str {
"text/plain"
}
fn etag(&self) -> BoxFuture<Option<Etag>> {
async { None }.boxed()
}
fn delete(&self) -> BoxFuture<std::result::Result<(), std::io::Error>> {
async { Err(std::io::Error::from(std::io::ErrorKind::PermissionDenied)) }.boxed()
}
fn diff<'a>(
&self,
_sync_token: Option<Token>,
) -> BoxFuture<
'a,
std::result::Result<(Token, Vec<Box<dyn DavNode>>, Vec<dav::Href>), std::io::Error>,
> {
async { Err(std::io::Error::from(std::io::ErrorKind::Unsupported)) }.boxed()
}
fn dav_header(&self) -> String {
"1, access-control, calendar-access".into()
}
}
#[derive(Clone)]
pub(crate) struct CalendarListNode {
list: Vec<String>,
}
impl CalendarListNode {
async fn new(user: &ArcUser) -> Result<Self> {
let list = user.calendars.list(user).await?;
Ok(Self { list })
}
}
impl DavNode for CalendarListNode {
fn fetch<'a>(
&self,
user: &'a ArcUser,
path: &'a [&str],
create: bool,
) -> BoxFuture<'a, Result<Box<dyn DavNode>>> {
if path.len() == 0 {
let node = Box::new(self.clone()) as Box<dyn DavNode>;
return async { Ok(node) }.boxed();
}
async move {
//@FIXME: we should create a node if the open returns a "not found".
let cal = user
.calendars
.open(user, path[0])
.await?
.ok_or(anyhow!("Not found"))?;
let child = Box::new(CalendarNode {
col: cal,
calname: path[0].to_string(),
});
child.fetch(user, &path[1..], create).await
}
.boxed()
}
fn children<'a>(&self, user: &'a ArcUser) -> BoxFuture<'a, Vec<Box<dyn DavNode>>> {
let list = self.list.clone();
async move {
//@FIXME maybe we want to be lazy here?!
futures::stream::iter(list.iter())
.filter_map(|name| async move {
user.calendars
.open(user, name)
.await
.ok()
.flatten()
.map(|v| (name, v))
})
.map(|(name, cal)| {
Box::new(CalendarNode {
col: cal,
calname: name.to_string(),
}) as Box<dyn DavNode>
})
.collect::<Vec<Box<dyn DavNode>>>()
.await
}
.boxed()
}
fn path(&self, user: &ArcUser) -> String {
format!("/{}/calendar/", user.username)
}
fn supported_properties(&self, user: &ArcUser) -> dav::PropName<All> {
dav::PropName(vec![
dav::PropertyRequest::DisplayName,
dav::PropertyRequest::ResourceType,
dav::PropertyRequest::GetContentType,
])
}
fn properties(&self, user: &ArcUser, prop: dav::PropName<All>) -> PropertyStream<'static> {
let user = user.clone();
futures::stream::iter(prop.0)
.map(move |n| {
let prop = match n {
dav::PropertyRequest::DisplayName => {
dav::Property::DisplayName(format!("{} calendars", user.username))
}
dav::PropertyRequest::ResourceType => {
dav::Property::ResourceType(vec![dav::ResourceType::Collection])
}
dav::PropertyRequest::GetContentType => {
dav::Property::GetContentType("httpd/unix-directory".into())
}
v => return Err(v),
};
Ok(prop)
})
.boxed()
}
fn put<'a>(
&'a self,
_policy: PutPolicy,
stream: Content<'a>,
) -> BoxFuture<'a, std::result::Result<Etag, std::io::Error>> {
futures::future::err(std::io::Error::from(std::io::ErrorKind::Unsupported)).boxed()
}
fn content<'a>(&self) -> Content<'a> {
futures::stream::once(futures::future::err(std::io::Error::from(
std::io::ErrorKind::Unsupported,
)))
.boxed()
}
fn content_type(&self) -> &str {
"text/plain"
}
fn etag(&self) -> BoxFuture<Option<Etag>> {
async { None }.boxed()
}
fn delete(&self) -> BoxFuture<std::result::Result<(), std::io::Error>> {
async { Err(std::io::Error::from(std::io::ErrorKind::PermissionDenied)) }.boxed()
}
fn diff<'a>(
&self,
_sync_token: Option<Token>,
) -> BoxFuture<
'a,
std::result::Result<(Token, Vec<Box<dyn DavNode>>, Vec<dav::Href>), std::io::Error>,
> {
async { Err(std::io::Error::from(std::io::ErrorKind::Unsupported)) }.boxed()
}
fn dav_header(&self) -> String {
"1, access-control, calendar-access".into()
}
}
#[derive(Clone)]
pub(crate) struct CalendarNode {
col: Arc<Calendar>,
calname: String,
}
impl DavNode for CalendarNode {
fn fetch<'a>(
&self,
user: &'a ArcUser,
path: &'a [&str],
create: bool,
) -> BoxFuture<'a, Result<Box<dyn DavNode>>> {
if path.len() == 0 {
let node = Box::new(self.clone()) as Box<dyn DavNode>;
return async { Ok(node) }.boxed();
}
let col = self.col.clone();
let calname = self.calname.clone();
async move {
match (col.dag().await.idx_by_filename.get(path[0]), create) {
(Some(blob_id), _) => {
let child = Box::new(EventNode {
col: col.clone(),
calname,
filename: path[0].to_string(),
blob_id: *blob_id,
});
child.fetch(user, &path[1..], create).await
}
(None, true) => {
let child = Box::new(CreateEventNode {
col: col.clone(),
calname,
filename: path[0].to_string(),
});
child.fetch(user, &path[1..], create).await
}
_ => Err(anyhow!("Not found")),
}
}
.boxed()
}
fn children<'a>(&self, user: &'a ArcUser) -> BoxFuture<'a, Vec<Box<dyn DavNode>>> {
let col = self.col.clone();
let calname = self.calname.clone();
async move {
col.dag()
.await
.idx_by_filename
.iter()
.map(|(filename, blob_id)| {
Box::new(EventNode {
col: col.clone(),
calname: calname.clone(),
filename: filename.to_string(),
blob_id: *blob_id,
}) as Box<dyn DavNode>
})
.collect()
}
.boxed()
}
fn path(&self, user: &ArcUser) -> String {
format!("/{}/calendar/{}/", user.username, self.calname)
}
fn supported_properties(&self, user: &ArcUser) -> dav::PropName<All> {
dav::PropName(vec![
dav::PropertyRequest::DisplayName,
dav::PropertyRequest::ResourceType,
dav::PropertyRequest::GetContentType,
dav::PropertyRequest::Extension(all::PropertyRequest::Cal(
cal::PropertyRequest::SupportedCalendarComponentSet,
)),
dav::PropertyRequest::Extension(all::PropertyRequest::Sync(
sync::PropertyRequest::SyncToken,
)),
dav::PropertyRequest::Extension(all::PropertyRequest::Vers(
vers::PropertyRequest::SupportedReportSet,
)),
])
}
fn properties(&self, _user: &ArcUser, prop: dav::PropName<All>) -> PropertyStream<'static> {
let calname = self.calname.to_string();
let col = self.col.clone();
futures::stream::iter(prop.0)
.then(move |n| {
let calname = calname.clone();
let col = col.clone();
async move {
let prop = match n {
dav::PropertyRequest::DisplayName => {
dav::Property::DisplayName(format!("{} calendar", calname))
}
dav::PropertyRequest::ResourceType => dav::Property::ResourceType(vec![
dav::ResourceType::Collection,
dav::ResourceType::Extension(all::ResourceType::Cal(
cal::ResourceType::Calendar,
)),
]),
//dav::PropertyRequest::GetContentType => dav::AnyProperty::Value(dav::Property::GetContentType("httpd/unix-directory".into())),
//@FIXME seems wrong but seems to be what Thunderbird expects...
dav::PropertyRequest::GetContentType => {
dav::Property::GetContentType("text/calendar".into())
}
dav::PropertyRequest::Extension(all::PropertyRequest::Cal(
cal::PropertyRequest::SupportedCalendarComponentSet,
)) => dav::Property::Extension(all::Property::Cal(
cal::Property::SupportedCalendarComponentSet(vec![
cal::CompSupport(cal::Component::VEvent),
cal::CompSupport(cal::Component::VTodo),
cal::CompSupport(cal::Component::VJournal),
]),
)),
dav::PropertyRequest::Extension(all::PropertyRequest::Sync(
sync::PropertyRequest::SyncToken,
)) => match col.token().await {
Ok(token) => dav::Property::Extension(all::Property::Sync(
sync::Property::SyncToken(sync::SyncToken(format!(
"{}{}",
BASE_TOKEN_URI, token
))),
)),
_ => return Err(n.clone()),
},
dav::PropertyRequest::Extension(all::PropertyRequest::Vers(
vers::PropertyRequest::SupportedReportSet,
)) => dav::Property::Extension(all::Property::Vers(
vers::Property::SupportedReportSet(vec![
vers::SupportedReport(vers::ReportName::Extension(
all::ReportTypeName::Cal(cal::ReportTypeName::Multiget),
)),
vers::SupportedReport(vers::ReportName::Extension(
all::ReportTypeName::Cal(cal::ReportTypeName::Query),
)),
vers::SupportedReport(vers::ReportName::Extension(
all::ReportTypeName::Sync(sync::ReportTypeName::SyncCollection),
)),
]),
)),
v => return Err(v),
};
Ok(prop)
}
})
.boxed()
}
fn put<'a>(
&'a self,
_policy: PutPolicy,
_stream: Content<'a>,
) -> BoxFuture<'a, std::result::Result<Etag, std::io::Error>> {
futures::future::err(std::io::Error::from(std::io::ErrorKind::Unsupported)).boxed()
}
fn content<'a>(&self) -> Content<'a> {
futures::stream::once(futures::future::err(std::io::Error::from(
std::io::ErrorKind::Unsupported,
)))
.boxed()
}
fn content_type(&self) -> &str {
"text/plain"
}
fn etag(&self) -> BoxFuture<Option<Etag>> {
async { None }.boxed()
}
fn delete(&self) -> BoxFuture<std::result::Result<(), std::io::Error>> {
async { Err(std::io::Error::from(std::io::ErrorKind::PermissionDenied)) }.boxed()
}
fn diff<'a>(
&self,
sync_token: Option<Token>,
) -> BoxFuture<
'a,
std::result::Result<(Token, Vec<Box<dyn DavNode>>, Vec<dav::Href>), std::io::Error>,
> {
let col = self.col.clone();
let calname = self.calname.clone();
async move {
let sync_token = match sync_token {
Some(v) => v,
None => {
let token = col
.token()
.await
.or(Err(std::io::Error::from(std::io::ErrorKind::Interrupted)))?;
let ok_nodes = col
.dag()
.await
.idx_by_filename
.iter()
.map(|(filename, blob_id)| {
Box::new(EventNode {
col: col.clone(),
calname: calname.clone(),
filename: filename.to_string(),
blob_id: *blob_id,
}) as Box<dyn DavNode>
})
.collect();
return Ok((token, ok_nodes, vec![]));
}
};
let (new_token, listed_changes) = match col.diff(sync_token).await {
Ok(v) => v,
Err(e) => {
tracing::info!(err=?e, "token resolution failed, maybe a forgotten token");
return Err(std::io::Error::from(std::io::ErrorKind::NotFound));
}
};
let mut ok_nodes: Vec<Box<dyn DavNode>> = vec![];
let mut rm_nodes: Vec<dav::Href> = vec![];
for change in listed_changes.into_iter() {
match change {
SyncChange::Ok((filename, blob_id)) => {
let child = Box::new(EventNode {
col: col.clone(),
calname: calname.clone(),
filename,
blob_id,
});
ok_nodes.push(child);
}
SyncChange::NotFound(filename) => {
rm_nodes.push(dav::Href(filename));
}
}
}
Ok((new_token, ok_nodes, rm_nodes))
}
.boxed()
}
fn dav_header(&self) -> String {
"1, access-control, calendar-access".into()
}
}
#[derive(Clone)]
pub(crate) struct EventNode {
col: Arc<Calendar>,
calname: String,
filename: String,
blob_id: BlobId,
}
impl DavNode for EventNode {
fn fetch<'a>(
&self,
user: &'a ArcUser,
path: &'a [&str],
create: bool,
) -> BoxFuture<'a, Result<Box<dyn DavNode>>> {
if path.len() == 0 {
let node = Box::new(self.clone()) as Box<dyn DavNode>;
return async { Ok(node) }.boxed();
}
async {
Err(anyhow!(
"Not supported: can't create a child on an event node"
))
}
.boxed()
}
fn children<'a>(&self, user: &'a ArcUser) -> BoxFuture<'a, Vec<Box<dyn DavNode>>> {
async { vec![] }.boxed()
}
fn path(&self, user: &ArcUser) -> String {
format!(
"/{}/calendar/{}/{}",
user.username, self.calname, self.filename
)
}
fn supported_properties(&self, user: &ArcUser) -> dav::PropName<All> {
dav::PropName(vec![
dav::PropertyRequest::DisplayName,
dav::PropertyRequest::ResourceType,
dav::PropertyRequest::GetEtag,
dav::PropertyRequest::Extension(all::PropertyRequest::Cal(
cal::PropertyRequest::CalendarData(cal::CalendarDataRequest::default()),
)),
])
}
fn properties(&self, _user: &ArcUser, prop: dav::PropName<All>) -> PropertyStream<'static> {
let this = self.clone();
futures::stream::iter(prop.0)
.then(move |n| {
let this = this.clone();
async move {
let prop = match &n {
dav::PropertyRequest::DisplayName => {
dav::Property::DisplayName(format!("{} event", this.filename))
}
dav::PropertyRequest::ResourceType => dav::Property::ResourceType(vec![]),
dav::PropertyRequest::GetContentType => {
dav::Property::GetContentType("text/calendar".into())
}
dav::PropertyRequest::GetEtag => {
let etag = this.etag().await.ok_or(n.clone())?;
dav::Property::GetEtag(etag)
}
dav::PropertyRequest::Extension(all::PropertyRequest::Cal(
cal::PropertyRequest::CalendarData(req),
)) => {
let ics = String::from_utf8(
this.col.get(this.blob_id).await.or(Err(n.clone()))?,
)
.or(Err(n.clone()))?;
let new_ics = match &req.comp {
None => ics,
Some(prune_comp) => {
// parse content
let ics = match icalendar::parser::read_calendar(&ics) {
Ok(v) => v,
Err(e) => {
tracing::warn!(err=?e, "Unable to parse ICS in calendar-query");
return Err(n.clone())
}
};
// build a fake vcal component for caldav compat
let fake_vcal_component = icalendar::parser::Component {
name: cal::Component::VCalendar.as_str().into(),
properties: ics.properties,
components: ics.components,
};
// rebuild component
let new_comp = match aero_ical::prune::component(&fake_vcal_component, prune_comp) {
Some(v) => v,
None => return Err(n.clone()),
};
// reserialize
format!("{}", icalendar::parser::Calendar { properties: new_comp.properties, components: new_comp.components })
},
};
dav::Property::Extension(all::Property::Cal(
cal::Property::CalendarData(cal::CalendarDataPayload {
mime: None,
payload: new_ics,
}),
))
}
_ => return Err(n),
};
Ok(prop)
}
})
.boxed()
}
fn put<'a>(
&'a self,
policy: PutPolicy,
stream: Content<'a>,
) -> BoxFuture<'a, std::result::Result<Etag, std::io::Error>> {
async {
let existing_etag = self
.etag()
.await
.ok_or(std::io::Error::new(std::io::ErrorKind::Other, "Etag error"))?;
match policy {
PutPolicy::CreateOnly => {
return Err(std::io::Error::from(std::io::ErrorKind::AlreadyExists))
}
PutPolicy::ReplaceEtag(etag) if etag != existing_etag.as_str() => {
return Err(std::io::Error::from(std::io::ErrorKind::AlreadyExists))
}
_ => (),
};
//@FIXME for now, our storage interface does not allow streaming,
// so we load everything in memory
let mut evt = Vec::new();
let mut reader = stream.into_async_read();
reader
.read_to_end(&mut evt)
.await
.or(Err(std::io::Error::from(std::io::ErrorKind::BrokenPipe)))?;
let (_token, entry) = self
.col
.put(self.filename.as_str(), evt.as_ref())
.await
.or(Err(std::io::ErrorKind::Interrupted))?;
self.col
.opportunistic_sync()
.await
.or(Err(std::io::ErrorKind::ConnectionReset))?;
Ok(entry.2)
}
.boxed()
}
fn content<'a>(&self) -> Content<'a> {
//@FIXME for now, our storage interface does not allow streaming,
// so we load everything in memory
let calendar = self.col.clone();
let blob_id = self.blob_id.clone();
let calblob = async move {
let raw_ics = calendar
.get(blob_id)
.await
.or(Err(std::io::Error::from(std::io::ErrorKind::Interrupted)))?;
Ok(hyper::body::Bytes::from(raw_ics))
};
futures::stream::once(Box::pin(calblob)).boxed()
}
fn content_type(&self) -> &str {
"text/calendar"
}
fn etag(&self) -> BoxFuture<Option<Etag>> {
let calendar = self.col.clone();
async move {
calendar
.dag()
.await
.table
.get(&self.blob_id)
.map(|(_, _, etag)| etag.to_string())
}
.boxed()
}
fn delete(&self) -> BoxFuture<std::result::Result<(), std::io::Error>> {
let calendar = self.col.clone();
let blob_id = self.blob_id.clone();
async move {
let _token = match calendar.delete(blob_id).await {
Ok(v) => v,
Err(e) => {
tracing::error!(err=?e, "delete event node");
return Err(std::io::Error::from(std::io::ErrorKind::Interrupted));
}
};
calendar
.opportunistic_sync()
.await
.or(Err(std::io::ErrorKind::ConnectionReset))?;
Ok(())
}
.boxed()
}
fn diff<'a>(
&self,
_sync_token: Option<Token>,
) -> BoxFuture<
'a,
std::result::Result<(Token, Vec<Box<dyn DavNode>>, Vec<dav::Href>), std::io::Error>,
> {
async { Err(std::io::Error::from(std::io::ErrorKind::Unsupported)) }.boxed()
}
fn dav_header(&self) -> String {
"1, access-control".into()
}
}
#[derive(Clone)]
pub(crate) struct CreateEventNode {
col: Arc<Calendar>,
calname: String,
filename: String,
}
impl DavNode for CreateEventNode {
fn fetch<'a>(
&self,
user: &'a ArcUser,
path: &'a [&str],
create: bool,
) -> BoxFuture<'a, Result<Box<dyn DavNode>>> {
if path.len() == 0 {
let node = Box::new(self.clone()) as Box<dyn DavNode>;
return async { Ok(node) }.boxed();
}
async {
Err(anyhow!(
"Not supported: can't create a child on an event node"
))
}
.boxed()
}
fn children<'a>(&self, user: &'a ArcUser) -> BoxFuture<'a, Vec<Box<dyn DavNode>>> {
async { vec![] }.boxed()
}
fn path(&self, user: &ArcUser) -> String {
format!(
"/{}/calendar/{}/{}",
user.username, self.calname, self.filename
)
}
fn supported_properties(&self, user: &ArcUser) -> dav::PropName<All> {
dav::PropName(vec![])
}
fn properties(&self, _user: &ArcUser, prop: dav::PropName<All>) -> PropertyStream<'static> {
futures::stream::iter(vec![]).boxed()
}
fn put<'a>(
&'a self,
_policy: PutPolicy,
stream: Content<'a>,
) -> BoxFuture<'a, std::result::Result<Etag, std::io::Error>> {
//@NOTE: policy might not be needed here: whatever we put, there is no known entries here
async {
//@FIXME for now, our storage interface does not allow for streaming
let mut evt = Vec::new();
let mut reader = stream.into_async_read();
reader.read_to_end(&mut evt).await.unwrap();
let (_token, entry) = self
.col
.put(self.filename.as_str(), evt.as_ref())
.await
.or(Err(std::io::ErrorKind::Interrupted))?;
self.col
.opportunistic_sync()
.await
.or(Err(std::io::ErrorKind::ConnectionReset))?;
Ok(entry.2)
}
.boxed()
}
fn content<'a>(&self) -> Content<'a> {
futures::stream::once(futures::future::err(std::io::Error::from(
std::io::ErrorKind::Unsupported,
)))
.boxed()
}
fn content_type(&self) -> &str {
"text/plain"
}
fn etag(&self) -> BoxFuture<Option<Etag>> {
async { None }.boxed()
}
fn delete(&self) -> BoxFuture<std::result::Result<(), std::io::Error>> {
// Nothing to delete
async { Ok(()) }.boxed()
}
fn diff<'a>(
&self,
_sync_token: Option<Token>,
) -> BoxFuture<
'a,
std::result::Result<(Token, Vec<Box<dyn DavNode>>, Vec<dav::Href>), std::io::Error>,
> {
async { Err(std::io::Error::from(std::io::ErrorKind::Unsupported)) }.boxed()
}
fn dav_header(&self) -> String {
"1, access-control".into()
}
}

View file

@ -0,0 +1,77 @@
use imap_codec::imap_types::command::FetchModifier;
use imap_codec::imap_types::fetch::{MacroOrMessageDataItemNames, MessageDataItemName, Section};
/// Internal decisions based on fetched attributes
/// passed by the client
pub struct AttributesProxy {
pub attrs: Vec<MessageDataItemName<'static>>,
}
impl AttributesProxy {
pub fn new(
attrs: &MacroOrMessageDataItemNames<'static>,
modifiers: &[FetchModifier],
is_uid_fetch: bool,
) -> Self {
// Expand macros
let mut fetch_attrs = match attrs {
MacroOrMessageDataItemNames::Macro(m) => {
use imap_codec::imap_types::fetch::Macro;
use MessageDataItemName::*;
match m {
Macro::All => vec![Flags, InternalDate, Rfc822Size, Envelope],
Macro::Fast => vec![Flags, InternalDate, Rfc822Size],
Macro::Full => vec![Flags, InternalDate, Rfc822Size, Envelope, Body],
_ => {
tracing::error!("unimplemented macro");
vec![]
}
}
}
MacroOrMessageDataItemNames::MessageDataItemNames(a) => a.clone(),
};
// Handle uids
if is_uid_fetch && !fetch_attrs.contains(&MessageDataItemName::Uid) {
fetch_attrs.push(MessageDataItemName::Uid);
}
// Handle inferred MODSEQ tag
let is_changed_since = modifiers
.iter()
.any(|m| matches!(m, FetchModifier::ChangedSince(..)));
if is_changed_since && !fetch_attrs.contains(&MessageDataItemName::ModSeq) {
fetch_attrs.push(MessageDataItemName::ModSeq);
}
Self { attrs: fetch_attrs }
}
pub fn is_enabling_condstore(&self) -> bool {
self.attrs
.iter()
.any(|x| matches!(x, MessageDataItemName::ModSeq))
}
pub fn need_body(&self) -> bool {
self.attrs.iter().any(|x| match x {
MessageDataItemName::Body
| MessageDataItemName::Rfc822
| MessageDataItemName::Rfc822Text
| MessageDataItemName::BodyStructure => true,
MessageDataItemName::BodyExt {
section: Some(section),
partial: _,
peek: _,
} => match section {
Section::Header(None)
| Section::HeaderFields(None, _)
| Section::HeaderFieldsNot(None, _) => false,
_ => true,
},
MessageDataItemName::BodyExt { .. } => true,
_ => false,
})
}
}

View file

@ -1,8 +1,11 @@
use imap_codec::imap_types::core::NonEmptyVec; use imap_codec::imap_types::command::{FetchModifier, SelectExamineModifier, StoreModifier};
use imap_codec::imap_types::core::Vec1;
use imap_codec::imap_types::extensions::enable::{CapabilityEnable, Utf8Kind}; use imap_codec::imap_types::extensions::enable::{CapabilityEnable, Utf8Kind};
use imap_codec::imap_types::response::Capability; use imap_codec::imap_types::response::Capability;
use std::collections::HashSet; use std::collections::HashSet;
use crate::imap::attributes::AttributesProxy;
fn capability_unselect() -> Capability<'static> { fn capability_unselect() -> Capability<'static> {
Capability::try_from("UNSELECT").unwrap() Capability::try_from("UNSELECT").unwrap()
} }
@ -11,9 +14,19 @@ fn capability_condstore() -> Capability<'static> {
Capability::try_from("CONDSTORE").unwrap() Capability::try_from("CONDSTORE").unwrap()
} }
fn capability_uidplus() -> Capability<'static> {
Capability::try_from("UIDPLUS").unwrap()
}
fn capability_liststatus() -> Capability<'static> {
Capability::try_from("LIST-STATUS").unwrap()
}
/*
fn capability_qresync() -> Capability<'static> { fn capability_qresync() -> Capability<'static> {
Capability::try_from("QRESYNC").unwrap() Capability::try_from("QRESYNC").unwrap()
} }
*/
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ServerCapability(HashSet<Capability<'static>>); pub struct ServerCapability(HashSet<Capability<'static>>);
@ -25,15 +38,18 @@ impl Default for ServerCapability {
Capability::Enable, Capability::Enable,
Capability::Move, Capability::Move,
Capability::LiteralPlus, Capability::LiteralPlus,
Capability::Idle,
capability_unselect(), capability_unselect(),
//capability_condstore(), capability_condstore(),
capability_uidplus(),
capability_liststatus(),
//capability_qresync(), //capability_qresync(),
])) ]))
} }
} }
impl ServerCapability { impl ServerCapability {
pub fn to_vec(&self) -> NonEmptyVec<Capability<'static>> { pub fn to_vec(&self) -> Vec1<Capability<'static>> {
self.0 self.0
.iter() .iter()
.map(|v| v.clone()) .map(|v| v.clone())
@ -48,15 +64,28 @@ impl ServerCapability {
} }
} }
enum ClientStatus { #[derive(Clone)]
pub enum ClientStatus {
NotSupportedByServer, NotSupportedByServer,
Disabled, Disabled,
Enabled, Enabled,
} }
impl ClientStatus {
pub fn is_enabled(&self) -> bool {
matches!(self, Self::Enabled)
}
pub fn enable(&self) -> Self {
match self {
Self::Disabled => Self::Enabled,
other => other.clone(),
}
}
}
pub struct ClientCapability { pub struct ClientCapability {
condstore: ClientStatus, pub condstore: ClientStatus,
utf8kind: Option<Utf8Kind>, pub utf8kind: Option<Utf8Kind>,
} }
impl ClientCapability { impl ClientCapability {
@ -70,6 +99,42 @@ impl ClientCapability {
} }
} }
pub fn enable_condstore(&mut self) {
self.condstore = self.condstore.enable();
}
pub fn attributes_enable(&mut self, ap: &AttributesProxy) {
if ap.is_enabling_condstore() {
self.enable_condstore()
}
}
pub fn fetch_modifiers_enable(&mut self, mods: &[FetchModifier]) {
if mods
.iter()
.any(|x| matches!(x, FetchModifier::ChangedSince(..)))
{
self.enable_condstore()
}
}
pub fn store_modifiers_enable(&mut self, mods: &[StoreModifier]) {
if mods
.iter()
.any(|x| matches!(x, StoreModifier::UnchangedSince(..)))
{
self.enable_condstore()
}
}
pub fn select_enable(&mut self, mods: &[SelectExamineModifier]) {
for m in mods.iter() {
match m {
SelectExamineModifier::Condstore => self.enable_condstore(),
}
}
}
pub fn try_enable( pub fn try_enable(
&mut self, &mut self,
caps: &[CapabilityEnable<'static>], caps: &[CapabilityEnable<'static>],

View file

@ -4,12 +4,13 @@ use imap_codec::imap_types::core::AString;
use imap_codec::imap_types::response::Code; use imap_codec::imap_types::response::Code;
use imap_codec::imap_types::secret::Secret; use imap_codec::imap_types::secret::Secret;
use aero_collections::user::User;
use aero_user::login::ArcLoginProvider;
use crate::imap::capability::ServerCapability; use crate::imap::capability::ServerCapability;
use crate::imap::command::anystate; use crate::imap::command::anystate;
use crate::imap::flow; use crate::imap::flow;
use crate::imap::response::Response; use crate::imap::response::Response;
use crate::login::ArcLoginProvider;
use crate::mail::user::User;
//--- dispatching //--- dispatching

View file

@ -1,9 +1,12 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::sync::Arc; use std::sync::Arc;
use thiserror::Error;
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, bail, Result};
use imap_codec::imap_types::command::{Command, CommandBody}; use imap_codec::imap_types::command::{
use imap_codec::imap_types::core::{Atom, Literal, NonEmptyVec, QuotedChar}; Command, CommandBody, ListReturnItem, SelectExamineModifier,
};
use imap_codec::imap_types::core::{Atom, Literal, QuotedChar, Vec1};
use imap_codec::imap_types::datetime::DateTime; use imap_codec::imap_types::datetime::DateTime;
use imap_codec::imap_types::extensions::enable::CapabilityEnable; use imap_codec::imap_types::extensions::enable::CapabilityEnable;
use imap_codec::imap_types::flag::{Flag, FlagNameAttribute}; use imap_codec::imap_types::flag::{Flag, FlagNameAttribute};
@ -11,17 +14,17 @@ use imap_codec::imap_types::mailbox::{ListMailbox, Mailbox as MailboxCodec};
use imap_codec::imap_types::response::{Code, CodeOther, Data}; use imap_codec::imap_types::response::{Code, CodeOther, Data};
use imap_codec::imap_types::status::{StatusDataItem, StatusDataItemName}; use imap_codec::imap_types::status::{StatusDataItem, StatusDataItemName};
use aero_collections::mail::namespace::MAILBOX_HIERARCHY_DELIMITER as MBX_HIER_DELIM_RAW;
use aero_collections::mail::uidindex::*;
use aero_collections::mail::IMF;
use aero_collections::user::User;
use crate::imap::capability::{ClientCapability, ServerCapability}; use crate::imap::capability::{ClientCapability, ServerCapability};
use crate::imap::command::{anystate, MailboxName}; use crate::imap::command::{anystate, MailboxName};
use crate::imap::flow; use crate::imap::flow;
use crate::imap::mailbox_view::MailboxView; use crate::imap::mailbox_view::MailboxView;
use crate::imap::response::Response; use crate::imap::response::Response;
use crate::mail::mailbox::Mailbox;
use crate::mail::uidindex::*;
use crate::mail::user::{User, MAILBOX_HIERARCHY_DELIMITER as MBX_HIER_DELIM_RAW};
use crate::mail::IMF;
pub struct AuthenticatedContext<'a> { pub struct AuthenticatedContext<'a> {
pub req: &'a Command<'static>, pub req: &'a Command<'static>,
pub server_capabilities: &'a ServerCapability, pub server_capabilities: &'a ServerCapability,
@ -30,7 +33,7 @@ pub struct AuthenticatedContext<'a> {
} }
pub async fn dispatch<'a>( pub async fn dispatch<'a>(
ctx: AuthenticatedContext<'a>, mut ctx: AuthenticatedContext<'a>,
) -> Result<(Response<'static>, flow::Transition)> { ) -> Result<(Response<'static>, flow::Transition)> {
match &ctx.req.body { match &ctx.req.body {
// Any state // Any state
@ -47,19 +50,20 @@ pub async fn dispatch<'a>(
CommandBody::Lsub { CommandBody::Lsub {
reference, reference,
mailbox_wildcard, mailbox_wildcard,
} => ctx.list(reference, mailbox_wildcard, true).await, } => ctx.list(reference, mailbox_wildcard, &[], true).await,
CommandBody::List { CommandBody::List {
reference, reference,
mailbox_wildcard, mailbox_wildcard,
} => ctx.list(reference, mailbox_wildcard, false).await, r#return,
} => ctx.list(reference, mailbox_wildcard, r#return, false).await,
CommandBody::Status { CommandBody::Status {
mailbox, mailbox,
item_names, item_names,
} => ctx.status(mailbox, item_names).await, } => ctx.status(mailbox, item_names).await,
CommandBody::Subscribe { mailbox } => ctx.subscribe(mailbox).await, CommandBody::Subscribe { mailbox } => ctx.subscribe(mailbox).await,
CommandBody::Unsubscribe { mailbox } => ctx.unsubscribe(mailbox).await, CommandBody::Unsubscribe { mailbox } => ctx.unsubscribe(mailbox).await,
CommandBody::Select { mailbox } => ctx.select(mailbox).await, CommandBody::Select { mailbox, modifiers } => ctx.select(mailbox, modifiers).await,
CommandBody::Examine { mailbox } => ctx.examine(mailbox).await, CommandBody::Examine { mailbox, modifiers } => ctx.examine(mailbox, modifiers).await,
CommandBody::Append { CommandBody::Append {
mailbox, mailbox,
flags, flags,
@ -163,9 +167,10 @@ impl<'a> AuthenticatedContext<'a> {
} }
async fn list( async fn list(
self, &mut self,
reference: &MailboxCodec<'a>, reference: &MailboxCodec<'a>,
mailbox_wildcard: &ListMailbox<'a>, mailbox_wildcard: &ListMailbox<'a>,
must_return: &[ListReturnItem],
is_lsub: bool, is_lsub: bool,
) -> Result<(Response<'static>, flow::Transition)> { ) -> Result<(Response<'static>, flow::Transition)> {
let mbx_hier_delim: QuotedChar = QuotedChar::unvalidated(MBX_HIER_DELIM_RAW); let mbx_hier_delim: QuotedChar = QuotedChar::unvalidated(MBX_HIER_DELIM_RAW);
@ -181,6 +186,11 @@ impl<'a> AuthenticatedContext<'a> {
)); ));
} }
let status_item_names = must_return.iter().find_map(|m| match m {
ListReturnItem::Status(v) => Some(v),
_ => None,
});
// @FIXME would probably need a rewrite to better use the imap_codec library // @FIXME would probably need a rewrite to better use the imap_codec library
let wildcard = match mailbox_wildcard { let wildcard = match mailbox_wildcard {
ListMailbox::Token(v) => std::str::from_utf8(v.as_ref())?, ListMailbox::Token(v) => std::str::from_utf8(v.as_ref())?,
@ -231,29 +241,57 @@ impl<'a> AuthenticatedContext<'a> {
let mut ret = vec![]; let mut ret = vec![];
for (mb, is_real) in vmailboxes.iter() { for (mb, is_real) in vmailboxes.iter() {
if matches_wildcard(&wildcard, mb) { if matches_wildcard(&wildcard, mb) {
let mailbox = mb let mailbox: MailboxCodec = mb
.to_string() .to_string()
.try_into() .try_into()
.map_err(|_| anyhow!("invalid mailbox name"))?; .map_err(|_| anyhow!("invalid mailbox name"))?;
let mut items = vec![FlagNameAttribute::try_from(Atom::unvalidated( let mut items = vec![FlagNameAttribute::from(Atom::unvalidated("Subscribed"))];
"Subscribed",
))?]; // Decoration
if !*is_real { if !*is_real {
items.push(FlagNameAttribute::Noselect); items.push(FlagNameAttribute::Noselect);
} else {
match *mb {
"Drafts" => items.push(Atom::unvalidated("Drafts").into()),
"Archive" => items.push(Atom::unvalidated("Archive").into()),
"Sent" => items.push(Atom::unvalidated("Sent").into()),
"Trash" => items.push(Atom::unvalidated("Trash").into()),
_ => (),
};
} }
// Result type
if is_lsub { if is_lsub {
ret.push(Data::Lsub { ret.push(Data::Lsub {
items, items,
delimiter: Some(mbx_hier_delim), delimiter: Some(mbx_hier_delim),
mailbox, mailbox: mailbox.clone(),
}); });
} else { } else {
ret.push(Data::List { ret.push(Data::List {
items, items,
delimiter: Some(mbx_hier_delim), delimiter: Some(mbx_hier_delim),
mailbox, mailbox: mailbox.clone(),
}); });
} }
// Also collect status
if let Some(sin) = status_item_names {
let ret_attrs = match self.status_items(mb, sin).await {
Ok(a) => a,
Err(e) => {
tracing::error!(err=?e, mailbox=%mb, "Unable to fetch status for mailbox");
continue;
}
};
let data = Data::Status {
mailbox,
items: ret_attrs.into(),
};
ret.push(data);
}
} }
} }
@ -273,26 +311,55 @@ impl<'a> AuthenticatedContext<'a> {
} }
async fn status( async fn status(
self, &mut self,
mailbox: &MailboxCodec<'static>, mailbox: &MailboxCodec<'static>,
attributes: &[StatusDataItemName], attributes: &[StatusDataItemName],
) -> Result<(Response<'static>, flow::Transition)> { ) -> Result<(Response<'static>, flow::Transition)> {
let name: &str = MailboxName(mailbox).try_into()?; let name: &str = MailboxName(mailbox).try_into()?;
let ret_attrs = match self.status_items(name, attributes).await {
Ok(v) => v,
Err(e) => match e.downcast_ref::<CommandError>() {
Some(CommandError::MailboxNotFound) => {
return Ok((
Response::build()
.to_req(self.req)
.message("Mailbox does not exist")
.no()?,
flow::Transition::None,
))
}
_ => return Err(e.into()),
},
};
let data = Data::Status {
mailbox: mailbox.clone(),
items: ret_attrs.into(),
};
Ok((
Response::build()
.to_req(self.req)
.message("STATUS completed")
.data(data)
.ok()?,
flow::Transition::None,
))
}
async fn status_items(
&mut self,
name: &str,
attributes: &[StatusDataItemName],
) -> Result<Vec<StatusDataItem>> {
let mb_opt = self.user.open_mailbox(name).await?; let mb_opt = self.user.open_mailbox(name).await?;
let mb = match mb_opt { let mb = match mb_opt {
Some(mb) => mb, Some(mb) => mb,
None => { None => return Err(CommandError::MailboxNotFound.into()),
return Ok((
Response::build()
.to_req(self.req)
.message("Mailbox does not exist")
.no()?,
flow::Transition::None,
))
}
}; };
let view = MailboxView::new(mb).await; let view = MailboxView::new(mb, self.client_capabilities.condstore.is_enabled()).await;
let mut ret_attrs = vec![]; let mut ret_attrs = vec![];
for attr in attributes.iter() { for attr in attributes.iter() {
@ -311,24 +378,12 @@ impl<'a> AuthenticatedContext<'a> {
bail!("quota not implemented, can't return freed storage after EXPUNGE will be run"); bail!("quota not implemented, can't return freed storage after EXPUNGE will be run");
}, },
StatusDataItemName::HighestModSeq => { StatusDataItemName::HighestModSeq => {
bail!("highestmodseq not yet implemented"); self.client_capabilities.enable_condstore();
} StatusDataItem::HighestModSeq(view.highestmodseq().get())
},
}); });
} }
Ok(ret_attrs)
let data = Data::Status {
mailbox: mailbox.clone(),
items: ret_attrs.into(),
};
Ok((
Response::build()
.to_req(self.req)
.message("STATUS completed")
.data(data)
.ok()?,
flow::Transition::None,
))
} }
async fn subscribe( async fn subscribe(
@ -405,6 +460,7 @@ impl<'a> AuthenticatedContext<'a> {
RFC9051 (imap4rev2) says that OK [UNSEEN] responses are deprecated after SELECT and EXAMINE RFC9051 (imap4rev2) says that OK [UNSEEN] responses are deprecated after SELECT and EXAMINE
For Aerogramme, we just don't send the OK [UNSEEN], it's correct to do in both specifications. For Aerogramme, we just don't send the OK [UNSEEN], it's correct to do in both specifications.
20 select "INBOX.achats" 20 select "INBOX.achats"
* FLAGS (\Answered \Flagged \Deleted \Seen \Draft $Forwarded JUNK $label1) * FLAGS (\Answered \Flagged \Deleted \Seen \Draft $Forwarded JUNK $label1)
* OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft $Forwarded JUNK $label1 \*)] Flags permitted. * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft $Forwarded JUNK $label1 \*)] Flags permitted.
@ -420,7 +476,10 @@ impl<'a> AuthenticatedContext<'a> {
async fn select( async fn select(
self, self,
mailbox: &MailboxCodec<'a>, mailbox: &MailboxCodec<'a>,
modifiers: &[SelectExamineModifier],
) -> Result<(Response<'static>, flow::Transition)> { ) -> Result<(Response<'static>, flow::Transition)> {
self.client_capabilities.select_enable(modifiers);
let name: &str = MailboxName(mailbox).try_into()?; let name: &str = MailboxName(mailbox).try_into()?;
let mb_opt = self.user.open_mailbox(&name).await?; let mb_opt = self.user.open_mailbox(&name).await?;
@ -438,7 +497,7 @@ impl<'a> AuthenticatedContext<'a> {
}; };
tracing::info!(username=%self.user.username, mailbox=%name, "mailbox.selected"); tracing::info!(username=%self.user.username, mailbox=%name, "mailbox.selected");
let mb = MailboxView::new(mb).await; let mb = MailboxView::new(mb, self.client_capabilities.condstore.is_enabled()).await;
let data = mb.summary()?; let data = mb.summary()?;
Ok(( Ok((
@ -448,14 +507,17 @@ impl<'a> AuthenticatedContext<'a> {
.code(Code::ReadWrite) .code(Code::ReadWrite)
.set_body(data) .set_body(data)
.ok()?, .ok()?,
flow::Transition::Select(mb), flow::Transition::Select(mb, flow::MailboxPerm::ReadWrite),
)) ))
} }
async fn examine( async fn examine(
self, self,
mailbox: &MailboxCodec<'a>, mailbox: &MailboxCodec<'a>,
modifiers: &[SelectExamineModifier],
) -> Result<(Response<'static>, flow::Transition)> { ) -> Result<(Response<'static>, flow::Transition)> {
self.client_capabilities.select_enable(modifiers);
let name: &str = MailboxName(mailbox).try_into()?; let name: &str = MailboxName(mailbox).try_into()?;
let mb_opt = self.user.open_mailbox(&name).await?; let mb_opt = self.user.open_mailbox(&name).await?;
@ -473,7 +535,7 @@ impl<'a> AuthenticatedContext<'a> {
}; };
tracing::info!(username=%self.user.username, mailbox=%name, "mailbox.examined"); tracing::info!(username=%self.user.username, mailbox=%name, "mailbox.examined");
let mb = MailboxView::new(mb).await; let mb = MailboxView::new(mb, self.client_capabilities.condstore.is_enabled()).await;
let data = mb.summary()?; let data = mb.summary()?;
Ok(( Ok((
@ -483,10 +545,12 @@ impl<'a> AuthenticatedContext<'a> {
.code(Code::ReadOnly) .code(Code::ReadOnly)
.set_body(data) .set_body(data)
.ok()?, .ok()?,
flow::Transition::Examine(mb), flow::Transition::Select(mb, flow::MailboxPerm::ReadOnly),
)) ))
} }
//@FIXME we should write a specific version for the "selected" state
//that returns some unsollicited responses
async fn append( async fn append(
self, self,
mailbox: &MailboxCodec<'a>, mailbox: &MailboxCodec<'a>,
@ -496,7 +560,7 @@ impl<'a> AuthenticatedContext<'a> {
) -> Result<(Response<'static>, flow::Transition)> { ) -> Result<(Response<'static>, flow::Transition)> {
let append_tag = self.req.tag.clone(); let append_tag = self.req.tag.clone();
match self.append_internal(mailbox, flags, date, message).await { match self.append_internal(mailbox, flags, date, message).await {
Ok((_mb, uidvalidity, uid)) => Ok(( Ok((_mb_view, uidvalidity, uid, _modseq)) => Ok((
Response::build() Response::build()
.tag(append_tag) .tag(append_tag)
.message("APPEND completed") .message("APPEND completed")
@ -518,7 +582,7 @@ impl<'a> AuthenticatedContext<'a> {
fn enable( fn enable(
self, self,
cap_enable: &NonEmptyVec<CapabilityEnable<'static>>, cap_enable: &Vec1<CapabilityEnable<'static>>,
) -> Result<(Response<'static>, flow::Transition)> { ) -> Result<(Response<'static>, flow::Transition)> {
let mut response_builder = Response::build().to_req(self.req); let mut response_builder = Response::build().to_req(self.req);
let capabilities = self.client_capabilities.try_enable(cap_enable.as_ref()); let capabilities = self.client_capabilities.try_enable(cap_enable.as_ref());
@ -531,13 +595,14 @@ impl<'a> AuthenticatedContext<'a> {
)) ))
} }
//@FIXME should be refactored and integrated to the mailbox view
pub(crate) async fn append_internal( pub(crate) async fn append_internal(
self, self,
mailbox: &MailboxCodec<'a>, mailbox: &MailboxCodec<'a>,
flags: &[Flag<'a>], flags: &[Flag<'a>],
date: &Option<DateTime>, date: &Option<DateTime>,
message: &Literal<'a>, message: &Literal<'a>,
) -> Result<(Arc<Mailbox>, ImapUidvalidity, ImapUidvalidity)> { ) -> Result<(MailboxView, ImapUidvalidity, ImapUid, ModSeq)> {
let name: &str = MailboxName(mailbox).try_into()?; let name: &str = MailboxName(mailbox).try_into()?;
let mb_opt = self.user.open_mailbox(&name).await?; let mb_opt = self.user.open_mailbox(&name).await?;
@ -545,6 +610,7 @@ impl<'a> AuthenticatedContext<'a> {
Some(mb) => mb, Some(mb) => mb,
None => bail!("Mailbox does not exist"), None => bail!("Mailbox does not exist"),
}; };
let view = MailboxView::new(mb, self.client_capabilities.condstore.is_enabled()).await;
if date.is_some() { if date.is_some() {
tracing::warn!("Cannot set date when appending message"); tracing::warn!("Cannot set date when appending message");
@ -555,9 +621,11 @@ impl<'a> AuthenticatedContext<'a> {
let flags = flags.iter().map(|x| x.to_string()).collect::<Vec<_>>(); let flags = flags.iter().map(|x| x.to_string()).collect::<Vec<_>>();
// TODO: filter allowed flags? ping @Quentin // TODO: filter allowed flags? ping @Quentin
let (uidvalidity, uid) = mb.append(msg, None, &flags[..]).await?; let (uidvalidity, uid, modseq) =
view.internal.mailbox.append(msg, None, &flags[..]).await?;
//let unsollicited = view.update(UpdateParameters::default()).await?;
Ok((mb, uidvalidity, uid)) Ok((view, uidvalidity, uid, modseq))
} }
} }
@ -590,6 +658,12 @@ fn matches_wildcard(wildcard: &str, name: &str) -> bool {
matches[name.len()][wildcard.len()] matches[name.len()][wildcard.len()]
} }
#[derive(Error, Debug)]
pub enum CommandError {
#[error("Mailbox not found")]
MailboxNotFound,
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View file

@ -1,10 +1,9 @@
pub mod anonymous; pub mod anonymous;
pub mod anystate; pub mod anystate;
pub mod authenticated; pub mod authenticated;
pub mod examined;
pub mod selected; pub mod selected;
use crate::mail::user::INBOX; use aero_collections::mail::namespace::INBOX;
use imap_codec::imap_types::mailbox::Mailbox as MailboxCodec; use imap_codec::imap_types::mailbox::Mailbox as MailboxCodec;
/// Convert an IMAP mailbox name/identifier representation /// Convert an IMAP mailbox name/identifier representation

View file

@ -1,7 +1,8 @@
use std::num::NonZeroU64;
use std::sync::Arc; use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use imap_codec::imap_types::command::{Command, CommandBody}; use imap_codec::imap_types::command::{Command, CommandBody, FetchModifier, StoreModifier};
use imap_codec::imap_types::core::Charset; use imap_codec::imap_types::core::Charset;
use imap_codec::imap_types::fetch::MacroOrMessageDataItemNames; use imap_codec::imap_types::fetch::MacroOrMessageDataItemNames;
use imap_codec::imap_types::flag::{Flag, StoreResponse, StoreType}; use imap_codec::imap_types::flag::{Flag, StoreResponse, StoreType};
@ -10,20 +11,22 @@ use imap_codec::imap_types::response::{Code, CodeOther};
use imap_codec::imap_types::search::SearchKey; use imap_codec::imap_types::search::SearchKey;
use imap_codec::imap_types::sequence::SequenceSet; use imap_codec::imap_types::sequence::SequenceSet;
use aero_collections::user::User;
use crate::imap::attributes::AttributesProxy;
use crate::imap::capability::{ClientCapability, ServerCapability}; use crate::imap::capability::{ClientCapability, ServerCapability};
use crate::imap::command::{anystate, authenticated, MailboxName}; use crate::imap::command::{anystate, authenticated, MailboxName};
use crate::imap::flow; use crate::imap::flow;
use crate::imap::mailbox_view::MailboxView; use crate::imap::mailbox_view::{MailboxView, UpdateParameters};
use crate::imap::response::Response; use crate::imap::response::Response;
use crate::mail::user::User;
pub struct SelectedContext<'a> { pub struct SelectedContext<'a> {
pub req: &'a Command<'static>, pub req: &'a Command<'static>,
pub user: &'a Arc<User>, pub user: &'a Arc<User>,
pub mailbox: &'a mut MailboxView, pub mailbox: &'a mut MailboxView,
pub server_capabilities: &'a ServerCapability, pub server_capabilities: &'a ServerCapability,
pub client_capabilities: &'a mut ClientCapability, pub client_capabilities: &'a mut ClientCapability,
pub perm: &'a flow::MailboxPerm,
} }
pub async fn dispatch<'a>( pub async fn dispatch<'a>(
@ -38,26 +41,44 @@ pub async fn dispatch<'a>(
CommandBody::Logout => anystate::logout(), CommandBody::Logout => anystate::logout(),
// Specific to this state (7 commands + NOOP) // Specific to this state (7 commands + NOOP)
CommandBody::Close => ctx.close().await, CommandBody::Close => match ctx.perm {
flow::MailboxPerm::ReadWrite => ctx.close().await,
flow::MailboxPerm::ReadOnly => ctx.examine_close().await,
},
CommandBody::Noop | CommandBody::Check => ctx.noop().await, CommandBody::Noop | CommandBody::Check => ctx.noop().await,
CommandBody::Fetch { CommandBody::Fetch {
sequence_set, sequence_set,
macro_or_item_names, macro_or_item_names,
modifiers,
uid, uid,
} => ctx.fetch(sequence_set, macro_or_item_names, uid).await, } => {
ctx.fetch(sequence_set, macro_or_item_names, modifiers, uid)
.await
}
//@FIXME SearchKey::And is a legacy hack, should be refactored
CommandBody::Search { CommandBody::Search {
charset, charset,
criteria, criteria,
uid, uid,
} => ctx.search(charset, criteria, uid).await, } => {
CommandBody::Expunge => ctx.expunge().await, ctx.search(charset, &SearchKey::And(criteria.clone()), uid)
.await
}
CommandBody::Expunge {
// UIDPLUS (rfc4315)
uid_sequence_set,
} => ctx.expunge(uid_sequence_set).await,
CommandBody::Store { CommandBody::Store {
sequence_set, sequence_set,
kind, kind,
response, response,
flags, flags,
modifiers,
uid, uid,
} => ctx.store(sequence_set, kind, response, flags, uid).await, } => {
ctx.store(sequence_set, kind, response, flags, modifiers, uid)
.await
}
CommandBody::Copy { CommandBody::Copy {
sequence_set, sequence_set,
mailbox, mailbox,
@ -92,13 +113,25 @@ impl<'a> SelectedContext<'a> {
// We expunge messages, // We expunge messages,
// but we don't send the untagged EXPUNGE responses // but we don't send the untagged EXPUNGE responses
let tag = self.req.tag.clone(); let tag = self.req.tag.clone();
self.expunge().await?; self.expunge(&None).await?;
Ok(( Ok((
Response::build().tag(tag).message("CLOSE completed").ok()?, Response::build().tag(tag).message("CLOSE completed").ok()?,
flow::Transition::Unselect, flow::Transition::Unselect,
)) ))
} }
/// CLOSE in examined state is not the same as in selected state
/// (in selected state it also does an EXPUNGE, here it doesn't)
async fn examine_close(self) -> Result<(Response<'static>, flow::Transition)> {
Ok((
Response::build()
.to_req(self.req)
.message("CLOSE completed")
.ok()?,
flow::Transition::Unselect,
))
}
async fn unselect(self) -> Result<(Response<'static>, flow::Transition)> { async fn unselect(self) -> Result<(Response<'static>, flow::Transition)> {
Ok(( Ok((
Response::build() Response::build()
@ -113,17 +146,38 @@ impl<'a> SelectedContext<'a> {
self, self,
sequence_set: &SequenceSet, sequence_set: &SequenceSet,
attributes: &'a MacroOrMessageDataItemNames<'static>, attributes: &'a MacroOrMessageDataItemNames<'static>,
modifiers: &[FetchModifier],
uid: &bool, uid: &bool,
) -> Result<(Response<'static>, flow::Transition)> { ) -> Result<(Response<'static>, flow::Transition)> {
match self.mailbox.fetch(sequence_set, attributes, uid).await { let ap = AttributesProxy::new(attributes, modifiers, *uid);
Ok(resp) => Ok(( let mut changed_since: Option<NonZeroU64> = None;
Response::build() modifiers.iter().for_each(|m| match m {
.to_req(self.req) FetchModifier::ChangedSince(val) => {
.message("FETCH completed") changed_since = Some(*val);
.set_body(resp) }
.ok()?, });
flow::Transition::None,
)), match self
.mailbox
.fetch(sequence_set, &ap, changed_since, uid)
.await
{
Ok(resp) => {
// Capabilities enabling logic only on successful command
// (according to my understanding of the spec)
self.client_capabilities.attributes_enable(&ap);
self.client_capabilities.fetch_modifiers_enable(modifiers);
// Response to the client
Ok((
Response::build()
.to_req(self.req)
.message("FETCH completed")
.set_body(resp)
.ok()?,
flow::Transition::None,
))
}
Err(e) => Ok(( Err(e) => Ok((
Response::build() Response::build()
.to_req(self.req) .to_req(self.req)
@ -140,7 +194,10 @@ impl<'a> SelectedContext<'a> {
criteria: &SearchKey<'a>, criteria: &SearchKey<'a>,
uid: &bool, uid: &bool,
) -> Result<(Response<'static>, flow::Transition)> { ) -> Result<(Response<'static>, flow::Transition)> {
let found = self.mailbox.search(charset, criteria, *uid).await?; let (found, enable_condstore) = self.mailbox.search(charset, criteria, *uid).await?;
if enable_condstore {
self.client_capabilities.enable_condstore();
}
Ok(( Ok((
Response::build() Response::build()
.to_req(self.req) .to_req(self.req)
@ -152,9 +209,9 @@ impl<'a> SelectedContext<'a> {
} }
pub async fn noop(self) -> Result<(Response<'static>, flow::Transition)> { pub async fn noop(self) -> Result<(Response<'static>, flow::Transition)> {
self.mailbox.0.mailbox.force_sync().await?; self.mailbox.internal.mailbox.force_sync().await?;
let updates = self.mailbox.update().await?; let updates = self.mailbox.update(UpdateParameters::default()).await?;
Ok(( Ok((
Response::build() Response::build()
.to_req(self.req) .to_req(self.req)
@ -165,9 +222,16 @@ impl<'a> SelectedContext<'a> {
)) ))
} }
async fn expunge(self) -> Result<(Response<'static>, flow::Transition)> { async fn expunge(
self,
uid_sequence_set: &Option<SequenceSet>,
) -> Result<(Response<'static>, flow::Transition)> {
if let Some(failed) = self.fail_read_only() {
return Ok((failed, flow::Transition::None));
}
let tag = self.req.tag.clone(); let tag = self.req.tag.clone();
let data = self.mailbox.expunge().await?; let data = self.mailbox.expunge(uid_sequence_set).await?;
Ok(( Ok((
Response::build() Response::build()
@ -185,21 +249,50 @@ impl<'a> SelectedContext<'a> {
kind: &StoreType, kind: &StoreType,
response: &StoreResponse, response: &StoreResponse,
flags: &[Flag<'a>], flags: &[Flag<'a>],
modifiers: &[StoreModifier],
uid: &bool, uid: &bool,
) -> Result<(Response<'static>, flow::Transition)> { ) -> Result<(Response<'static>, flow::Transition)> {
let data = self if let Some(failed) = self.fail_read_only() {
return Ok((failed, flow::Transition::None));
}
let mut unchanged_since: Option<NonZeroU64> = None;
modifiers.iter().for_each(|m| match m {
StoreModifier::UnchangedSince(val) => {
unchanged_since = Some(*val);
}
});
let (data, modified) = self
.mailbox .mailbox
.store(sequence_set, kind, response, flags, uid) .store(sequence_set, kind, response, flags, unchanged_since, uid)
.await?; .await?;
Ok(( let mut ok_resp = Response::build()
Response::build() .to_req(self.req)
.to_req(self.req) .message("STORE completed")
.message("STORE completed") .set_body(data);
.set_body(data)
.ok()?, match modified[..] {
flow::Transition::None, [] => (),
)) [_head, ..] => {
let modified_str = format!(
"MODIFIED {}",
modified
.into_iter()
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join(",")
);
ok_resp = ok_resp.code(Code::Other(CodeOther::unvalidated(
modified_str.into_bytes(),
)));
}
};
self.client_capabilities.store_modifiers_enable(modifiers);
Ok((ok_resp.ok()?, flow::Transition::None))
} }
async fn copy( async fn copy(
@ -208,6 +301,11 @@ impl<'a> SelectedContext<'a> {
mailbox: &MailboxCodec<'a>, mailbox: &MailboxCodec<'a>,
uid: &bool, uid: &bool,
) -> Result<(Response<'static>, flow::Transition)> { ) -> Result<(Response<'static>, flow::Transition)> {
//@FIXME Could copy be valid in EXAMINE mode?
if let Some(failed) = self.fail_read_only() {
return Ok((failed, flow::Transition::None));
}
let name: &str = MailboxName(mailbox).try_into()?; let name: &str = MailboxName(mailbox).try_into()?;
let mb_opt = self.user.open_mailbox(&name).await?; let mb_opt = self.user.open_mailbox(&name).await?;
@ -260,6 +358,10 @@ impl<'a> SelectedContext<'a> {
mailbox: &MailboxCodec<'a>, mailbox: &MailboxCodec<'a>,
uid: &bool, uid: &bool,
) -> Result<(Response<'static>, flow::Transition)> { ) -> Result<(Response<'static>, flow::Transition)> {
if let Some(failed) = self.fail_read_only() {
return Ok((failed, flow::Transition::None));
}
let name: &str = MailboxName(mailbox).try_into()?; let name: &str = MailboxName(mailbox).try_into()?;
let mb_opt = self.user.open_mailbox(&name).await?; let mb_opt = self.user.open_mailbox(&name).await?;
@ -307,4 +409,17 @@ impl<'a> SelectedContext<'a> {
flow::Transition::None, flow::Transition::None,
)) ))
} }
fn fail_read_only(&self) -> Option<Response<'static>> {
match self.perm {
flow::MailboxPerm::ReadWrite => None,
flow::MailboxPerm::ReadOnly => Some(
Response::build()
.to_req(self.req)
.message("Write command are forbidden while exmining mailbox")
.no()
.unwrap(),
),
}
}
} }

115
aero-proto/src/imap/flow.rs Normal file
View file

@ -0,0 +1,115 @@
use std::error::Error as StdError;
use std::fmt;
use std::sync::Arc;
use imap_codec::imap_types::core::Tag;
use tokio::sync::Notify;
use aero_collections::user::User;
use crate::imap::mailbox_view::MailboxView;
#[derive(Debug)]
pub enum Error {
ForbiddenTransition,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Forbidden Transition")
}
}
impl StdError for Error {}
pub enum State {
NotAuthenticated,
Authenticated(Arc<User>),
Selected(Arc<User>, MailboxView, MailboxPerm),
Idle(
Arc<User>,
MailboxView,
MailboxPerm,
Tag<'static>,
Arc<Notify>,
),
Logout,
}
impl State {
pub fn notify(&self) -> Option<Arc<Notify>> {
match self {
Self::Idle(_, _, _, _, anotif) => Some(anotif.clone()),
_ => None,
}
}
}
impl fmt::Display for State {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use State::*;
match self {
NotAuthenticated => write!(f, "NotAuthenticated"),
Authenticated(..) => write!(f, "Authenticated"),
Selected(..) => write!(f, "Selected"),
Idle(..) => write!(f, "Idle"),
Logout => write!(f, "Logout"),
}
}
}
#[derive(Clone)]
pub enum MailboxPerm {
ReadOnly,
ReadWrite,
}
pub enum Transition {
None,
Authenticate(Arc<User>),
Select(MailboxView, MailboxPerm),
Idle(Tag<'static>, Notify),
UnIdle,
Unselect,
Logout,
}
impl fmt::Display for Transition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use Transition::*;
match self {
None => write!(f, "None"),
Authenticate(..) => write!(f, "Authenticated"),
Select(..) => write!(f, "Selected"),
Idle(..) => write!(f, "Idle"),
UnIdle => write!(f, "UnIdle"),
Unselect => write!(f, "Unselect"),
Logout => write!(f, "Logout"),
}
}
}
// See RFC3501 section 3.
// https://datatracker.ietf.org/doc/html/rfc3501#page-13
impl State {
pub fn apply(&mut self, tr: Transition) -> Result<(), Error> {
tracing::debug!(state=%self, transition=%tr, "try change state");
let new_state = match (std::mem::replace(self, State::Logout), tr) {
(s, Transition::None) => s,
(State::NotAuthenticated, Transition::Authenticate(u)) => State::Authenticated(u),
(State::Authenticated(u) | State::Selected(u, _, _), Transition::Select(m, p)) => {
State::Selected(u, m, p)
}
(State::Selected(u, _, _), Transition::Unselect) => State::Authenticated(u.clone()),
(State::Selected(u, m, p), Transition::Idle(t, s)) => {
State::Idle(u, m, p, t, Arc::new(s))
}
(State::Idle(u, m, p, _, _), Transition::UnIdle) => State::Selected(u, m, p),
(_, Transition::Logout) => State::Logout,
(s, t) => {
tracing::error!(state=%s, transition=%t, "forbidden transition");
return Err(Error::ForbiddenTransition);
}
};
*self = new_state;
tracing::debug!(state=%self, "transition succeeded");
Ok(())
}
}

View file

@ -1,10 +1,10 @@
use std::num::NonZeroU32; use std::num::{NonZeroU32, NonZeroU64};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use imap_codec::imap_types::sequence::{self, SeqOrUid, Sequence, SequenceSet}; use imap_codec::imap_types::sequence::{SeqOrUid, Sequence, SequenceSet};
use crate::mail::uidindex::{ImapUid, UidIndex}; use aero_collections::mail::uidindex::{ImapUid, ModSeq, UidIndex};
use crate::mail::unique_ident::UniqueIdent; use aero_collections::unique_ident::UniqueIdent;
pub struct Index<'a> { pub struct Index<'a> {
pub imap_index: Vec<MailIndex<'a>>, pub imap_index: Vec<MailIndex<'a>>,
@ -17,12 +17,10 @@ impl<'a> Index<'a> {
.iter() .iter()
.enumerate() .enumerate()
.map(|(i_enum, (&uid, &uuid))| { .map(|(i_enum, (&uid, &uuid))| {
let flags = internal let (_, modseq, flags) = internal
.table .table
.get(&uuid) .get(&uuid)
.ok_or(anyhow!("mail is missing from index"))? .ok_or(anyhow!("mail is missing from index"))?;
.1
.as_ref();
let i_int: u32 = (i_enum + 1).try_into()?; let i_int: u32 = (i_enum + 1).try_into()?;
let i: NonZeroU32 = i_int.try_into()?; let i: NonZeroU32 = i_int.try_into()?;
@ -30,6 +28,7 @@ impl<'a> Index<'a> {
i, i,
uid, uid,
uuid, uuid,
modseq: *modseq,
flags, flags,
}) })
}) })
@ -61,10 +60,8 @@ impl<'a> Index<'a> {
if self.imap_index.is_empty() { if self.imap_index.is_empty() {
return vec![]; return vec![];
} }
let iter_strat = sequence::Strategy::Naive { let largest = self.last().expect("The mailbox is not empty").uid;
largest: self.last().expect("The mailbox is not empty").uid, let mut unroll_seq = sequence_set.iter(largest).collect::<Vec<_>>();
};
let mut unroll_seq = sequence_set.iter(iter_strat).collect::<Vec<_>>();
unroll_seq.sort(); unroll_seq.sort();
let start_seq = match unroll_seq.iter().next() { let start_seq = match unroll_seq.iter().next() {
@ -103,11 +100,9 @@ impl<'a> Index<'a> {
if self.imap_index.is_empty() { if self.imap_index.is_empty() {
return Ok(vec![]); return Ok(vec![]);
} }
let iter_strat = sequence::Strategy::Naive { let largest = NonZeroU32::try_from(self.imap_index.len() as u32)?;
largest: NonZeroU32::try_from(self.imap_index.len() as u32)?,
};
let mut acc = sequence_set let mut acc = sequence_set
.iter(iter_strat) .iter(largest)
.map(|wanted_id| { .map(|wanted_id| {
self.imap_index self.imap_index
.get((wanted_id.get() as usize) - 1) .get((wanted_id.get() as usize) - 1)
@ -131,6 +126,36 @@ impl<'a> Index<'a> {
_ => self.fetch_on_id(sequence_set), _ => self.fetch_on_id(sequence_set),
} }
} }
pub fn fetch_changed_since(
self: &'a Index<'a>,
sequence_set: &SequenceSet,
maybe_modseq: Option<NonZeroU64>,
by_uid: bool,
) -> Result<Vec<&'a MailIndex<'a>>> {
let raw = self.fetch(sequence_set, by_uid)?;
let res = match maybe_modseq {
Some(pit) => raw.into_iter().filter(|midx| midx.modseq > pit).collect(),
None => raw,
};
Ok(res)
}
pub fn fetch_unchanged_since(
self: &'a Index<'a>,
sequence_set: &SequenceSet,
maybe_modseq: Option<NonZeroU64>,
by_uid: bool,
) -> Result<(Vec<&'a MailIndex<'a>>, Vec<&'a MailIndex<'a>>)> {
let raw = self.fetch(sequence_set, by_uid)?;
let res = match maybe_modseq {
Some(pit) => raw.into_iter().partition(|midx| midx.modseq <= pit),
None => (raw, vec![]),
};
Ok(res)
}
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -138,6 +163,7 @@ pub struct MailIndex<'a> {
pub i: NonZeroU32, pub i: NonZeroU32,
pub uid: ImapUid, pub uid: ImapUid,
pub uuid: UniqueIdent, pub uuid: UniqueIdent,
pub modseq: ModSeq,
pub flags: &'a Vec<String>, pub flags: &'a Vec<String>,
} }

View file

@ -16,7 +16,7 @@ use eml_codec::{
part::{composite::Message, AnyPart}, part::{composite::Message, AnyPart},
}; };
use crate::mail::query::QueryResult; use aero_collections::mail::query::QueryResult;
use crate::imap::attributes::AttributesProxy; use crate::imap::attributes::AttributesProxy;
use crate::imap::flags; use crate::imap::flags;
@ -90,6 +90,7 @@ impl<'a> MailView<'a> {
Ok(body) Ok(body)
} }
MessageDataItemName::InternalDate => self.internal_date(), MessageDataItemName::InternalDate => self.internal_date(),
MessageDataItemName::ModSeq => Ok(self.modseq()),
}) })
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
@ -252,6 +253,10 @@ impl<'a> MailView<'a> {
.ok_or(anyhow!("Unable to parse internal date"))?; .ok_or(anyhow!("Unable to parse internal date"))?;
Ok(MessageDataItem::InternalDate(DateTime::unvalidated(dt))) Ok(MessageDataItem::InternalDate(DateTime::unvalidated(dt)))
} }
fn modseq(&self) -> MessageDataItem<'static> {
MessageDataItem::ModSeq(self.in_idx.modseq)
}
} }
pub enum SeenFlag { pub enum SeenFlag {

View file

@ -1,21 +1,23 @@
use std::num::NonZeroU32; use std::collections::HashSet;
use std::num::{NonZeroU32, NonZeroU64};
use std::sync::Arc; use std::sync::Arc;
use anyhow::{anyhow, Error, Result}; use anyhow::{anyhow, Error, Result};
use futures::stream::{FuturesOrdered, StreamExt}; use futures::stream::{StreamExt, TryStreamExt};
use imap_codec::imap_types::core::Charset; use imap_codec::imap_types::core::Charset;
use imap_codec::imap_types::fetch::{MacroOrMessageDataItemNames, MessageDataItem}; use imap_codec::imap_types::fetch::MessageDataItem;
use imap_codec::imap_types::flag::{Flag, FlagFetch, FlagPerm, StoreResponse, StoreType}; use imap_codec::imap_types::flag::{Flag, FlagFetch, FlagPerm, StoreResponse, StoreType};
use imap_codec::imap_types::response::{Code, Data, Status}; use imap_codec::imap_types::response::{Code, CodeOther, Data, Status};
use imap_codec::imap_types::search::SearchKey; use imap_codec::imap_types::search::SearchKey;
use imap_codec::imap_types::sequence::SequenceSet; use imap_codec::imap_types::sequence::SequenceSet;
use crate::mail::mailbox::Mailbox; use aero_collections::mail::mailbox::Mailbox;
use crate::mail::query::QueryScope; use aero_collections::mail::query::QueryScope;
use crate::mail::snapshot::FrozenMailbox; use aero_collections::mail::snapshot::FrozenMailbox;
use crate::mail::uidindex::{ImapUid, ImapUidvalidity}; use aero_collections::mail::uidindex::{ImapUid, ImapUidvalidity, ModSeq};
use aero_collections::unique_ident::UniqueIdent;
use crate::imap::attributes::AttributesProxy; use crate::imap::attributes::AttributesProxy;
use crate::imap::flags; use crate::imap::flags;
@ -32,6 +34,21 @@ const DEFAULT_FLAGS: [Flag; 5] = [
Flag::Draft, Flag::Draft,
]; ];
pub struct UpdateParameters {
pub silence: HashSet<UniqueIdent>,
pub with_modseq: bool,
pub with_uid: bool,
}
impl Default for UpdateParameters {
fn default() -> Self {
Self {
silence: HashSet::new(),
with_modseq: false,
with_uid: false,
}
}
}
/// A MailboxView is responsible for giving the client the information /// A MailboxView is responsible for giving the client the information
/// it needs about a mailbox, such as an initial summary of the mailbox's /// it needs about a mailbox, such as an initial summary of the mailbox's
/// content and continuous updates indicating when the content /// content and continuous updates indicating when the content
@ -39,12 +56,18 @@ const DEFAULT_FLAGS: [Flag; 5] = [
/// To do this, it keeps a variable `known_state` that corresponds to /// To do this, it keeps a variable `known_state` that corresponds to
/// what the client knows, and produces IMAP messages to be sent to the /// what the client knows, and produces IMAP messages to be sent to the
/// client that go along updates to `known_state`. /// client that go along updates to `known_state`.
pub struct MailboxView(pub FrozenMailbox); pub struct MailboxView {
pub internal: FrozenMailbox,
pub is_condstore: bool,
}
impl MailboxView { impl MailboxView {
/// Creates a new IMAP view into a mailbox. /// Creates a new IMAP view into a mailbox.
pub async fn new(mailbox: Arc<Mailbox>) -> Self { pub async fn new(mailbox: Arc<Mailbox>, is_cond: bool) -> Self {
Self(mailbox.frozen().await) Self {
internal: mailbox.frozen().await,
is_condstore: is_cond,
}
} }
/// Create an updated view, useful to make a diff /// Create an updated view, useful to make a diff
@ -53,9 +76,9 @@ impl MailboxView {
/// what the client knows and what is actually in the mailbox. /// what the client knows and what is actually in the mailbox.
/// This does NOT trigger a sync, it bases itself on what is currently /// This does NOT trigger a sync, it bases itself on what is currently
/// loaded in RAM by Bayou. /// loaded in RAM by Bayou.
pub async fn update(&mut self) -> Result<Vec<Body<'static>>> { pub async fn update(&mut self, params: UpdateParameters) -> Result<Vec<Body<'static>>> {
let old_snapshot = self.0.update().await; let old_snapshot = self.internal.update().await;
let new_snapshot = &self.0.snapshot; let new_snapshot = &self.internal.snapshot;
let mut data = Vec::<Body>::new(); let mut data = Vec::<Body>::new();
@ -99,19 +122,29 @@ impl MailboxView {
} else { } else {
// - if flags changed for existing mails, tell client // - if flags changed for existing mails, tell client
for (i, (_uid, uuid)) in new_snapshot.idx_by_uid.iter().enumerate() { for (i, (_uid, uuid)) in new_snapshot.idx_by_uid.iter().enumerate() {
if params.silence.contains(uuid) {
continue;
}
let old_mail = old_snapshot.table.get(uuid); let old_mail = old_snapshot.table.get(uuid);
let new_mail = new_snapshot.table.get(uuid); let new_mail = new_snapshot.table.get(uuid);
if old_mail.is_some() && old_mail != new_mail { if old_mail.is_some() && old_mail != new_mail {
if let Some((uid, flags)) = new_mail { if let Some((uid, modseq, flags)) = new_mail {
let mut items = vec![MessageDataItem::Flags(
flags.iter().filter_map(|f| flags::from_str(f)).collect(),
)];
if params.with_uid {
items.push(MessageDataItem::Uid(*uid));
}
if params.with_modseq {
items.push(MessageDataItem::ModSeq(*modseq));
}
data.push(Body::Data(Data::Fetch { data.push(Body::Data(Data::Fetch {
seq: NonZeroU32::try_from((i + 1) as u32).unwrap(), seq: NonZeroU32::try_from((i + 1) as u32).unwrap(),
items: vec![ items: items.try_into()?,
MessageDataItem::Uid(*uid),
MessageDataItem::Flags(
flags.iter().filter_map(|f| flags::from_str(f)).collect(),
),
]
.try_into()?,
})); }));
} }
} }
@ -130,8 +163,11 @@ impl MailboxView {
data.extend(self.flags_status()?.into_iter()); data.extend(self.flags_status()?.into_iter());
data.push(self.uidvalidity_status()?); data.push(self.uidvalidity_status()?);
data.push(self.uidnext_status()?); data.push(self.uidnext_status()?);
self.unseen_first_status()? if self.is_condstore {
.map(|unseen_status| data.push(unseen_status)); data.push(self.highestmodseq_status()?);
}
/*self.unseen_first_status()?
.map(|unseen_status| data.push(unseen_status));*/
Ok(data) Ok(data)
} }
@ -140,50 +176,102 @@ impl MailboxView {
&mut self, &mut self,
sequence_set: &SequenceSet, sequence_set: &SequenceSet,
kind: &StoreType, kind: &StoreType,
_response: &StoreResponse, response: &StoreResponse,
flags: &[Flag<'a>], flags: &[Flag<'a>],
unchanged_since: Option<NonZeroU64>,
is_uid_store: &bool, is_uid_store: &bool,
) -> Result<Vec<Body<'static>>> { ) -> Result<(Vec<Body<'static>>, Vec<NonZeroU32>)> {
self.0.sync().await?; self.internal.sync().await?;
let flags = flags.iter().map(|x| x.to_string()).collect::<Vec<_>>(); let flags = flags.iter().map(|x| x.to_string()).collect::<Vec<_>>();
let idx = self.index()?; let idx = self.index()?;
let mails = idx.fetch(sequence_set, *is_uid_store)?; let (editable, in_conflict) =
for mi in mails.iter() { idx.fetch_unchanged_since(sequence_set, unchanged_since, *is_uid_store)?;
for mi in editable.iter() {
match kind { match kind {
StoreType::Add => { StoreType::Add => {
self.0.mailbox.add_flags(mi.uuid, &flags[..]).await?; self.internal.mailbox.add_flags(mi.uuid, &flags[..]).await?;
} }
StoreType::Remove => { StoreType::Remove => {
self.0.mailbox.del_flags(mi.uuid, &flags[..]).await?; self.internal.mailbox.del_flags(mi.uuid, &flags[..]).await?;
} }
StoreType::Replace => { StoreType::Replace => {
self.0.mailbox.set_flags(mi.uuid, &flags[..]).await?; self.internal.mailbox.set_flags(mi.uuid, &flags[..]).await?;
} }
} }
} }
// @TODO: handle _response let silence = match response {
self.update().await StoreResponse::Answer => HashSet::new(),
StoreResponse::Silent => editable.iter().map(|midx| midx.uuid).collect(),
};
let conflict_id_or_uid = match is_uid_store {
true => in_conflict.into_iter().map(|midx| midx.uid).collect(),
_ => in_conflict.into_iter().map(|midx| midx.i).collect(),
};
let summary = self
.update(UpdateParameters {
with_uid: *is_uid_store,
with_modseq: unchanged_since.is_some(),
silence,
})
.await?;
Ok((summary, conflict_id_or_uid))
} }
pub async fn expunge(&mut self) -> Result<Vec<Body<'static>>> { pub async fn idle_sync(&mut self) -> Result<Vec<Body<'static>>> {
self.0.sync().await?; self.internal
let state = self.0.peek().await; .mailbox
.notify()
.await
.upgrade()
.ok_or(anyhow!("test"))?
.notified()
.await;
self.internal.mailbox.opportunistic_sync().await?;
self.update(UpdateParameters::default()).await
}
pub async fn expunge(
&mut self,
maybe_seq_set: &Option<SequenceSet>,
) -> Result<Vec<Body<'static>>> {
// Get a recent view to apply our change
self.internal.sync().await?;
let state = self.internal.peek().await;
let idx = Index::new(&state)?;
// Build a default sequence set for the default case
use imap_codec::imap_types::sequence::{SeqOrUid, Sequence};
let seq = match maybe_seq_set {
Some(s) => s.clone(),
None => SequenceSet(
vec![Sequence::Range(
SeqOrUid::Value(NonZeroU32::MIN),
SeqOrUid::Asterisk,
)]
.try_into()
.unwrap(),
),
};
let deleted_flag = Flag::Deleted.to_string(); let deleted_flag = Flag::Deleted.to_string();
let msgs = state let msgs = idx
.table .fetch_on_uid(&seq)
.iter() .into_iter()
.filter(|(_uuid, (_uid, flags))| flags.iter().any(|x| *x == deleted_flag)) .filter(|midx| midx.flags.iter().any(|x| *x == deleted_flag))
.map(|(uuid, _)| *uuid); .map(|midx| midx.uuid);
for msg in msgs { for msg in msgs {
self.0.mailbox.delete(msg).await?; self.internal.mailbox.delete(msg).await?;
} }
self.update().await self.update(UpdateParameters::default()).await
} }
pub async fn copy( pub async fn copy(
@ -197,7 +285,7 @@ impl MailboxView {
let mut new_uuids = vec![]; let mut new_uuids = vec![];
for mi in mails.iter() { for mi in mails.iter() {
new_uuids.push(to.copy_from(&self.0.mailbox, mi.uuid).await?); new_uuids.push(to.copy_from(&self.internal.mailbox, mi.uuid).await?);
} }
let mut ret = vec![]; let mut ret = vec![];
@ -224,7 +312,7 @@ impl MailboxView {
let mails = idx.fetch(sequence_set, *is_uid_copy)?; let mails = idx.fetch(sequence_set, *is_uid_copy)?;
for mi in mails.iter() { for mi in mails.iter() {
to.move_from(&self.0.mailbox, mi.uuid).await?; to.move_from(&self.internal.mailbox, mi.uuid).await?;
} }
let mut ret = vec![]; let mut ret = vec![];
@ -238,7 +326,12 @@ impl MailboxView {
ret.push((mi.uid, dest_uid)); ret.push((mi.uid, dest_uid));
} }
let update = self.update().await?; let update = self
.update(UpdateParameters {
with_uid: *is_uid_copy,
..UpdateParameters::default()
})
.await?;
Ok((to_state.uidvalidity, ret, update)) Ok((to_state.uidvalidity, ret, update))
} }
@ -248,66 +341,57 @@ impl MailboxView {
pub async fn fetch<'b>( pub async fn fetch<'b>(
&self, &self,
sequence_set: &SequenceSet, sequence_set: &SequenceSet,
attributes: &'b MacroOrMessageDataItemNames<'static>, ap: &AttributesProxy,
changed_since: Option<NonZeroU64>,
is_uid_fetch: &bool, is_uid_fetch: &bool,
) -> Result<Vec<Body<'static>>> { ) -> Result<Vec<Body<'static>>> {
// [1/6] Pre-compute data // [1/6] Pre-compute data
// a. what are the uuids of the emails we want? // a. what are the uuids of the emails we want?
// b. do we need to fetch the full body? // b. do we need to fetch the full body?
let ap = AttributesProxy::new(attributes, *is_uid_fetch); //let ap = AttributesProxy::new(attributes, *is_uid_fetch);
let query_scope = match ap.need_body() { let query_scope = match ap.need_body() {
true => QueryScope::Full, true => QueryScope::Full,
_ => QueryScope::Partial, _ => QueryScope::Partial,
}; };
tracing::debug!("Query scope {:?}", query_scope); tracing::debug!("Query scope {:?}", query_scope);
let idx = self.index()?; let idx = self.index()?;
let mail_idx_list = idx.fetch(sequence_set, *is_uid_fetch)?; let mail_idx_list = idx.fetch_changed_since(sequence_set, changed_since, *is_uid_fetch)?;
// [2/6] Fetch the emails // [2/6] Fetch the emails
let uuids = mail_idx_list let uuids = mail_idx_list
.iter() .iter()
.map(|midx| midx.uuid) .map(|midx| midx.uuid)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let query_result = self.0.query(&uuids, query_scope).fetch().await?;
// [3/6] Derive an IMAP-specific view from the results, apply the filters let query = self.internal.query(&uuids, query_scope);
let views = query_result //let query_result = self.internal.query(&uuids, query_scope).fetch().await?;
.iter()
.zip(mail_idx_list.into_iter())
.map(|(qr, midx)| MailView::new(qr, midx))
.collect::<Result<Vec<_>, _>>()?;
// [4/6] Apply the IMAP transformation, bubble up any error let query_stream = query
// We get 2 results: .fetch()
// - The one we send to the client .zip(futures::stream::iter(mail_idx_list))
// - The \Seen flags we must set internally // [3/6] Derive an IMAP-specific view from the results, apply the filters
let (flag_mgmt, imap_ret): (Vec<_>, Vec<_>) = views .map(|(maybe_qr, midx)| match maybe_qr {
.iter() Ok(qr) => Ok((MailView::new(&qr, midx)?.filter(&ap)?, midx)),
.map(|mv| mv.filter(&ap).map(|(body, seen)| ((mv, seen), body))) Err(e) => Err(e),
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.unzip();
// [5/6] Register the \Seen flags
flag_mgmt
.iter()
.filter(|(_mv, seen)| matches!(seen, SeenFlag::MustAdd))
.map(|(mv, _seen)| async move {
let seen_flag = Flag::Seen.to_string();
self.0
.mailbox
.add_flags(*mv.query_result.uuid(), &[seen_flag])
.await?;
Ok::<_, anyhow::Error>(())
}) })
.collect::<FuturesOrdered<_>>() // [4/6] Apply the IMAP transformation
.collect::<Vec<_>>() .then(|maybe_ret| async move {
.await let ((body, seen), midx) = maybe_ret?;
.into_iter()
.collect::<Result<_, _>>()?; // [5/6] Register the \Seen flags
if matches!(seen, SeenFlag::MustAdd) {
let seen_flag = Flag::Seen.to_string();
self.internal
.mailbox
.add_flags(midx.uuid, &[seen_flag])
.await?;
}
Ok::<_, anyhow::Error>(body)
});
// [6/6] Build the final result that will be sent to the client. // [6/6] Build the final result that will be sent to the client.
Ok(imap_ret) query_stream.try_collect().await
} }
/// A naive search implementation... /// A naive search implementation...
@ -316,7 +400,7 @@ impl MailboxView {
_charset: &Option<Charset<'a>>, _charset: &Option<Charset<'a>>,
search_key: &SearchKey<'a>, search_key: &SearchKey<'a>,
uid: bool, uid: bool,
) -> Result<Vec<Body<'static>>> { ) -> Result<(Vec<Body<'static>>, bool)> {
// 1. Compute the subset of sequence identifiers we need to fetch // 1. Compute the subset of sequence identifiers we need to fetch
// based on the search query // based on the search query
let crit = search::Criteria(search_key); let crit = search::Criteria(search_key);
@ -329,23 +413,55 @@ impl MailboxView {
// 3. Filter the selection based on the ID / UID / Flags // 3. Filter the selection based on the ID / UID / Flags
let (kept_idx, to_fetch) = crit.filter_on_idx(&selection); let (kept_idx, to_fetch) = crit.filter_on_idx(&selection);
// 4. Fetch additional info about the emails // 4.a Fetch additional info about the emails
let query_scope = crit.query_scope(); let query_scope = crit.query_scope();
let uuids = to_fetch.iter().map(|midx| midx.uuid).collect::<Vec<_>>(); let uuids = to_fetch.iter().map(|midx| midx.uuid).collect::<Vec<_>>();
let query_result = self.0.query(&uuids, query_scope).fetch().await?; let query = self.internal.query(&uuids, query_scope);
// 5. If needed, filter the selection based on the body // 4.b We don't want to keep all data in memory, so we do the computing in a stream
let kept_query = crit.filter_on_query(&to_fetch, &query_result)?; let query_stream = query
.fetch()
.zip(futures::stream::iter(&to_fetch))
// 5.a Build a mailview with the body, might fail with an error
// 5.b If needed, filter the selection based on the body, but keep the errors
// 6. Drop the query+mailbox, keep only the mail index
// Here we release a lot of memory, this is the most important part ^^
.filter_map(|(maybe_qr, midx)| {
let r = match maybe_qr {
Ok(qr) => match MailView::new(&qr, midx).map(|mv| crit.is_keep_on_query(&mv)) {
Ok(true) => Some(Ok(*midx)),
Ok(_) => None,
Err(e) => Some(Err(e)),
},
Err(e) => Some(Err(e)),
};
futures::future::ready(r)
});
// 6. Format the result according to the client's taste: // 7. Chain both streams (part resolved from index, part resolved from metadata+body)
// either return UID or ID. let main_stream = futures::stream::iter(kept_idx)
let final_selection = kept_idx.into_iter().chain(kept_query.into_iter()); .map(Ok)
let selection_fmt = match uid { .chain(query_stream)
true => final_selection.map(|in_idx| in_idx.uid).collect(), .map_ok(|idx| match uid {
_ => final_selection.map(|in_idx| in_idx.i).collect(), true => (idx.uid, idx.modseq),
_ => (idx.i, idx.modseq),
});
// 8. Do the actual computation
let internal_result: Vec<_> = main_stream.try_collect().await?;
let (selection, modseqs): (Vec<_>, Vec<_>) = internal_result.into_iter().unzip();
// 9. Aggregate the maximum modseq value
let maybe_modseq = match crit.is_modseq() {
true => modseqs.into_iter().max(),
_ => None,
}; };
Ok(vec![Body::Data(Data::Search(selection_fmt))]) // 10. Return the final result
Ok((
vec![Body::Data(Data::Search(selection, maybe_modseq))],
maybe_modseq.is_some(),
))
} }
// ---- // ----
@ -354,7 +470,7 @@ impl MailboxView {
/// It's not trivial to refactor the code to do that, so we are doing /// It's not trivial to refactor the code to do that, so we are doing
/// some useless computation for now... /// some useless computation for now...
fn index<'a>(&'a self) -> Result<Index<'a>> { fn index<'a>(&'a self) -> Result<Index<'a>> {
Index::new(&self.0.snapshot) Index::new(&self.internal.snapshot)
} }
/// Produce an OK [UIDVALIDITY _] message corresponding to `known_state` /// Produce an OK [UIDVALIDITY _] message corresponding to `known_state`
@ -369,7 +485,7 @@ impl MailboxView {
} }
pub(crate) fn uidvalidity(&self) -> ImapUidvalidity { pub(crate) fn uidvalidity(&self) -> ImapUidvalidity {
self.0.snapshot.uidvalidity self.internal.snapshot.uidvalidity
} }
/// Produce an OK [UIDNEXT _] message corresponding to `known_state` /// Produce an OK [UIDNEXT _] message corresponding to `known_state`
@ -384,7 +500,21 @@ impl MailboxView {
} }
pub(crate) fn uidnext(&self) -> ImapUid { pub(crate) fn uidnext(&self) -> ImapUid {
self.0.snapshot.uidnext self.internal.snapshot.uidnext
}
pub(crate) fn highestmodseq_status(&self) -> Result<Body<'static>> {
Ok(Body::Status(Status::ok(
None,
Some(Code::Other(CodeOther::unvalidated(
format!("HIGHESTMODSEQ {}", self.highestmodseq()).into_bytes(),
))),
"Highest",
)?))
}
pub(crate) fn highestmodseq(&self) -> ModSeq {
self.internal.snapshot.highestmodseq
} }
/// Produce an EXISTS message corresponding to the number of mails /// Produce an EXISTS message corresponding to the number of mails
@ -394,7 +524,7 @@ impl MailboxView {
} }
pub(crate) fn exists(&self) -> Result<u32> { pub(crate) fn exists(&self) -> Result<u32> {
Ok(u32::try_from(self.0.snapshot.idx_by_uid.len())?) Ok(u32::try_from(self.internal.snapshot.idx_by_uid.len())?)
} }
/// Produce a RECENT message corresponding to the number of /// Produce a RECENT message corresponding to the number of
@ -403,6 +533,7 @@ impl MailboxView {
Ok(Body::Data(Data::Recent(self.recent()?))) Ok(Body::Data(Data::Recent(self.recent()?)))
} }
#[allow(dead_code)]
fn unseen_first_status(&self) -> Result<Option<Body<'static>>> { fn unseen_first_status(&self) -> Result<Option<Body<'static>>> {
Ok(self Ok(self
.unseen_first()? .unseen_first()?
@ -412,21 +543,22 @@ impl MailboxView {
.transpose()?) .transpose()?)
} }
#[allow(dead_code)]
fn unseen_first(&self) -> Result<Option<NonZeroU32>> { fn unseen_first(&self) -> Result<Option<NonZeroU32>> {
Ok(self Ok(self
.0 .internal
.snapshot .snapshot
.table .table
.values() .values()
.enumerate() .enumerate()
.find(|(_i, (_imap_uid, flags))| !flags.contains(&"\\Seen".to_string())) .find(|(_i, (_imap_uid, _modseq, flags))| !flags.contains(&"\\Seen".to_string()))
.map(|(i, _)| NonZeroU32::try_from(i as u32 + 1)) .map(|(i, _)| NonZeroU32::try_from(i as u32 + 1))
.transpose()?) .transpose()?)
} }
pub(crate) fn recent(&self) -> Result<u32> { pub(crate) fn recent(&self) -> Result<u32> {
let recent = self let recent = self
.0 .internal
.snapshot .snapshot
.idx_by_flag .idx_by_flag
.get(&"\\Recent".to_string()) .get(&"\\Recent".to_string())
@ -443,7 +575,7 @@ impl MailboxView {
// 1. Collecting all the possible flags in the mailbox // 1. Collecting all the possible flags in the mailbox
// 1.a Fetch them from our index // 1.a Fetch them from our index
let mut known_flags: Vec<Flag> = self let mut known_flags: Vec<Flag> = self
.0 .internal
.snapshot .snapshot
.idx_by_flag .idx_by_flag
.flags() .flags()
@ -483,9 +615,9 @@ impl MailboxView {
} }
pub(crate) fn unseen_count(&self) -> usize { pub(crate) fn unseen_count(&self) -> usize {
let total = self.0.snapshot.table.len(); let total = self.internal.snapshot.table.len();
let seen = self let seen = self
.0 .internal
.snapshot .snapshot
.idx_by_flag .idx_by_flag
.get(&Flag::Seen.to_string()) .get(&Flag::Seen.to_string())
@ -499,20 +631,20 @@ impl MailboxView {
mod tests { mod tests {
use super::*; use super::*;
use imap_codec::encode::Encoder; use imap_codec::encode::Encoder;
use imap_codec::imap_types::core::NonEmptyVec; use imap_codec::imap_types::core::Vec1;
use imap_codec::imap_types::fetch::Section; use imap_codec::imap_types::fetch::Section;
use imap_codec::imap_types::fetch::{MacroOrMessageDataItemNames, MessageDataItemName}; use imap_codec::imap_types::fetch::{MacroOrMessageDataItemNames, MessageDataItemName};
use imap_codec::imap_types::response::Response; use imap_codec::imap_types::response::Response;
use imap_codec::ResponseCodec; use imap_codec::ResponseCodec;
use std::fs; use std::fs;
use crate::cryptoblob; use aero_collections::mail::mailbox::MailMeta;
use aero_collections::mail::query::QueryResult;
use aero_collections::unique_ident;
use aero_user::cryptoblob;
use crate::imap::index::MailIndex; use crate::imap::index::MailIndex;
use crate::imap::mail_view::MailView;
use crate::imap::mime_view; use crate::imap::mime_view;
use crate::mail::mailbox::MailMeta;
use crate::mail::query::QueryResult;
use crate::mail::unique_ident;
#[test] #[test]
fn mailview_body_ext() -> Result<()> { fn mailview_body_ext() -> Result<()> {
@ -524,6 +656,7 @@ mod tests {
peek: false, peek: false,
}, },
]), ]),
&[],
false, false,
); );
@ -535,12 +668,13 @@ mod tests {
rfc822_size: 8usize, rfc822_size: 8usize,
}; };
let index_entry = (NonZeroU32::MIN, vec![]); let index_entry = (NonZeroU32::MIN, NonZeroU64::MIN, vec![]);
let mail_in_idx = MailIndex { let mail_in_idx = MailIndex {
i: NonZeroU32::MIN, i: NonZeroU32::MIN,
uid: index_entry.0, uid: index_entry.0,
modseq: index_entry.1,
uuid: unique_ident::gen_ident(), uuid: unique_ident::gen_ident(),
flags: &index_entry.1, flags: &index_entry.2,
}; };
let rfc822 = b"Subject: hello\r\nFrom: a@a.a\r\nTo: b@b.b\r\nDate: Thu, 12 Oct 2023 08:45:28 +0000\r\n\r\nhello world"; let rfc822 = b"Subject: hello\r\nFrom: a@a.a\r\nTo: b@b.b\r\nDate: Thu, 12 Oct 2023 08:45:28 +0000\r\n\r\nhello world";
let qr = QueryResult::FullResult { let qr = QueryResult::FullResult {
@ -611,13 +745,13 @@ mod tests {
for pref in prefixes.iter() { for pref in prefixes.iter() {
println!("{}", pref); println!("{}", pref);
let txt = fs::read(format!("{}.eml", pref))?; let txt = fs::read(format!("../{}.eml", pref))?;
let oracle = fs::read(format!("{}.dovecot.body", pref))?; let oracle = fs::read(format!("../{}.dovecot.body", pref))?;
let message = eml_codec::parse_message(&txt).unwrap().1; let message = eml_codec::parse_message(&txt).unwrap().1;
let test_repr = Response::Data(Data::Fetch { let test_repr = Response::Data(Data::Fetch {
seq: NonZeroU32::new(1).unwrap(), seq: NonZeroU32::new(1).unwrap(),
items: NonEmptyVec::from(MessageDataItem::Body(mime_view::bodystructure( items: Vec1::from(MessageDataItem::Body(mime_view::bodystructure(
&message.child, &message.child,
false, false,
)?)), )?)),

View file

@ -8,7 +8,7 @@ use imap_codec::imap_types::body::{
BasicFields, Body as FetchBody, BodyStructure, MultiPartExtensionData, SinglePartExtensionData, BasicFields, Body as FetchBody, BodyStructure, MultiPartExtensionData, SinglePartExtensionData,
SpecificFields, SpecificFields,
}; };
use imap_codec::imap_types::core::{AString, IString, NString, NonEmptyVec}; use imap_codec::imap_types::core::{AString, IString, NString, Vec1};
use imap_codec::imap_types::fetch::{Part as FetchPart, Section as FetchSection}; use imap_codec::imap_types::fetch::{Part as FetchPart, Section as FetchSection};
use eml_codec::{ use eml_codec::{
@ -33,7 +33,7 @@ pub enum BodySection<'a> {
/// ///
/// Example of message sections: /// Example of message sections:
/// ///
/// ``` /// ```text
/// HEADER ([RFC-2822] header of the message) /// HEADER ([RFC-2822] header of the message)
/// TEXT ([RFC-2822] text body of the message) MULTIPART/MIXED /// TEXT ([RFC-2822] text body of the message) MULTIPART/MIXED
/// 1 TEXT/PLAIN /// 1 TEXT/PLAIN
@ -141,8 +141,8 @@ impl<'a> NodeMime<'a> {
enum SubsettedSection<'a> { enum SubsettedSection<'a> {
Part, Part,
Header, Header,
HeaderFields(&'a NonEmptyVec<AString<'a>>), HeaderFields(&'a Vec1<AString<'a>>),
HeaderFieldsNot(&'a NonEmptyVec<AString<'a>>), HeaderFieldsNot(&'a Vec1<AString<'a>>),
Text, Text,
Mime, Mime,
} }
@ -238,7 +238,7 @@ impl<'a> SelectedMime<'a> {
/// case-insensitive but otherwise exact. /// case-insensitive but otherwise exact.
fn header_fields( fn header_fields(
&self, &self,
fields: &'a NonEmptyVec<AString<'a>>, fields: &'a Vec1<AString<'a>>,
invert: bool, invert: bool,
) -> Result<ExtractedFull<'a>> { ) -> Result<ExtractedFull<'a>> {
// Build a lowercase ascii hashset with the fields to fetch // Build a lowercase ascii hashset with the fields to fetch
@ -384,6 +384,8 @@ impl<'a> NodeMsg<'a> {
}) })
} }
} }
#[allow(dead_code)]
struct NodeMult<'a>(&'a NodeMime<'a>, &'a composite::Multipart<'a>); struct NodeMult<'a>(&'a NodeMime<'a>, &'a composite::Multipart<'a>);
impl<'a> NodeMult<'a> { impl<'a> NodeMult<'a> {
fn structure(&self, is_ext: bool) -> Result<BodyStructure<'static>> { fn structure(&self, is_ext: bool) -> Result<BodyStructure<'static>> {
@ -398,8 +400,8 @@ impl<'a> NodeMult<'a> {
.filter_map(|inner| NodeMime(&inner).structure(is_ext).ok()) .filter_map(|inner| NodeMime(&inner).structure(is_ext).ok())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
NonEmptyVec::validate(&inner_bodies)?; Vec1::validate(&inner_bodies)?;
let bodies = NonEmptyVec::unvalidated(inner_bodies); let bodies = Vec1::unvalidated(inner_bodies);
Ok(BodyStructure::Multi { Ok(BodyStructure::Multi {
bodies, bodies,

336
aero-proto/src/imap/mod.rs Normal file
View file

@ -0,0 +1,336 @@
mod attributes;
mod capability;
mod command;
mod flags;
mod flow;
mod imf_view;
mod index;
mod mail_view;
mod mailbox_view;
mod mime_view;
mod request;
mod response;
mod search;
mod session;
use std::net::SocketAddr;
use anyhow::{anyhow, bail, Result};
use futures::stream::{FuturesUnordered, StreamExt};
use imap_codec::imap_types::response::{Code, CommandContinuationRequest, Response, Status};
use imap_codec::imap_types::{core::Text, response::Greeting};
use imap_flow::server::{ServerFlow, ServerFlowEvent, ServerFlowOptions};
use imap_flow::stream::AnyStream;
use rustls_pemfile::{certs, private_key};
use tokio::net::TcpListener;
use tokio::sync::mpsc;
use tokio::sync::watch;
use tokio_rustls::TlsAcceptor;
use aero_user::config::{ImapConfig, ImapUnsecureConfig};
use aero_user::login::ArcLoginProvider;
use crate::imap::capability::ServerCapability;
use crate::imap::request::Request;
use crate::imap::response::{Body, ResponseOrIdle};
use crate::imap::session::Instance;
/// Server is a thin wrapper to register our Services in BàL
pub struct Server {
bind_addr: SocketAddr,
login_provider: ArcLoginProvider,
capabilities: ServerCapability,
tls: Option<TlsAcceptor>,
}
#[derive(Clone)]
struct ClientContext {
addr: SocketAddr,
login_provider: ArcLoginProvider,
must_exit: watch::Receiver<bool>,
server_capabilities: ServerCapability,
}
pub fn new(config: ImapConfig, login: ArcLoginProvider) -> Result<Server> {
let loaded_certs = certs(&mut std::io::BufReader::new(std::fs::File::open(
config.certs,
)?))
.collect::<Result<Vec<_>, _>>()?;
let loaded_key = private_key(&mut std::io::BufReader::new(std::fs::File::open(
config.key,
)?))?
.unwrap();
let tls_config = rustls::ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(loaded_certs, loaded_key)?;
let acceptor = TlsAcceptor::from(Arc::new(tls_config));
Ok(Server {
bind_addr: config.bind_addr,
login_provider: login,
capabilities: ServerCapability::default(),
tls: Some(acceptor),
})
}
pub fn new_unsecure(config: ImapUnsecureConfig, login: ArcLoginProvider) -> Server {
Server {
bind_addr: config.bind_addr,
login_provider: login,
capabilities: ServerCapability::default(),
tls: None,
}
}
impl Server {
pub async fn run(self: Self, mut must_exit: watch::Receiver<bool>) -> Result<()> {
let tcp = TcpListener::bind(self.bind_addr).await?;
tracing::info!("IMAP server listening on {:#}", self.bind_addr);
let mut connections = FuturesUnordered::new();
while !*must_exit.borrow() {
let wait_conn_finished = async {
if connections.is_empty() {
futures::future::pending().await
} else {
connections.next().await
}
};
let (socket, remote_addr) = tokio::select! {
a = tcp.accept() => a?,
_ = wait_conn_finished => continue,
_ = must_exit.changed() => continue,
};
tracing::info!("IMAP: accepted connection from {}", remote_addr);
let stream = match self.tls.clone() {
Some(acceptor) => {
let stream = match acceptor.accept(socket).await {
Ok(v) => v,
Err(e) => {
tracing::error!(err=?e, "TLS negociation failed");
continue;
}
};
AnyStream::new(stream)
}
None => AnyStream::new(socket),
};
let client = ClientContext {
addr: remote_addr.clone(),
login_provider: self.login_provider.clone(),
must_exit: must_exit.clone(),
server_capabilities: self.capabilities.clone(),
};
let conn = tokio::spawn(NetLoop::handler(client, stream));
connections.push(conn);
}
drop(tcp);
tracing::info!("IMAP server shutting down, draining remaining connections...");
while connections.next().await.is_some() {}
Ok(())
}
}
use std::sync::Arc;
use tokio::sync::mpsc::*;
use tokio::sync::Notify;
const PIPELINABLE_COMMANDS: usize = 64;
// @FIXME a full refactor of this part of the code will be needed sooner or later
struct NetLoop {
ctx: ClientContext,
server: ServerFlow,
cmd_tx: Sender<Request>,
resp_rx: UnboundedReceiver<ResponseOrIdle>,
}
impl NetLoop {
async fn handler(ctx: ClientContext, sock: AnyStream) {
let addr = ctx.addr.clone();
let mut nl = match Self::new(ctx, sock).await {
Ok(nl) => {
tracing::debug!(addr=?addr, "netloop successfully initialized");
nl
}
Err(e) => {
tracing::error!(addr=?addr, err=?e, "netloop can not be initialized, closing session");
return;
}
};
match nl.core().await {
Ok(()) => {
tracing::debug!("closing successful netloop core for {:?}", addr);
}
Err(e) => {
tracing::error!("closing errored netloop core for {:?}: {}", addr, e);
}
}
}
async fn new(ctx: ClientContext, sock: AnyStream) -> Result<Self> {
let mut opts = ServerFlowOptions::default();
opts.crlf_relaxed = false;
opts.literal_accept_text = Text::unvalidated("OK");
opts.literal_reject_text = Text::unvalidated("Literal rejected");
// Send greeting
let (server, _) = ServerFlow::send_greeting(
sock,
opts,
Greeting::ok(
Some(Code::Capability(ctx.server_capabilities.to_vec())),
"Aerogramme",
)
.unwrap(),
)
.await?;
// Start a mailbox session in background
let (cmd_tx, cmd_rx) = mpsc::channel::<Request>(PIPELINABLE_COMMANDS);
let (resp_tx, resp_rx) = mpsc::unbounded_channel::<ResponseOrIdle>();
tokio::spawn(Self::session(ctx.clone(), cmd_rx, resp_tx));
// Return the object
Ok(NetLoop {
ctx,
server,
cmd_tx,
resp_rx,
})
}
/// Coms with the background session
async fn session(
ctx: ClientContext,
mut cmd_rx: Receiver<Request>,
resp_tx: UnboundedSender<ResponseOrIdle>,
) -> () {
let mut session = Instance::new(ctx.login_provider, ctx.server_capabilities);
loop {
let cmd = match cmd_rx.recv().await {
None => break,
Some(cmd_recv) => cmd_recv,
};
tracing::debug!(cmd=?cmd, sock=%ctx.addr, "command");
let maybe_response = session.request(cmd).await;
tracing::debug!(cmd=?maybe_response, sock=%ctx.addr, "response");
match resp_tx.send(maybe_response) {
Err(_) => break,
Ok(_) => (),
};
}
tracing::info!("runner is quitting");
}
async fn core(&mut self) -> Result<()> {
let mut maybe_idle: Option<Arc<Notify>> = None;
loop {
tokio::select! {
// Managing imap_flow stuff
srv_evt = self.server.progress() => match srv_evt? {
ServerFlowEvent::ResponseSent { handle: _handle, response } => {
match response {
Response::Status(Status::Bye(_)) => return Ok(()),
_ => tracing::trace!("sent to {} content {:?}", self.ctx.addr, response),
}
},
ServerFlowEvent::CommandReceived { command } => {
match self.cmd_tx.try_send(Request::ImapCommand(command)) {
Ok(_) => (),
Err(mpsc::error::TrySendError::Full(_)) => {
self.server.enqueue_status(Status::bye(None, "Too fast").unwrap());
tracing::error!("client {:?} is sending commands too fast, closing.", self.ctx.addr);
}
_ => {
self.server.enqueue_status(Status::bye(None, "Internal session exited").unwrap());
tracing::error!("session task exited for {:?}, quitting", self.ctx.addr);
}
}
},
ServerFlowEvent::IdleCommandReceived { tag } => {
match self.cmd_tx.try_send(Request::IdleStart(tag)) {
Ok(_) => (),
Err(mpsc::error::TrySendError::Full(_)) => {
self.server.enqueue_status(Status::bye(None, "Too fast").unwrap());
tracing::error!("client {:?} is sending commands too fast, closing.", self.ctx.addr);
}
_ => {
self.server.enqueue_status(Status::bye(None, "Internal session exited").unwrap());
tracing::error!("session task exited for {:?}, quitting", self.ctx.addr);
}
}
}
ServerFlowEvent::IdleDoneReceived => {
tracing::trace!("client sent DONE and want to stop IDLE");
maybe_idle.ok_or(anyhow!("Received IDLE done but not idling currently"))?.notify_one();
maybe_idle = None;
}
flow => {
self.server.enqueue_status(Status::bye(None, "Unsupported server flow event").unwrap());
tracing::error!("session task exited for {:?} due to unsupported flow {:?}", self.ctx.addr, flow);
}
},
// Managing response generated by Aerogramme
maybe_msg = self.resp_rx.recv() => match maybe_msg {
Some(ResponseOrIdle::Response(response)) => {
tracing::trace!("Interactive, server has a response for the client");
for body_elem in response.body.into_iter() {
let _handle = match body_elem {
Body::Data(d) => self.server.enqueue_data(d),
Body::Status(s) => self.server.enqueue_status(s),
};
}
self.server.enqueue_status(response.completion);
},
Some(ResponseOrIdle::IdleAccept(stop)) => {
tracing::trace!("Interactive, server agreed to switch in idle mode");
let cr = CommandContinuationRequest::basic(None, "Idling")?;
self.server.idle_accept(cr).or(Err(anyhow!("refused continuation for idle accept")))?;
self.cmd_tx.try_send(Request::IdlePoll)?;
if maybe_idle.is_some() {
bail!("Can't start IDLE if already idling");
}
maybe_idle = Some(stop);
},
Some(ResponseOrIdle::IdleEvent(elems)) => {
tracing::trace!("server imap session has some change to communicate to the client");
for body_elem in elems.into_iter() {
let _handle = match body_elem {
Body::Data(d) => self.server.enqueue_data(d),
Body::Status(s) => self.server.enqueue_status(s),
};
}
self.cmd_tx.try_send(Request::IdlePoll)?;
},
Some(ResponseOrIdle::IdleReject(response)) => {
tracing::trace!("inform client that session rejected idle");
self.server
.idle_reject(response.completion)
.or(Err(anyhow!("wrong reject command")))?;
},
None => {
self.server.enqueue_status(Status::bye(None, "Internal session exited").unwrap());
tracing::error!("session task exited for {:?}, quitting", self.ctx.addr);
},
},
// When receiving a CTRL+C
_ = self.ctx.must_exit.changed() => {
tracing::trace!("Interactive, CTRL+C, exiting");
self.server.enqueue_status(Status::bye(None, "Server is being shutdown").unwrap());
},
};
}
}
}

View file

@ -0,0 +1,9 @@
use imap_codec::imap_types::command::Command;
use imap_codec::imap_types::core::Tag;
#[derive(Debug)]
pub enum Request {
ImapCommand(Command<'static>),
IdleStart(Tag<'static>),
IdlePoll,
}

View file

@ -2,7 +2,10 @@ use anyhow::Result;
use imap_codec::imap_types::command::Command; use imap_codec::imap_types::command::Command;
use imap_codec::imap_types::core::Tag; use imap_codec::imap_types::core::Tag;
use imap_codec::imap_types::response::{Code, Data, Status}; use imap_codec::imap_types::response::{Code, Data, Status};
use std::sync::Arc;
use tokio::sync::Notify;
#[derive(Debug)]
pub enum Body<'a> { pub enum Body<'a> {
Data(Data<'a>), Data(Data<'a>),
Status(Status<'a>), Status(Status<'a>),
@ -88,6 +91,7 @@ impl<'a> ResponseBuilder<'a> {
} }
} }
#[derive(Debug)]
pub struct Response<'a> { pub struct Response<'a> {
pub body: Vec<Body<'a>>, pub body: Vec<Body<'a>>,
pub completion: Status<'a>, pub completion: Status<'a>,
@ -110,3 +114,11 @@ impl<'a> Response<'a> {
}) })
} }
} }
#[derive(Debug)]
pub enum ResponseOrIdle {
Response(Response<'static>),
IdleAccept(Arc<Notify>),
IdleReject(Response<'static>),
IdleEvent(Vec<Body<'static>>),
}

View file

@ -1,13 +1,13 @@
use std::num::NonZeroU32; use std::num::{NonZeroU32, NonZeroU64};
use anyhow::Result; use imap_codec::imap_types::core::Vec1;
use imap_codec::imap_types::core::NonEmptyVec; use imap_codec::imap_types::search::{MetadataItemSearch, SearchKey};
use imap_codec::imap_types::search::SearchKey;
use imap_codec::imap_types::sequence::{SeqOrUid, Sequence, SequenceSet}; use imap_codec::imap_types::sequence::{SeqOrUid, Sequence, SequenceSet};
use aero_collections::mail::query::QueryScope;
use crate::imap::index::MailIndex; use crate::imap::index::MailIndex;
use crate::imap::mail_view::MailView; use crate::imap::mail_view::MailView;
use crate::mail::query::{QueryResult, QueryScope};
pub enum SeqType { pub enum SeqType {
Undefined, Undefined,
@ -49,7 +49,7 @@ impl<'a> Criteria<'a> {
let mut new_vec = base.0.into_inner(); let mut new_vec = base.0.into_inner();
new_vec.extend_from_slice(ext.0.as_ref()); new_vec.extend_from_slice(ext.0.as_ref());
let seq = SequenceSet( let seq = SequenceSet(
NonEmptyVec::try_from(new_vec) Vec1::try_from(new_vec)
.expect("merging non empty vec lead to non empty vec"), .expect("merging non empty vec lead to non empty vec"),
); );
(seq, x) (seq, x)
@ -112,6 +112,20 @@ impl<'a> Criteria<'a> {
} }
} }
pub fn is_modseq(&self) -> bool {
use SearchKey::*;
match self.0 {
And(and_list) => and_list
.as_ref()
.iter()
.any(|child| Criteria(child).is_modseq()),
Or(left, right) => Criteria(left).is_modseq() || Criteria(right).is_modseq(),
Not(child) => Criteria(child).is_modseq(),
ModSeq { .. } => true,
_ => false,
}
}
/// Returns emails that we now for sure we want to keep /// Returns emails that we now for sure we want to keep
/// but also a second list of emails we need to investigate further by /// but also a second list of emails we need to investigate further by
/// fetching some remote data /// fetching some remote data
@ -131,22 +145,6 @@ impl<'a> Criteria<'a> {
(to_keep, to_fetch) (to_keep, to_fetch)
} }
pub fn filter_on_query<'b>(
&self,
midx_list: &[&'b MailIndex<'b>],
query_result: &'b Vec<QueryResult>,
) -> Result<Vec<&'b MailIndex<'b>>> {
Ok(midx_list
.iter()
.zip(query_result.iter())
.map(|(midx, qr)| MailView::new(qr, midx))
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.filter(|mail_view| self.is_keep_on_query(mail_view))
.map(|mail_view| mail_view.in_idx)
.collect())
}
// ---- // ----
/// Here we are doing a partial filtering: we do not have access /// Here we are doing a partial filtering: we do not have access
@ -176,6 +174,10 @@ impl<'a> Criteria<'a> {
// Sequence logic // Sequence logic
maybe_seq if is_sk_seq(maybe_seq) => is_keep_seq(maybe_seq, midx).into(), maybe_seq if is_sk_seq(maybe_seq) => is_keep_seq(maybe_seq, midx).into(),
maybe_flag if is_sk_flag(maybe_flag) => is_keep_flag(maybe_flag, midx).into(), maybe_flag if is_sk_flag(maybe_flag) => is_keep_flag(maybe_flag, midx).into(),
ModSeq {
metadata_item,
modseq,
} => is_keep_modseq(metadata_item, modseq, midx).into(),
// All the stuff we can't evaluate yet // All the stuff we can't evaluate yet
Bcc(_) | Cc(_) | From(_) | Header(..) | SentBefore(_) | SentOn(_) | SentSince(_) Bcc(_) | Cc(_) | From(_) | Header(..) | SentBefore(_) | SentOn(_) | SentSince(_)
@ -195,7 +197,7 @@ impl<'a> Criteria<'a> {
/// the email, as body(x) might be false. So we need to check it. But as seqid(x) is true, /// the email, as body(x) might be false. So we need to check it. But as seqid(x) is true,
/// we could simplify the request to just body(x) and truncate the first OR. Today, we are /// we could simplify the request to just body(x) and truncate the first OR. Today, we are
/// not doing that, and thus we reevaluate everything. /// not doing that, and thus we reevaluate everything.
fn is_keep_on_query(&self, mail_view: &MailView) -> bool { pub fn is_keep_on_query(&self, mail_view: &MailView) -> bool {
use SearchKey::*; use SearchKey::*;
match self.0 { match self.0 {
// Combinator logic // Combinator logic
@ -210,9 +212,13 @@ impl<'a> Criteria<'a> {
Not(expr) => !Criteria(expr).is_keep_on_query(mail_view), Not(expr) => !Criteria(expr).is_keep_on_query(mail_view),
All => true, All => true,
// Reevaluating our previous logic... //@FIXME Reevaluating our previous logic...
maybe_seq if is_sk_seq(maybe_seq) => is_keep_seq(maybe_seq, &mail_view.in_idx), maybe_seq if is_sk_seq(maybe_seq) => is_keep_seq(maybe_seq, &mail_view.in_idx),
maybe_flag if is_sk_flag(maybe_flag) => is_keep_flag(maybe_flag, &mail_view.in_idx), maybe_flag if is_sk_flag(maybe_flag) => is_keep_flag(maybe_flag, &mail_view.in_idx),
ModSeq {
metadata_item,
modseq,
} => is_keep_modseq(metadata_item, modseq, &mail_view.in_idx).into(),
// Filter on mail meta // Filter on mail meta
Before(search_naive) => match mail_view.stored_naive_date() { Before(search_naive) => match mail_view.stored_naive_date() {
@ -319,6 +325,7 @@ fn approx_sequence_set_size(seq_set: &SequenceSet) -> u64 {
// This is wrong as sequence UID can have holes, // This is wrong as sequence UID can have holes,
// as we don't know the number of messages in the mailbox also // as we don't know the number of messages in the mailbox also
// we gave to guess
fn approx_sequence_size(seq: &Sequence) -> u64 { fn approx_sequence_size(seq: &Sequence) -> u64 {
match seq { match seq {
Sequence::Single(_) => 1, Sequence::Single(_) => 1,
@ -458,3 +465,14 @@ fn is_keep_seq(sk: &SearchKey, midx: &MailIndex) -> bool {
_ => unreachable!(), _ => unreachable!(),
} }
} }
fn is_keep_modseq(
filter: &Option<MetadataItemSearch>,
modseq: &NonZeroU64,
midx: &MailIndex,
) -> bool {
if filter.is_some() {
tracing::warn!(filter=?filter, "Ignoring search metadata filter as it's not supported yet");
}
modseq <= &midx.modseq
}

View file

@ -0,0 +1,175 @@
use anyhow::{anyhow, bail, Context, Result};
use imap_codec::imap_types::{command::Command, core::Tag};
use aero_user::login::ArcLoginProvider;
use crate::imap::capability::{ClientCapability, ServerCapability};
use crate::imap::command::{anonymous, authenticated, selected};
use crate::imap::flow;
use crate::imap::request::Request;
use crate::imap::response::{Response, ResponseOrIdle};
//-----
pub struct Instance {
pub login_provider: ArcLoginProvider,
pub server_capabilities: ServerCapability,
pub client_capabilities: ClientCapability,
pub state: flow::State,
}
impl Instance {
pub fn new(login_provider: ArcLoginProvider, cap: ServerCapability) -> Self {
let client_cap = ClientCapability::new(&cap);
Self {
login_provider,
state: flow::State::NotAuthenticated,
server_capabilities: cap,
client_capabilities: client_cap,
}
}
pub async fn request(&mut self, req: Request) -> ResponseOrIdle {
match req {
Request::IdleStart(tag) => self.idle_init(tag),
Request::IdlePoll => self.idle_poll().await,
Request::ImapCommand(cmd) => self.command(cmd).await,
}
}
pub fn idle_init(&mut self, tag: Tag<'static>) -> ResponseOrIdle {
// Build transition
//@FIXME the notifier should be hidden inside the state and thus not part of the transition!
let transition = flow::Transition::Idle(tag.clone(), tokio::sync::Notify::new());
// Try to apply the transition and get the stop notifier
let maybe_stop = self
.state
.apply(transition)
.context("IDLE transition failed")
.and_then(|_| {
self.state
.notify()
.ok_or(anyhow!("IDLE state has no Notify object"))
});
// Build an appropriate response
match maybe_stop {
Ok(stop) => ResponseOrIdle::IdleAccept(stop),
Err(e) => {
tracing::error!(err=?e, "unable to init idle due to a transition error");
//ResponseOrIdle::IdleReject(tag)
let no = Response::build()
.tag(tag)
.message(
"Internal error, processing command triggered an illegal IMAP state transition",
)
.no()
.unwrap();
ResponseOrIdle::IdleReject(no)
}
}
}
pub async fn idle_poll(&mut self) -> ResponseOrIdle {
match self.idle_poll_happy().await {
Ok(r) => r,
Err(e) => {
tracing::error!(err=?e, "something bad happened in idle");
ResponseOrIdle::Response(Response::bye().unwrap())
}
}
}
pub async fn idle_poll_happy(&mut self) -> Result<ResponseOrIdle> {
let (mbx, tag, stop) = match &mut self.state {
flow::State::Idle(_, ref mut mbx, _, tag, stop) => (mbx, tag.clone(), stop.clone()),
_ => bail!("Invalid session state, can't idle"),
};
tokio::select! {
_ = stop.notified() => {
self.state.apply(flow::Transition::UnIdle)?;
return Ok(ResponseOrIdle::Response(Response::build()
.tag(tag.clone())
.message("IDLE completed")
.ok()?))
},
change = mbx.idle_sync() => {
tracing::debug!("idle event");
return Ok(ResponseOrIdle::IdleEvent(change?));
}
}
}
pub async fn command(&mut self, cmd: Command<'static>) -> ResponseOrIdle {
// Command behavior is modulated by the state.
// To prevent state error, we handle the same command in separate code paths.
let (resp, tr) = match &mut self.state {
flow::State::NotAuthenticated => {
let ctx = anonymous::AnonymousContext {
req: &cmd,
login_provider: &self.login_provider,
server_capabilities: &self.server_capabilities,
};
anonymous::dispatch(ctx).await
}
flow::State::Authenticated(ref user) => {
let ctx = authenticated::AuthenticatedContext {
req: &cmd,
server_capabilities: &self.server_capabilities,
client_capabilities: &mut self.client_capabilities,
user,
};
authenticated::dispatch(ctx).await
}
flow::State::Selected(ref user, ref mut mailbox, ref perm) => {
let ctx = selected::SelectedContext {
req: &cmd,
server_capabilities: &self.server_capabilities,
client_capabilities: &mut self.client_capabilities,
user,
mailbox,
perm,
};
selected::dispatch(ctx).await
}
flow::State::Idle(..) => Err(anyhow!("can not receive command while idling")),
flow::State::Logout => Response::build()
.tag(cmd.tag.clone())
.message("No commands are allowed in the LOGOUT state.")
.bad()
.map(|r| (r, flow::Transition::None)),
}
.unwrap_or_else(|err| {
tracing::error!("Command error {:?} occured while processing {:?}", err, cmd);
(
Response::build()
.to_req(&cmd)
.message("Internal error while processing command")
.bad()
.unwrap(),
flow::Transition::None,
)
});
if let Err(e) = self.state.apply(tr) {
tracing::error!(
"Transition error {:?} occured while processing on command {:?}",
e,
cmd
);
return ResponseOrIdle::Response(Response::build()
.to_req(&cmd)
.message(
"Internal error, processing command triggered an illegal IMAP state transition",
)
.bad()
.unwrap());
}
ResponseOrIdle::Response(resp)
/*match &self.state {
flow::State::Idle(_, _, _, _, n) => ResponseOrIdle::StartIdle(n.clone()),
_ => ResponseOrIdle::Response(resp),
}*/
}
}

6
aero-proto/src/lib.rs Normal file
View file

@ -0,0 +1,6 @@
#![feature(async_closure)]
pub mod dav;
pub mod imap;
pub mod lmtp;
pub mod sasl;

View file

@ -10,18 +10,16 @@ use futures::{
stream::{FuturesOrdered, FuturesUnordered}, stream::{FuturesOrdered, FuturesUnordered},
StreamExt, StreamExt,
}; };
use log::*; use smtp_message::{DataUnescaper, Email, EscapedDataReader, Reply, ReplyCode};
use smtp_server::{reply, Config, ConnectionMetadata, Decision, MailMetadata};
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tokio::select; use tokio::select;
use tokio::sync::watch; use tokio::sync::watch;
use tokio_util::compat::*; use tokio_util::compat::*;
use smtp_message::{Email, EscapedDataReader, Reply, ReplyCode}; use aero_collections::mail::incoming::EncryptedMessage;
use smtp_server::{reply, Config, ConnectionMetadata, Decision, MailMetadata}; use aero_user::config::*;
use aero_user::login::*;
use crate::config::*;
use crate::login::*;
use crate::mail::incoming::EncryptedMessage;
pub struct LmtpServer { pub struct LmtpServer {
bind_addr: SocketAddr, bind_addr: SocketAddr,
@ -43,7 +41,7 @@ impl LmtpServer {
pub async fn run(self: &Arc<Self>, mut must_exit: watch::Receiver<bool>) -> Result<()> { pub async fn run(self: &Arc<Self>, mut must_exit: watch::Receiver<bool>) -> Result<()> {
let tcp = TcpListener::bind(self.bind_addr).await?; let tcp = TcpListener::bind(self.bind_addr).await?;
info!("LMTP server listening on {:#}", self.bind_addr); tracing::info!("LMTP server listening on {:#}", self.bind_addr);
let mut connections = FuturesUnordered::new(); let mut connections = FuturesUnordered::new();
@ -60,7 +58,7 @@ impl LmtpServer {
_ = wait_conn_finished => continue, _ = wait_conn_finished => continue,
_ = must_exit.changed() => continue, _ = must_exit.changed() => continue,
}; };
info!("LMTP: accepted connection from {}", remote_addr); tracing::info!("LMTP: accepted connection from {}", remote_addr);
let conn = tokio::spawn(smtp_server::interact( let conn = tokio::spawn(smtp_server::interact(
socket.compat(), socket.compat(),
@ -73,7 +71,7 @@ impl LmtpServer {
} }
drop(tcp); drop(tcp);
info!("LMTP server shutting down, draining remaining connections..."); tracing::info!("LMTP server shutting down, draining remaining connections...");
while connections.next().await.is_some() {} while connections.next().await.is_some() {}
Ok(()) Ok(())
@ -181,6 +179,12 @@ impl Config for LmtpServer {
return err_response_stream(meta, format!("io error: {}", e)); return err_response_stream(meta, format!("io error: {}", e));
} }
reader.complete(); reader.complete();
let raw_size = text.len();
// Unescape email, shrink it also to remove last dot
let unesc_res = DataUnescaper::new(true).unescape(&mut text);
text.truncate(unesc_res.written);
tracing::debug!(prev_sz = raw_size, new_sz = text.len(), "unescaped");
let encrypted_message = match EncryptedMessage::new(text) { let encrypted_message = match EncryptedMessage::new(text) {
Ok(x) => Arc::new(x), Ok(x) => Arc::new(x),

142
aero-proto/src/sasl.rs Normal file
View file

@ -0,0 +1,142 @@
use std::net::SocketAddr;
use anyhow::{anyhow, bail, Result};
use futures::stream::{FuturesUnordered, StreamExt};
use tokio::io::BufStream;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::watch;
use tokio_util::bytes::BytesMut;
use aero_sasl::{decode::client_command, encode::Encode, flow::State};
use aero_user::config::AuthConfig;
use aero_user::login::ArcLoginProvider;
pub struct AuthServer {
login_provider: ArcLoginProvider,
bind_addr: SocketAddr,
}
impl AuthServer {
pub fn new(config: AuthConfig, login_provider: ArcLoginProvider) -> Self {
Self {
bind_addr: config.bind_addr,
login_provider,
}
}
pub async fn run(self: Self, mut must_exit: watch::Receiver<bool>) -> Result<()> {
let tcp = TcpListener::bind(self.bind_addr).await?;
tracing::info!(
"SASL Authentication Protocol listening on {:#}",
self.bind_addr
);
let mut connections = FuturesUnordered::new();
while !*must_exit.borrow() {
let wait_conn_finished = async {
if connections.is_empty() {
futures::future::pending().await
} else {
connections.next().await
}
};
let (socket, remote_addr) = tokio::select! {
a = tcp.accept() => a?,
_ = wait_conn_finished => continue,
_ = must_exit.changed() => continue,
};
tracing::info!("AUTH: accepted connection from {}", remote_addr);
let conn = tokio::spawn(
NetLoop::new(socket, self.login_provider.clone(), must_exit.clone()).run_error(),
);
connections.push(conn);
}
drop(tcp);
tracing::info!("AUTH server shutting down, draining remaining connections...");
while connections.next().await.is_some() {}
Ok(())
}
}
struct NetLoop {
login: ArcLoginProvider,
stream: BufStream<TcpStream>,
stop: watch::Receiver<bool>,
state: State,
read_buf: Vec<u8>,
write_buf: BytesMut,
}
impl NetLoop {
fn new(stream: TcpStream, login: ArcLoginProvider, stop: watch::Receiver<bool>) -> Self {
Self {
login,
stream: BufStream::new(stream),
state: State::Init,
stop,
read_buf: Vec::new(),
write_buf: BytesMut::new(),
}
}
async fn run_error(self) {
match self.run().await {
Ok(()) => tracing::info!("Auth session succeeded"),
Err(e) => tracing::error!(err=?e, "Auth session failed"),
}
}
async fn run(mut self) -> Result<()> {
loop {
tokio::select! {
read_res = self.stream.read_until(b'\n', &mut self.read_buf) => {
// Detect EOF / socket close
let bread = read_res?;
if bread == 0 {
tracing::info!("Reading buffer empty, connection has been closed. Exiting AUTH session.");
return Ok(())
}
// Parse command
let (_, cmd) = client_command(&self.read_buf).map_err(|_| anyhow!("Unable to parse command"))?;
tracing::trace!(cmd=?cmd, "Received command");
// Make some progress in our local state
let login = async |user: String, pass: String| self.login.login(user.as_str(), pass.as_str()).await.is_ok();
self.state.progress(cmd, login).await;
if matches!(self.state, State::Error) {
bail!("Internal state is in error, previous logs explain what went wrong");
}
// Build response
let srv_cmds = self.state.response();
srv_cmds.iter().try_for_each(|r| {
tracing::trace!(cmd=?r, "Sent command");
r.encode(&mut self.write_buf)
})?;
// Send responses if at least one command response has been generated
if !srv_cmds.is_empty() {
self.stream.write_all(&self.write_buf).await?;
self.stream.flush().await?;
}
// Reset buffers
self.read_buf.clear();
self.write_buf.clear();
},
_ = self.stop.changed() => {
tracing::debug!("Server is stopping, quitting this runner");
return Ok(())
}
}
}
}
}

22
aero-sasl/Cargo.toml Normal file
View file

@ -0,0 +1,22 @@
[package]
name = "aero-sasl"
version = "0.3.0"
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
edition = "2021"
license = "EUPL-1.2"
description = "A partial and standalone implementation of the Dovecot SASL Auth Protocol"
[dependencies]
anyhow.workspace = true
base64.workspace = true
futures.workspace = true
nom.workspace = true
rand.workspace = true
tokio.workspace = true
tokio-util.workspace = true
tracing.workspace = true
hex.workspace = true
#log.workspace = true
#serde.workspace = true

243
aero-sasl/src/decode.rs Normal file
View file

@ -0,0 +1,243 @@
use base64::Engine;
use nom::{
branch::alt,
bytes::complete::{tag, tag_no_case, take, take_while, take_while1},
character::complete::{tab, u16, u64},
combinator::{map, opt, recognize, rest, value},
error::{Error, ErrorKind},
multi::{many1, separated_list0},
sequence::{pair, preceded, tuple},
IResult,
};
use super::types::*;
pub fn client_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> {
alt((version_command, cpid_command, auth_command, cont_command))(input)
}
/*
fn server_command(buf: &u8) -> IResult<&u8, ServerCommand> {
unimplemented!();
}
*/
// ---------------------
fn version_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> {
let mut parser = tuple((tag_no_case(b"VERSION"), tab, u64, tab, u64));
let (input, (_, _, major, _, minor)) = parser(input)?;
Ok((input, ClientCommand::Version(Version { major, minor })))
}
pub fn cpid_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> {
preceded(
pair(tag_no_case(b"CPID"), tab),
map(u64, |v| ClientCommand::Cpid(v)),
)(input)
}
fn mechanism<'a>(input: &'a [u8]) -> IResult<&'a [u8], Mechanism> {
alt((
value(Mechanism::Plain, tag_no_case(b"PLAIN")),
value(Mechanism::Login, tag_no_case(b"LOGIN")),
))(input)
}
fn is_not_tab_or_esc_or_lf(c: u8) -> bool {
c != 0x09 && c != 0x01 && c != 0x0a // TAB or 0x01 or LF
}
fn is_esc<'a>(input: &'a [u8]) -> IResult<&'a [u8], &[u8]> {
preceded(tag(&[0x01]), take(1usize))(input)
}
fn parameter<'a>(input: &'a [u8]) -> IResult<&'a [u8], &[u8]> {
recognize(many1(alt((take_while1(is_not_tab_or_esc_or_lf), is_esc))))(input)
}
fn parameter_str(input: &[u8]) -> IResult<&[u8], String> {
let (input, buf) = parameter(input)?;
std::str::from_utf8(buf)
.map(|v| (input, v.to_string()))
.map_err(|_| nom::Err::Failure(Error::new(input, ErrorKind::TakeWhile1)))
}
fn is_param_name_char(c: u8) -> bool {
is_not_tab_or_esc_or_lf(c) && c != 0x3d // =
}
fn parameter_name(input: &[u8]) -> IResult<&[u8], String> {
let (input, buf) = take_while1(is_param_name_char)(input)?;
std::str::from_utf8(buf)
.map(|v| (input, v.to_string()))
.map_err(|_| nom::Err::Failure(Error::new(input, ErrorKind::TakeWhile1)))
}
fn service<'a>(input: &'a [u8]) -> IResult<&'a [u8], String> {
preceded(tag_no_case("service="), parameter_str)(input)
}
fn auth_option<'a>(input: &'a [u8]) -> IResult<&'a [u8], AuthOption> {
use AuthOption::*;
alt((
alt((
value(Debug, tag_no_case(b"debug")),
value(NoPenalty, tag_no_case(b"no-penalty")),
value(ClientId, tag_no_case(b"client_id")),
value(NoLogin, tag_no_case(b"nologin")),
map(preceded(tag_no_case(b"session="), u64), |id| Session(id)),
map(preceded(tag_no_case(b"lip="), parameter_str), |ip| {
LocalIp(ip)
}),
map(preceded(tag_no_case(b"rip="), parameter_str), |ip| {
RemoteIp(ip)
}),
map(preceded(tag_no_case(b"lport="), u16), |port| {
LocalPort(port)
}),
map(preceded(tag_no_case(b"rport="), u16), |port| {
RemotePort(port)
}),
map(preceded(tag_no_case(b"real_rip="), parameter_str), |ip| {
RealRemoteIp(ip)
}),
map(preceded(tag_no_case(b"real_lip="), parameter_str), |ip| {
RealLocalIp(ip)
}),
map(preceded(tag_no_case(b"real_lport="), u16), |port| {
RealLocalPort(port)
}),
map(preceded(tag_no_case(b"real_rport="), u16), |port| {
RealRemotePort(port)
}),
)),
alt((
map(
preceded(tag_no_case(b"local_name="), parameter_str),
|name| LocalName(name),
),
map(
preceded(tag_no_case(b"forward_views="), parameter),
|views| ForwardViews(views.into()),
),
map(preceded(tag_no_case(b"secured="), parameter_str), |info| {
Secured(Some(info))
}),
value(Secured(None), tag_no_case(b"secured")),
value(CertUsername, tag_no_case(b"cert_username")),
map(preceded(tag_no_case(b"transport="), parameter_str), |ts| {
Transport(ts)
}),
map(
preceded(tag_no_case(b"tls_cipher="), parameter_str),
|cipher| TlsCipher(cipher),
),
map(
preceded(tag_no_case(b"tls_cipher_bits="), parameter_str),
|bits| TlsCipherBits(bits),
),
map(preceded(tag_no_case(b"tls_pfs="), parameter_str), |pfs| {
TlsPfs(pfs)
}),
map(
preceded(tag_no_case(b"tls_protocol="), parameter_str),
|proto| TlsProtocol(proto),
),
map(
preceded(tag_no_case(b"valid-client-cert="), parameter_str),
|cert| ValidClientCert(cert),
),
)),
alt((
map(preceded(tag_no_case(b"resp="), base64), |data| Resp(data)),
map(
tuple((parameter_name, tag(b"="), parameter)),
|(n, _, v)| UnknownPair(n, v.into()),
),
map(parameter, |v| UnknownBool(v.into())),
)),
))(input)
}
fn auth_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> {
let mut parser = tuple((
tag_no_case(b"AUTH"),
tab,
u64,
tab,
mechanism,
tab,
service,
map(opt(preceded(tab, separated_list0(tab, auth_option))), |o| {
o.unwrap_or(vec![])
}),
));
let (input, (_, _, id, _, mech, _, service, options)) = parser(input)?;
Ok((
input,
ClientCommand::Auth {
id,
mech,
service,
options,
},
))
}
fn is_base64_core(c: u8) -> bool {
c >= 0x30 && c <= 0x39 // 0-9
|| c >= 0x41 && c <= 0x5a // A-Z
|| c >= 0x61 && c <= 0x7a // a-z
|| c == 0x2b // +
|| c == 0x2f // /
}
fn is_base64_pad(c: u8) -> bool {
c == 0x3d // =
}
fn base64(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
let (input, (b64, _)) = tuple((take_while1(is_base64_core), take_while(is_base64_pad)))(input)?;
let data = base64::engine::general_purpose::STANDARD_NO_PAD
.decode(b64)
.map_err(|_| nom::Err::Failure(Error::new(input, ErrorKind::TakeWhile1)))?;
Ok((input, data))
}
/// @FIXME Dovecot does not say if base64 content must be padded or not
fn cont_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> {
let mut parser = tuple((tag_no_case(b"CONT"), tab, u64, tab, base64));
let (input, (_, _, id, _, data)) = parser(input)?;
Ok((input, ClientCommand::Cont { id, data }))
}
// -----------------------------------------------------------------
//
// SASL DECODING
//
// -----------------------------------------------------------------
fn not_null(c: u8) -> bool {
c != 0x0
}
// impersonated user, login, password
pub fn auth_plain<'a>(input: &'a [u8]) -> IResult<&'a [u8], (&'a [u8], &'a [u8], &'a [u8])> {
map(
tuple((
take_while(not_null),
take(1usize),
take_while(not_null),
take(1usize),
rest,
)),
|(imp, _, user, _, pass)| (imp, user, pass),
)(input)
}

157
aero-sasl/src/encode.rs Normal file
View file

@ -0,0 +1,157 @@
use anyhow::Result;
use base64::Engine;
use tokio_util::bytes::{BufMut, BytesMut};
use super::types::*;
pub trait Encode {
fn encode(&self, out: &mut BytesMut) -> Result<()>;
}
fn tab_enc(out: &mut BytesMut) {
out.put(&[0x09][..])
}
fn lf_enc(out: &mut BytesMut) {
out.put(&[0x0A][..])
}
impl Encode for Mechanism {
fn encode(&self, out: &mut BytesMut) -> Result<()> {
match self {
Self::Plain => out.put(&b"PLAIN"[..]),
Self::Login => out.put(&b"LOGIN"[..]),
}
Ok(())
}
}
impl Encode for MechanismParameters {
fn encode(&self, out: &mut BytesMut) -> Result<()> {
match self {
Self::Anonymous => out.put(&b"anonymous"[..]),
Self::PlainText => out.put(&b"plaintext"[..]),
Self::Dictionary => out.put(&b"dictionary"[..]),
Self::Active => out.put(&b"active"[..]),
Self::ForwardSecrecy => out.put(&b"forward-secrecy"[..]),
Self::MutualAuth => out.put(&b"mutual-auth"[..]),
Self::Private => out.put(&b"private"[..]),
}
Ok(())
}
}
impl Encode for FailCode {
fn encode(&self, out: &mut BytesMut) -> Result<()> {
match self {
Self::TempFail => out.put(&b"temp_fail"[..]),
Self::AuthzFail => out.put(&b"authz_fail"[..]),
Self::UserDisabled => out.put(&b"user_disabled"[..]),
Self::PassExpired => out.put(&b"pass_expired"[..]),
};
Ok(())
}
}
impl Encode for ServerCommand {
fn encode(&self, out: &mut BytesMut) -> Result<()> {
match self {
Self::Version(Version { major, minor }) => {
out.put(&b"VERSION"[..]);
tab_enc(out);
out.put(major.to_string().as_bytes());
tab_enc(out);
out.put(minor.to_string().as_bytes());
lf_enc(out);
}
Self::Spid(pid) => {
out.put(&b"SPID"[..]);
tab_enc(out);
out.put(pid.to_string().as_bytes());
lf_enc(out);
}
Self::Cuid(pid) => {
out.put(&b"CUID"[..]);
tab_enc(out);
out.put(pid.to_string().as_bytes());
lf_enc(out);
}
Self::Cookie(cval) => {
out.put(&b"COOKIE"[..]);
tab_enc(out);
out.put(hex::encode(cval).as_bytes());
lf_enc(out);
}
Self::Mech { kind, parameters } => {
out.put(&b"MECH"[..]);
tab_enc(out);
kind.encode(out)?;
for p in parameters.iter() {
tab_enc(out);
p.encode(out)?;
}
lf_enc(out);
}
Self::Done => {
out.put(&b"DONE"[..]);
lf_enc(out);
}
Self::Cont { id, data } => {
out.put(&b"CONT"[..]);
tab_enc(out);
out.put(id.to_string().as_bytes());
tab_enc(out);
if let Some(rdata) = data {
let b64 = base64::engine::general_purpose::STANDARD.encode(rdata);
out.put(b64.as_bytes());
}
lf_enc(out);
}
Self::Ok {
id,
user_id,
extra_parameters,
} => {
out.put(&b"OK"[..]);
tab_enc(out);
out.put(id.to_string().as_bytes());
if let Some(user) = user_id {
tab_enc(out);
out.put(&b"user="[..]);
out.put(user.as_bytes());
}
for p in extra_parameters.iter() {
tab_enc(out);
out.put(&p[..]);
}
lf_enc(out);
}
Self::Fail {
id,
user_id,
code,
extra_parameters,
} => {
out.put(&b"FAIL"[..]);
tab_enc(out);
out.put(id.to_string().as_bytes());
if let Some(user) = user_id {
tab_enc(out);
out.put(&b"user="[..]);
out.put(user.as_bytes());
}
if let Some(code_val) = code {
tab_enc(out);
out.put(&b"code="[..]);
code_val.encode(out)?;
}
for p in extra_parameters.iter() {
tab_enc(out);
out.put(&p[..]);
}
lf_enc(out);
}
}
Ok(())
}
}

201
aero-sasl/src/flow.rs Normal file
View file

@ -0,0 +1,201 @@
use futures::Future;
use rand::prelude::*;
use super::decode::auth_plain;
use super::types::*;
#[derive(Debug)]
pub enum AuthRes {
Success(String),
Failed(Option<String>, Option<FailCode>),
}
#[derive(Debug)]
pub enum State {
Error,
Init,
HandshakePart(Version),
HandshakeDone,
AuthPlainProgress { id: u64 },
AuthDone { id: u64, res: AuthRes },
}
const SERVER_MAJOR: u64 = 1;
const SERVER_MINOR: u64 = 2;
const EMPTY_AUTHZ: &[u8] = &[];
impl State {
pub fn new() -> Self {
Self::Init
}
async fn try_auth_plain<X, F>(&self, data: &[u8], login: X) -> AuthRes
where
X: FnOnce(String, String) -> F,
F: Future<Output = bool>,
{
// Check that we can extract user's login+pass
let (ubin, pbin) = match auth_plain(&data) {
Ok(([], (authz, user, pass))) if authz == user || authz == EMPTY_AUTHZ => (user, pass),
Ok(_) => {
tracing::error!("Impersonating user is not supported");
return AuthRes::Failed(None, None);
}
Err(e) => {
tracing::error!(err=?e, "Could not parse the SASL PLAIN data chunk");
return AuthRes::Failed(None, None);
}
};
// Try to convert it to UTF-8
let (user, password) = match (std::str::from_utf8(ubin), std::str::from_utf8(pbin)) {
(Ok(u), Ok(p)) => (u, p),
_ => {
tracing::error!("Username or password contain invalid UTF-8 characters");
return AuthRes::Failed(None, None);
}
};
// Try to connect user
match login(user.to_string(), password.to_string()).await {
true => AuthRes::Success(user.to_string()),
false => {
tracing::warn!("login failed");
AuthRes::Failed(Some(user.to_string()), None)
}
}
}
pub async fn progress<F, X>(&mut self, cmd: ClientCommand, login: X)
where
X: FnOnce(String, String) -> F,
F: Future<Output = bool>,
{
let new_state = 'state: {
match (std::mem::replace(self, State::Error), cmd) {
(Self::Init, ClientCommand::Version(v)) => Self::HandshakePart(v),
(Self::HandshakePart(version), ClientCommand::Cpid(_cpid)) => {
if version.major != SERVER_MAJOR {
tracing::error!(
client_major = version.major,
server_major = SERVER_MAJOR,
"Unsupported client major version"
);
break 'state Self::Error;
}
Self::HandshakeDone
}
(
Self::HandshakeDone { .. },
ClientCommand::Auth {
id, mech, options, ..
},
)
| (
Self::AuthDone { .. },
ClientCommand::Auth {
id, mech, options, ..
},
) => {
if mech != Mechanism::Plain {
tracing::error!(mechanism=?mech, "Unsupported Authentication Mechanism");
break 'state Self::AuthDone {
id,
res: AuthRes::Failed(None, None),
};
}
match options.last() {
Some(AuthOption::Resp(data)) => Self::AuthDone {
id,
res: self.try_auth_plain(&data, login).await,
},
_ => Self::AuthPlainProgress { id },
}
}
(Self::AuthPlainProgress { id }, ClientCommand::Cont { id: cid, data }) => {
// Check that ID matches
if cid != id {
tracing::error!(
auth_id = id,
cont_id = cid,
"CONT id does not match AUTH id"
);
break 'state Self::AuthDone {
id,
res: AuthRes::Failed(None, None),
};
}
Self::AuthDone {
id,
res: self.try_auth_plain(&data, login).await,
}
}
_ => {
tracing::error!("This command is not valid in this context");
Self::Error
}
}
};
tracing::debug!(state=?new_state, "Made progress");
*self = new_state;
}
pub fn response(&self) -> Vec<ServerCommand> {
let mut srv_cmd: Vec<ServerCommand> = Vec::new();
match self {
Self::HandshakeDone { .. } => {
srv_cmd.push(ServerCommand::Version(Version {
major: SERVER_MAJOR,
minor: SERVER_MINOR,
}));
srv_cmd.push(ServerCommand::Mech {
kind: Mechanism::Plain,
parameters: vec![MechanismParameters::PlainText],
});
srv_cmd.push(ServerCommand::Spid(15u64));
srv_cmd.push(ServerCommand::Cuid(19350u64));
let mut cookie = [0u8; 16];
thread_rng().fill(&mut cookie);
srv_cmd.push(ServerCommand::Cookie(cookie));
srv_cmd.push(ServerCommand::Done);
}
Self::AuthPlainProgress { id } => {
srv_cmd.push(ServerCommand::Cont {
id: *id,
data: None,
});
}
Self::AuthDone {
id,
res: AuthRes::Success(user),
} => {
srv_cmd.push(ServerCommand::Ok {
id: *id,
user_id: Some(user.to_string()),
extra_parameters: vec![],
});
}
Self::AuthDone {
id,
res: AuthRes::Failed(maybe_user, maybe_failcode),
} => {
srv_cmd.push(ServerCommand::Fail {
id: *id,
user_id: maybe_user.clone(),
code: maybe_failcode.clone(),
extra_parameters: vec![],
});
}
_ => (),
};
srv_cmd
}
}

43
aero-sasl/src/lib.rs Normal file
View file

@ -0,0 +1,43 @@
pub mod decode;
pub mod encode;
pub mod flow;
/// Seek compatibility with the Dovecot Authentication Protocol
///
/// ## Trace
///
/// ```text
/// S: VERSION 1 2
/// S: MECH PLAIN plaintext
/// S: MECH LOGIN plaintext
/// S: SPID 15
/// S: CUID 17654
/// S: COOKIE f56692bee41f471ed01bd83520025305
/// S: DONE
/// C: VERSION 1 2
/// C: CPID 1
///
/// C: AUTH 2 PLAIN service=smtp
/// S: CONT 2
/// C: CONT 2 base64stringFollowingRFC4616==
/// S: OK 2 user=alice@example.tld
///
/// C: AUTH 42 LOGIN service=smtp
/// S: CONT 42 VXNlcm5hbWU6
/// C: CONT 42 b64User
/// S: CONT 42 UGFzc3dvcmQ6
/// C: CONT 42 b64Pass
/// S: FAIL 42 user=alice
/// ```
///
/// ## RFC References
///
/// PLAIN SASL - https://datatracker.ietf.org/doc/html/rfc4616
///
///
/// ## Dovecot References
///
/// https://doc.dovecot.org/developer_manual/design/auth_protocol/
/// https://doc.dovecot.org/configuration_manual/authentication/authentication_mechanisms/#authentication-authentication-mechanisms
/// https://doc.dovecot.org/configuration_manual/howto/simple_virtual_install/#simple-virtual-install-smtp-auth
/// https://doc.dovecot.org/configuration_manual/howto/postfix_and_dovecot_sasl/#howto-postfix-and-dovecot-sasl
pub mod types;

161
aero-sasl/src/types.rs Normal file
View file

@ -0,0 +1,161 @@
#[derive(Debug, Clone, PartialEq)]
pub enum Mechanism {
Plain,
Login,
}
#[derive(Clone, Debug)]
pub enum AuthOption {
/// Unique session ID. Mainly used for logging.
Session(u64),
/// Local IP connected to by the client. In standard string format, e.g. 127.0.0.1 or ::1.
LocalIp(String),
/// Remote client IP
RemoteIp(String),
/// Local port connected to by the client.
LocalPort(u16),
/// Remote client port
RemotePort(u16),
/// When Dovecot proxy is used, the real_rip/real_port are the proxys IP/port and real_lip/real_lport are the backends IP/port where the proxy was connected to.
RealRemoteIp(String),
RealLocalIp(String),
RealLocalPort(u16),
RealRemotePort(u16),
/// TLS SNI name
LocalName(String),
/// Enable debugging for this lookup.
Debug,
/// List of fields that will become available via %{forward_*} variables. The list is double-tab-escaped, like: tab_escaped[tab_escaped(key=value)[<TAB>...]
/// Note: we do not unescape the tabulation, and thus we don't parse the data
ForwardViews(Vec<u8>),
/// Remote user has secured transport to auth client (e.g. localhost, SSL, TLS).
Secured(Option<String>),
/// The value can be “insecure”, “trusted” or “TLS”.
Transport(String),
/// TLS cipher being used.
TlsCipher(String),
/// The number of bits in the TLS cipher.
/// @FIXME: I don't know how if it's a string or an integer
TlsCipherBits(String),
/// TLS perfect forward secrecy algorithm (e.g. DH, ECDH)
TlsPfs(String),
/// TLS protocol name (e.g. SSLv3, TLSv1.2)
TlsProtocol(String),
/// Remote user has presented a valid SSL certificate.
ValidClientCert(String),
/// Ignore auth penalty tracking for this request
NoPenalty,
/// Unknown option sent by Postfix
NoLogin,
/// Username taken from clients SSL certificate.
CertUsername,
/// IMAP ID string
ClientId,
/// An unknown key
UnknownPair(String, Vec<u8>),
UnknownBool(Vec<u8>),
/// Initial response for authentication mechanism.
/// NOTE: This must be the last parameter. Everything after it is ignored.
/// This is to avoid accidental security holes if user-given data is directly put to base64 string without filtering out tabs.
/// **This field is used when the data to pass is small, it's a way to "inline a continuation".
Resp(Vec<u8>),
}
#[derive(Debug, Clone)]
pub struct Version {
pub major: u64,
pub minor: u64,
}
#[derive(Debug)]
pub enum ClientCommand {
/// Both client and server should check that they support the same major version number. If they dont, the other side isnt expected to be talking the same protocol and should be disconnected. Minor version can be ignored. This document specifies the version number 1.2.
Version(Version),
/// CPID finishes the handshake from client.
Cpid(u64),
Auth {
/// ID is a connection-specific unique request identifier. It must be a 32bit number, so typically youd just increment it by one.
id: u64,
/// A SASL mechanism (eg. LOGIN, PLAIN, etc.)
/// See: https://doc.dovecot.org/configuration_manual/authentication/authentication_mechanisms/#authentication-authentication-mechanisms
mech: Mechanism,
/// Service is the service requesting authentication, eg. pop3, imap, smtp.
service: String,
/// All the optional parameters
options: Vec<AuthOption>,
},
Cont {
/// The <id> must match the <id> of the AUTH command.
id: u64,
/// Data that will be serialized to / deserialized from base64
data: Vec<u8>,
},
}
#[derive(Debug)]
pub enum MechanismParameters {
/// Anonymous authentication
Anonymous,
/// Transfers plaintext passwords
PlainText,
/// Subject to passive (dictionary) attack
Dictionary,
/// Subject to active (non-dictionary) attack
Active,
/// Provides forward secrecy between sessions
ForwardSecrecy,
/// Provides mutual authentication
MutualAuth,
/// Dont advertise this as available SASL mechanism (eg. APOP)
Private,
}
#[derive(Debug, Clone)]
pub enum FailCode {
/// This is a temporary internal failure, e.g. connection was lost to SQL database.
TempFail,
/// Authentication succeeded, but authorization failed (master users password was ok, but destination user was not ok).
AuthzFail,
/// User is disabled (password may or may not have been correct)
UserDisabled,
/// Users password has expired.
PassExpired,
}
#[derive(Debug)]
pub enum ServerCommand {
/// Both client and server should check that they support the same major version number. If they dont, the other side isnt expected to be talking the same protocol and should be disconnected. Minor version can be ignored. This document specifies the version number 1.2.
Version(Version),
/// CPID and SPID specify client and server Process Identifiers (PIDs). They should be unique identifiers for the specific process. UNIX process IDs are good choices.
/// SPID can be used by authentication client to tell master which server process handled the authentication.
Spid(u64),
/// CUID is a server process-specific unique connection identifier. Its different each time a connection is established for the server.
/// CUID is currently useful only for APOP authentication.
Cuid(u64),
Mech {
kind: Mechanism,
parameters: Vec<MechanismParameters>,
},
/// COOKIE returns connection-specific 128 bit cookie in hex. It must be given to REQUEST command. (Protocol v1.1+ / Dovecot v2.0+)
Cookie([u8; 16]),
/// DONE finishes the handshake from server.
Done,
Fail {
id: u64,
user_id: Option<String>,
code: Option<FailCode>,
extra_parameters: Vec<Vec<u8>>,
},
Cont {
id: u64,
data: Option<Vec<u8>>,
},
/// FAIL and OK may contain multiple unspecified parameters which authentication client may handle specially.
/// The only one specified here is user=<userid> parameter, which should always be sent if the userid is known.
Ok {
id: u64,
user_id: Option<String>,
extra_parameters: Vec<Vec<u8>>,
},
}

30
aero-user/Cargo.toml Normal file
View file

@ -0,0 +1,30 @@
[package]
name = "aero-user"
version = "0.3.0"
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
edition = "2021"
license = "EUPL-1.2"
description = "Represent an encrypted user profile"
[dependencies]
anyhow.workspace = true
serde.workspace = true
zstd.workspace = true
sodiumoxide.workspace = true
log.workspace = true
async-trait.workspace = true
ldap3.workspace = true
base64.workspace = true
rand.workspace = true
tokio.workspace = true
aws-config.workspace = true
aws-sdk-s3.workspace = true
aws-smithy-runtime.workspace = true
aws-smithy-runtime-api.workspace = true
hyper-rustls.workspace = true
hyper-util.workspace = true
k2v-client.workspace = true
rmp-serde.workspace = true
toml.workspace = true
tracing.workspace = true
argon2.workspace = true

View file

@ -9,8 +9,8 @@ use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CompanionConfig { pub struct CompanionConfig {
pub pid: Option<PathBuf>, pub pid: Option<PathBuf>,
pub imap: ImapConfig, pub imap: ImapUnsecureConfig,
// @FIXME Add DAV
#[serde(flatten)] #[serde(flatten)]
pub users: LoginStaticConfig, pub users: LoginStaticConfig,
} }
@ -18,8 +18,12 @@ pub struct CompanionConfig {
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ProviderConfig { pub struct ProviderConfig {
pub pid: Option<PathBuf>, pub pid: Option<PathBuf>,
pub imap: ImapConfig, pub imap: Option<ImapConfig>,
pub lmtp: LmtpConfig, pub imap_unsecure: Option<ImapUnsecureConfig>,
pub lmtp: Option<LmtpConfig>,
pub auth: Option<AuthConfig>,
pub dav: Option<DavConfig>,
pub dav_unsecure: Option<DavUnsecureConfig>,
pub users: UserManagement, pub users: UserManagement,
} }
@ -31,6 +35,11 @@ pub enum UserManagement {
Ldap(LoginLdapConfig), Ldap(LoginLdapConfig),
} }
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AuthConfig {
pub bind_addr: SocketAddr,
}
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct LmtpConfig { pub struct LmtpConfig {
pub bind_addr: SocketAddr, pub bind_addr: SocketAddr,
@ -40,6 +49,25 @@ pub struct LmtpConfig {
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ImapConfig { pub struct ImapConfig {
pub bind_addr: SocketAddr, pub bind_addr: SocketAddr,
pub certs: PathBuf,
pub key: PathBuf,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct DavUnsecureConfig {
pub bind_addr: SocketAddr,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct DavConfig {
pub bind_addr: SocketAddr,
pub certs: PathBuf,
pub key: PathBuf,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ImapUnsecureConfig {
pub bind_addr: SocketAddr,
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]

9
aero-user/src/lib.rs Normal file
View file

@ -0,0 +1,9 @@
pub mod config;
pub mod cryptoblob;
pub mod login;
pub mod storage;
// A user is composed of 3 things:
// - An identity (login)
// - A storage profile (storage)
// - Some cryptography data (cryptoblob)

View file

@ -1,10 +1,9 @@
use anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;
use ldap3::{LdapConnAsync, Scope, SearchEntry}; use ldap3::{LdapConnAsync, Scope, SearchEntry};
use log::debug; use log::debug;
use super::*;
use crate::config::*; use crate::config::*;
use crate::login::*;
use crate::storage; use crate::storage;
pub struct LdapLoginProvider { pub struct LdapLoginProvider {
@ -21,6 +20,7 @@ pub struct LdapLoginProvider {
storage_specific: StorageSpecific, storage_specific: StorageSpecific,
in_memory_store: storage::in_memory::MemDb, in_memory_store: storage::in_memory::MemDb,
garage_store: storage::garage::GarageRoot,
} }
enum BucketSource { enum BucketSource {
@ -60,6 +60,9 @@ impl LdapLoginProvider {
let specific = match config.storage { let specific = match config.storage {
LdapStorage::InMemory => StorageSpecific::InMemory, LdapStorage::InMemory => StorageSpecific::InMemory,
LdapStorage::Garage(grgconf) => { LdapStorage::Garage(grgconf) => {
attrs_to_retrieve.push(grgconf.aws_access_key_id_attr.clone());
attrs_to_retrieve.push(grgconf.aws_secret_access_key_attr.clone());
let bucket_source = let bucket_source =
match (grgconf.default_bucket.clone(), grgconf.bucket_attr.clone()) { match (grgconf.default_bucket.clone(), grgconf.bucket_attr.clone()) {
(Some(b), None) => BucketSource::Constant(b), (Some(b), None) => BucketSource::Constant(b),
@ -88,7 +91,11 @@ impl LdapLoginProvider {
mail_attr: config.mail_attr, mail_attr: config.mail_attr,
crypto_root_attr: config.crypto_root_attr, crypto_root_attr: config.crypto_root_attr,
storage_specific: specific, storage_specific: specific,
//@FIXME should be created outside of the login provider
//Login provider should return only a cryptoroot + a storage URI
//storage URI that should be resolved outside...
in_memory_store: storage::in_memory::MemDb::new(), in_memory_store: storage::in_memory::MemDb::new(),
garage_store: storage::garage::GarageRoot::new()?,
}) })
} }
@ -111,7 +118,7 @@ impl LdapLoginProvider {
BucketSource::Attr(a) => get_attr(user, &a)?, BucketSource::Attr(a) => get_attr(user, &a)?,
}; };
storage::garage::GarageBuilder::new(storage::garage::GarageConf { self.garage_store.user(storage::garage::GarageConf {
region: from_config.aws_region.clone(), region: from_config.aws_region.clone(),
s3_endpoint: from_config.s3_endpoint.clone(), s3_endpoint: from_config.s3_endpoint.clone(),
k2v_endpoint: from_config.k2v_endpoint.clone(), k2v_endpoint: from_config.k2v_endpoint.clone(),

View file

@ -2,11 +2,11 @@ pub mod demo_provider;
pub mod ldap_provider; pub mod ldap_provider;
pub mod static_provider; pub mod static_provider;
use base64::Engine;
use std::sync::Arc; use std::sync::Arc;
use anyhow::{anyhow, bail, Context, Result}; use anyhow::{anyhow, bail, Context, Result};
use async_trait::async_trait; use async_trait::async_trait;
use base64::Engine;
use rand::prelude::*; use rand::prelude::*;
use crate::cryptoblob::*; use crate::cryptoblob::*;

View file

@ -1,12 +1,11 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{anyhow, bail};
use async_trait::async_trait;
use tokio::signal::unix::{signal, SignalKind}; use tokio::signal::unix::{signal, SignalKind};
use tokio::sync::watch; use tokio::sync::watch;
use anyhow::{anyhow, bail, Result};
use async_trait::async_trait;
use crate::config::*; use crate::config::*;
use crate::login::*; use crate::login::*;
use crate::storage; use crate::storage;
@ -25,6 +24,7 @@ pub struct UserDatabase {
pub struct StaticLoginProvider { pub struct StaticLoginProvider {
user_db: watch::Receiver<UserDatabase>, user_db: watch::Receiver<UserDatabase>,
in_memory_store: storage::in_memory::MemDb, in_memory_store: storage::in_memory::MemDb,
garage_store: storage::garage::GarageRoot,
} }
pub async fn update_user_list(config: PathBuf, up: watch::Sender<UserDatabase>) -> Result<()> { pub async fn update_user_list(config: PathBuf, up: watch::Sender<UserDatabase>) -> Result<()> {
@ -84,6 +84,7 @@ impl StaticLoginProvider {
Ok(Self { Ok(Self {
user_db: rx, user_db: rx,
in_memory_store: storage::in_memory::MemDb::new(), in_memory_store: storage::in_memory::MemDb::new(),
garage_store: storage::garage::GarageRoot::new()?,
}) })
} }
} }
@ -109,7 +110,7 @@ impl LoginProvider for StaticLoginProvider {
let storage: storage::Builder = match &user.config.storage { let storage: storage::Builder = match &user.config.storage {
StaticStorage::InMemory => self.in_memory_store.builder(username).await, StaticStorage::InMemory => self.in_memory_store.builder(username).await,
StaticStorage::Garage(grgconf) => { StaticStorage::Garage(grgconf) => {
storage::garage::GarageBuilder::new(storage::garage::GarageConf { self.garage_store.user(storage::garage::GarageConf {
region: grgconf.aws_region.clone(), region: grgconf.aws_region.clone(),
k2v_endpoint: grgconf.k2v_endpoint.clone(), k2v_endpoint: grgconf.k2v_endpoint.clone(),
s3_endpoint: grgconf.s3_endpoint.clone(), s3_endpoint: grgconf.s3_endpoint.clone(),
@ -140,7 +141,7 @@ impl LoginProvider for StaticLoginProvider {
let storage: storage::Builder = match &user.config.storage { let storage: storage::Builder = match &user.config.storage {
StaticStorage::InMemory => self.in_memory_store.builder(&user.username).await, StaticStorage::InMemory => self.in_memory_store.builder(&user.username).await,
StaticStorage::Garage(grgconf) => { StaticStorage::Garage(grgconf) => {
storage::garage::GarageBuilder::new(storage::garage::GarageConf { self.garage_store.user(storage::garage::GarageConf {
region: grgconf.aws_region.clone(), region: grgconf.aws_region.clone(),
k2v_endpoint: grgconf.k2v_endpoint.clone(), k2v_endpoint: grgconf.k2v_endpoint.clone(),
s3_endpoint: grgconf.s3_endpoint.clone(), s3_endpoint: grgconf.s3_endpoint.clone(),

View file

@ -1,7 +1,45 @@
use crate::storage::*;
use aws_sdk_s3::{self as s3, error::SdkError, operation::get_object::GetObjectError}; use aws_sdk_s3::{self as s3, error::SdkError, operation::get_object::GetObjectError};
use aws_smithy_runtime::client::http::hyper_014::HyperClientBuilder;
use aws_smithy_runtime_api::client::http::SharedHttpClient;
use hyper_rustls::HttpsConnector;
use hyper_util::client::legacy::{connect::HttpConnector, Client as HttpClient};
use hyper_util::rt::TokioExecutor;
use serde::Serialize; use serde::Serialize;
use super::*;
pub struct GarageRoot {
k2v_http: HttpClient<HttpsConnector<HttpConnector>, k2v_client::Body>,
aws_http: SharedHttpClient,
}
impl GarageRoot {
pub fn new() -> anyhow::Result<Self> {
let connector = hyper_rustls::HttpsConnectorBuilder::new()
.with_native_roots()?
.https_or_http()
.enable_http1()
.enable_http2()
.build();
let k2v_http = HttpClient::builder(TokioExecutor::new()).build(connector);
let aws_http = HyperClientBuilder::new().build_https();
Ok(Self { k2v_http, aws_http })
}
pub fn user(&self, conf: GarageConf) -> anyhow::Result<Arc<GarageUser>> {
let mut unicity: Vec<u8> = vec![];
unicity.extend_from_slice(file!().as_bytes());
unicity.append(&mut rmp_serde::to_vec(&conf)?);
Ok(Arc::new(GarageUser {
conf,
aws_http: self.aws_http.clone(),
k2v_http: self.k2v_http.clone(),
unicity,
}))
}
}
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
pub struct GarageConf { pub struct GarageConf {
pub region: String, pub region: String,
@ -12,23 +50,19 @@ pub struct GarageConf {
pub bucket: String, pub bucket: String,
} }
//@FIXME we should get rid of this builder
//and allocate a S3 + K2V client only once per user
//(and using a shared HTTP client)
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct GarageBuilder { pub struct GarageUser {
conf: GarageConf, conf: GarageConf,
aws_http: SharedHttpClient,
k2v_http: HttpClient<HttpsConnector<HttpConnector>, k2v_client::Body>,
unicity: Vec<u8>, unicity: Vec<u8>,
} }
impl GarageBuilder {
pub fn new(conf: GarageConf) -> anyhow::Result<Arc<Self>> {
let mut unicity: Vec<u8> = vec![];
unicity.extend_from_slice(file!().as_bytes());
unicity.append(&mut rmp_serde::to_vec(&conf)?);
Ok(Arc::new(Self { conf, unicity }))
}
}
#[async_trait] #[async_trait]
impl IBuilder for GarageBuilder { impl IBuilder for GarageUser {
async fn build(&self) -> Result<Store, StorageError> { async fn build(&self) -> Result<Store, StorageError> {
let s3_creds = s3::config::Credentials::new( let s3_creds = s3::config::Credentials::new(
self.conf.aws_access_key_id.clone(), self.conf.aws_access_key_id.clone(),
@ -41,6 +75,7 @@ impl IBuilder for GarageBuilder {
let sdk_config = aws_config::from_env() let sdk_config = aws_config::from_env()
.region(aws_config::Region::new(self.conf.region.clone())) .region(aws_config::Region::new(self.conf.region.clone()))
.credentials_provider(s3_creds) .credentials_provider(s3_creds)
.http_client(self.aws_http.clone())
.endpoint_url(self.conf.s3_endpoint.clone()) .endpoint_url(self.conf.s3_endpoint.clone())
.load() .load()
.await; .await;
@ -60,13 +95,14 @@ impl IBuilder for GarageBuilder {
user_agent: None, user_agent: None,
}; };
let k2v_client = match k2v_client::K2vClient::new(k2v_config) { let k2v_client =
Err(e) => { match k2v_client::K2vClient::new_with_client(k2v_config, self.k2v_http.clone()) {
tracing::error!("unable to build k2v client: {}", e); Err(e) => {
return Err(StorageError::Internal); tracing::error!("unable to build k2v client: {}", e);
} return Err(StorageError::Internal);
Ok(v) => v, }
}; Ok(v) => v,
};
Ok(Box::new(GarageStore { Ok(Box::new(GarageStore {
bucket: self.conf.bucket.clone(), bucket: self.conf.bucket.clone(),
@ -105,6 +141,7 @@ fn causal_to_row_val(row_ref: RowRef, causal_value: k2v_client::CausalValue) ->
#[async_trait] #[async_trait]
impl IStore for GarageStore { impl IStore for GarageStore {
async fn row_fetch<'a>(&self, select: &Selector<'a>) -> Result<Vec<RowVal>, StorageError> { async fn row_fetch<'a>(&self, select: &Selector<'a>) -> Result<Vec<RowVal>, StorageError> {
tracing::trace!(select=%select, command="row_fetch");
let (pk_list, batch_op) = match select { let (pk_list, batch_op) = match select {
Selector::Range { Selector::Range {
shard, shard,
@ -196,21 +233,26 @@ impl IStore for GarageStore {
} }
Ok(v) => v, Ok(v) => v,
}; };
//println!("fetch res -> {:?}", all_raw_res);
let row_vals = all_raw_res let row_vals =
.into_iter() all_raw_res
.fold(vec![], |mut acc, v| { .into_iter()
acc.extend(v.items); .zip(pk_list.into_iter())
acc .fold(vec![], |mut acc, (page, pk)| {
}) page.items
.into_iter() .into_iter()
.zip(pk_list.into_iter()) .map(|(sk, cv)| causal_to_row_val(RowRef::new(&pk, &sk), cv))
.map(|((sk, cv), pk)| causal_to_row_val(RowRef::new(&pk, &sk), cv)) .for_each(|rr| acc.push(rr));
.collect::<Vec<_>>();
acc
});
tracing::debug!(fetch_count = row_vals.len(), command = "row_fetch");
Ok(row_vals) Ok(row_vals)
} }
async fn row_rm<'a>(&self, select: &Selector<'a>) -> Result<(), StorageError> { async fn row_rm<'a>(&self, select: &Selector<'a>) -> Result<(), StorageError> {
tracing::trace!(select=%select, command="row_rm");
let del_op = match select { let del_op = match select {
Selector::Range { Selector::Range {
shard, shard,
@ -280,6 +322,7 @@ impl IStore for GarageStore {
} }
async fn row_insert(&self, values: Vec<RowVal>) -> Result<(), StorageError> { async fn row_insert(&self, values: Vec<RowVal>) -> Result<(), StorageError> {
tracing::trace!(entries=%values.iter().map(|v| v.row_ref.to_string()).collect::<Vec<_>>().join(","), command="row_insert");
let batch_ops = values let batch_ops = values
.iter() .iter()
.map(|v| k2v_client::BatchInsertOp { .map(|v| k2v_client::BatchInsertOp {
@ -307,6 +350,7 @@ impl IStore for GarageStore {
} }
} }
async fn row_poll(&self, value: &RowRef) -> Result<RowVal, StorageError> { async fn row_poll(&self, value: &RowRef) -> Result<RowVal, StorageError> {
tracing::trace!(entry=%value, command="row_poll");
loop { loop {
if let Some(ct) = &value.causality { if let Some(ct) = &value.causality {
match self match self
@ -343,6 +387,7 @@ impl IStore for GarageStore {
} }
async fn blob_fetch(&self, blob_ref: &BlobRef) -> Result<BlobVal, StorageError> { async fn blob_fetch(&self, blob_ref: &BlobRef) -> Result<BlobVal, StorageError> {
tracing::trace!(entry=%blob_ref, command="blob_fetch");
let maybe_out = self let maybe_out = self
.s3 .s3
.get_object() .get_object()
@ -381,14 +426,16 @@ impl IStore for GarageStore {
tracing::debug!("Fetched {}/{}", self.bucket, blob_ref.0); tracing::debug!("Fetched {}/{}", self.bucket, blob_ref.0);
Ok(bv) Ok(bv)
} }
async fn blob_insert(&self, blob_val: BlobVal) -> Result<(), StorageError> { async fn blob_insert(&self, blob_val: BlobVal) -> Result<String, StorageError> {
tracing::trace!(entry=%blob_val.blob_ref, command="blob_insert");
let streamable_value = s3::primitives::ByteStream::from(blob_val.value); let streamable_value = s3::primitives::ByteStream::from(blob_val.value);
let obj_key = blob_val.blob_ref.0;
let maybe_send = self let maybe_send = self
.s3 .s3
.put_object() .put_object()
.bucket(self.bucket.to_string()) .bucket(self.bucket.to_string())
.key(blob_val.blob_ref.0.to_string()) .key(obj_key.to_string())
.set_metadata(Some(blob_val.meta)) .set_metadata(Some(blob_val.meta))
.body(streamable_value) .body(streamable_value)
.send() .send()
@ -399,13 +446,17 @@ impl IStore for GarageStore {
tracing::error!("unable to send object: {}", e); tracing::error!("unable to send object: {}", e);
Err(StorageError::Internal) Err(StorageError::Internal)
} }
Ok(_) => { Ok(put_output) => {
tracing::debug!("Inserted {}/{}", self.bucket, blob_val.blob_ref.0); tracing::debug!("Inserted {}/{}", self.bucket, obj_key);
Ok(()) Ok(put_output
.e_tag()
.map(|v| format!("\"{}\"", v))
.unwrap_or(format!("W/\"{}\"", obj_key)))
} }
} }
} }
async fn blob_copy(&self, src: &BlobRef, dst: &BlobRef) -> Result<(), StorageError> { async fn blob_copy(&self, src: &BlobRef, dst: &BlobRef) -> Result<(), StorageError> {
tracing::trace!(src=%src, dst=%dst, command="blob_copy");
let maybe_copy = self let maybe_copy = self
.s3 .s3
.copy_object() .copy_object()
@ -433,6 +484,7 @@ impl IStore for GarageStore {
} }
} }
async fn blob_list(&self, prefix: &str) -> Result<Vec<BlobRef>, StorageError> { async fn blob_list(&self, prefix: &str) -> Result<Vec<BlobRef>, StorageError> {
tracing::trace!(prefix = prefix, command = "blob_list");
let maybe_list = self let maybe_list = self
.s3 .s3
.list_objects_v2() .list_objects_v2()
@ -462,6 +514,7 @@ impl IStore for GarageStore {
} }
} }
async fn blob_rm(&self, blob_ref: &BlobRef) -> Result<(), StorageError> { async fn blob_rm(&self, blob_ref: &BlobRef) -> Result<(), StorageError> {
tracing::trace!(entry=%blob_ref, command="blob_rm");
let maybe_delete = self let maybe_delete = self
.s3 .s3
.delete_object() .delete_object()

View file

@ -1,9 +1,12 @@
use crate::storage::*; use std::collections::BTreeMap;
use std::collections::{BTreeMap, HashMap};
use std::ops::Bound::{self, Excluded, Included, Unbounded}; use std::ops::Bound::{self, Excluded, Included, Unbounded};
use std::sync::{Arc, RwLock}; use std::sync::RwLock;
use sodiumoxide::{crypto::hash, hex};
use tokio::sync::Notify; use tokio::sync::Notify;
use crate::storage::*;
/// This implementation is very inneficient, and not completely correct /// This implementation is very inneficient, and not completely correct
/// Indeed, when the connector is dropped, the memory is freed. /// Indeed, when the connector is dropped, the memory is freed.
/// It means that when a user disconnects, its data are lost. /// It means that when a user disconnects, its data are lost.
@ -80,6 +83,12 @@ impl InternalBlobVal {
value: self.data.clone(), value: self.data.clone(),
} }
} }
fn etag(&self) -> String {
let digest = hash::hash(self.data.as_ref());
let buff = digest.as_ref();
let hexstr = hex::encode(buff);
format!("\"{}\"", hexstr)
}
} }
type ArcRow = Arc<RwLock<HashMap<String, BTreeMap<String, InternalRowVal>>>>; type ArcRow = Arc<RwLock<HashMap<String, BTreeMap<String, InternalRowVal>>>>;
@ -300,13 +309,14 @@ impl IStore for MemStore {
.ok_or(StorageError::NotFound) .ok_or(StorageError::NotFound)
.map(|v| v.to_blob_val(blob_ref)) .map(|v| v.to_blob_val(blob_ref))
} }
async fn blob_insert(&self, blob_val: BlobVal) -> Result<(), StorageError> { async fn blob_insert(&self, blob_val: BlobVal) -> Result<String, StorageError> {
tracing::trace!(entry=%blob_val.blob_ref, command="blob_insert"); tracing::trace!(entry=%blob_val.blob_ref, command="blob_insert");
let mut store = self.blob.write().or(Err(StorageError::Internal))?; let mut store = self.blob.write().or(Err(StorageError::Internal))?;
let entry = store.entry(blob_val.blob_ref.0.clone()).or_default(); let entry = store.entry(blob_val.blob_ref.0.clone()).or_default();
entry.data = blob_val.value.clone(); entry.data = blob_val.value.clone();
entry.metadata = blob_val.meta.clone(); entry.metadata = blob_val.meta.clone();
Ok(())
Ok(entry.etag())
} }
async fn blob_copy(&self, src: &BlobRef, dst: &BlobRef) -> Result<(), StorageError> { async fn blob_copy(&self, src: &BlobRef, dst: &BlobRef) -> Result<(), StorageError> {
tracing::trace!(src=%src, dst=%dst, command="blob_copy"); tracing::trace!(src=%src, dst=%dst, command="blob_copy");

View file

@ -11,11 +11,12 @@
pub mod garage; pub mod garage;
pub mod in_memory; pub mod in_memory;
use async_trait::async_trait;
use std::collections::HashMap; use std::collections::HashMap;
use std::hash::Hash; use std::hash::Hash;
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Alternative { pub enum Alternative {
Tombstone, Tombstone,
@ -158,7 +159,7 @@ pub trait IStore {
async fn row_poll(&self, value: &RowRef) -> Result<RowVal, StorageError>; async fn row_poll(&self, value: &RowRef) -> Result<RowVal, StorageError>;
async fn blob_fetch(&self, blob_ref: &BlobRef) -> Result<BlobVal, StorageError>; async fn blob_fetch(&self, blob_ref: &BlobRef) -> Result<BlobVal, StorageError>;
async fn blob_insert(&self, blob_val: BlobVal) -> Result<(), StorageError>; async fn blob_insert(&self, blob_val: BlobVal) -> Result<String, StorageError>;
async fn blob_copy(&self, src: &BlobRef, dst: &BlobRef) -> Result<(), StorageError>; async fn blob_copy(&self, src: &BlobRef, dst: &BlobRef) -> Result<(), StorageError>;
async fn blob_list(&self, prefix: &str) -> Result<Vec<BlobRef>, StorageError>; async fn blob_list(&self, prefix: &str) -> Result<Vec<BlobRef>, StorageError>;
async fn blob_rm(&self, blob_ref: &BlobRef) -> Result<(), StorageError>; async fn blob_rm(&self, blob_ref: &BlobRef) -> Result<(), StorageError>;

32
aerogramme/Cargo.toml Normal file
View file

@ -0,0 +1,32 @@
[package]
name = "aerogramme"
version = "0.3.0"
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
edition = "2021"
license = "EUPL-1.2"
description = "A robust email server"
[dependencies]
aero-user.workspace = true
aero-proto.workspace = true
anyhow.workspace = true
backtrace.workspace = true
futures.workspace = true
tokio.workspace = true
log.workspace = true
nix.workspace = true
clap.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
rpassword.workspace = true
[dev-dependencies]
reqwest.workspace = true
aero-dav.workspace = true
quick-xml.workspace = true
[[test]]
name = "behavior"
path = "tests/behavior.rs"
harness = false

Some files were not shown because too many files have changed in this diff Show more