From cdae585ee64e466e843b10dd043fe02dba7d4af0 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Sat, 15 Aug 2020 22:10:49 +0300 Subject: [PATCH] Initial commit --- .gitignore | 1 + Cargo.lock | 2149 ++++++++++++++++++++++++++++++ Cargo.toml | 6 + LICENSE | 661 +++++++++ README.md | 190 +++ cli/Cargo.toml | 21 + cli/README.md | 5 + cli/src/main.rs | 472 +++++++ core/Cargo.toml | 23 + core/README.md | 7 + core/build.rs | 32 + core/src/config.rs | 109 ++ core/src/db.rs | 604 +++++++++ core/src/errors.rs | 30 + core/src/lib.rs | 38 + core/src/mail.rs | 85 ++ core/src/mail/message_filters.rs | 206 +++ core/src/models.rs | 226 ++++ core/src/models/changesets.rs | 80 ++ core/src/schema.sql | 74 + core/src/schema.sql.m4 | 78 ++ rest-http/Cargo.toml | 21 + rest-http/README.md | 5 + rest-http/src/main.rs | 99 ++ 24 files changed, 5222 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 cli/Cargo.toml create mode 100644 cli/README.md create mode 100644 cli/src/main.rs create mode 100644 core/Cargo.toml create mode 100644 core/README.md create mode 100644 core/build.rs create mode 100644 core/src/config.rs create mode 100644 core/src/db.rs create mode 100644 core/src/errors.rs create mode 100644 core/src/lib.rs create mode 100644 core/src/mail.rs create mode 100644 core/src/mail/message_filters.rs create mode 100644 core/src/models.rs create mode 100644 core/src/models/changesets.rs create mode 100644 core/src/schema.sql create mode 100644 core/src/schema.sql.m4 create mode 100644 rest-http/Cargo.toml create mode 100644 rest-http/README.md create mode 100644 rest-http/src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..ac35ea5 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2149 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "async-channel" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "once_cell", + "slab", +] + +[[package]] +name = "async-fs" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b3ca4f8ff117c37c278a2f7415ce9be55560b846b5bc4412aaa5d29c1c3dae2" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a811e6a479f2439f0c04038796b5cfb3d2ad56c230e0f2d3f7b04d68cfee607b" +dependencies = [ + "concurrent-queue", + "futures-lite", + "libc", + "log", + "once_cell", + "parking", + "polling", + "slab", + "socket2", + "waker-fn", + "winapi", +] + +[[package]] +name = "async-lock" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e97a171d191782fba31bb902b14ad94e24a68145032b7eedf871ab0bc0d077b6" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-net" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5373304df79b9b4395068fb080369ec7178608827306ce4d081cba51cac551df" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf2c06e30a24e8c78a3987d07f0930edf76ef35e027e7bdb063fccafdad1f60c" +dependencies = [ + "async-io", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "libc", + "once_cell", + "signal-hook", + "winapi", +] + +[[package]] +name = "async-stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad5c83079eae9969be7fadefe640a1c566901f05ff91ab221de4b6f68d9507e" +dependencies = [ + "async-stream-impl", + "futures-core", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-task" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30696a84d817107fc028e049980e09d5e140e8da8f1caeb17e8e950658a3cea9" + +[[package]] +name = "atomic-waker" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11a17d453482a265fd5f8479f2a3f405566e6ca627837aaddb85af8b1ab8ef61" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6ccb65d468978a086b69884437ded69a90faab3bbe6e67f242173ea728acccc" +dependencies = [ + "async-channel", + "async-task", + "atomic-waker", + "fastrand", + "futures-lite", + "once_cell", +] + +[[package]] +name = "buf_redux" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" +dependencies = [ + "memchr", + "safemem", +] + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + +[[package]] +name = "cache-padded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "serde", + "time", + "winapi", +] + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "concurrent-queue" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" +dependencies = [ + "cache-padded", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "cpufeatures" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "data-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +dependencies = [ + "block-buffer 0.10.2", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "encoding" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" +dependencies = [ + "encoding-index-japanese", + "encoding-index-korean", + "encoding-index-simpchinese", + "encoding-index-singlebyte", + "encoding-index-tradchinese", +] + +[[package]] +name = "encoding-index-japanese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-korean" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-simpchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-singlebyte" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-tradchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding_index_tests" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" + +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "backtrace", + "version_check", +] + +[[package]] +name = "event-listener" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71" + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +dependencies = [ + "instant", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" + +[[package]] +name = "futures-executor" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" + +[[package]] +name = "futures-lite" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" + +[[package]] +name = "futures-task" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" + +[[package]] +name = "futures-util" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.10.2+wasi-snapshot-preview1", +] + +[[package]] +name = "gimli" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" + +[[package]] +name = "h2" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util 0.7.1", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "headers" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d" +dependencies = [ + "base64", + "bitflags", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha-1 0.10.0", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "http" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff8670570af52249509a86f5e3e18a08c60b177071826898fde8997cf5f6bfbb" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "hyper" +version = "0.14.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" +dependencies = [ + "autocfg", + "hashbrown", + "serde", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b" + +[[package]] +name = "libloading" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "lock_api" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "mailpot" +version = "0.1.0" +dependencies = [ + "chrono", + "error-chain", + "log", + "melib", + "rusqlite", + "serde", + "serde_json", + "toml", + "xdg", +] + +[[package]] +name = "mailpot-cli" +version = "0.1.0" +dependencies = [ + "mailpot", + "stderrlog", + "structopt", +] + +[[package]] +name = "mailpot-http" +version = "0.1.0" +dependencies = [ + "mailpot", + "tokio", + "warp", +] + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "melib" +version = "0.7.2" +source = "git+https://github.com/meli/meli?branch=master#721891c2955e9f5e223949bde2dd43604cec8390" +dependencies = [ + "async-stream", + "base64", + "bincode", + "bitflags", + "data-encoding", + "encoding", + "futures", + "indexmap", + "libc", + "libloading", + "native-tls", + "nix", + "nom", + "serde", + "serde_derive", + "smallvec", + "smol", + "unicode-segmentation", + "uuid", + "xdg", + "xdg-utils", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "wasi 0.11.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi", +] + +[[package]] +name = "multipart" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00dec633863867f29cb39df64a397cdf4a6354708ddd7759f70c7fb51c5f9182" +dependencies = [ + "buf_redux", + "httparse", + "log", + "mime", + "mime_guess", + "quick-error", + "rand", + "safemem", + "tempfile", + "twoway", +] + +[[package]] +name = "native-tls" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nix" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f17df307904acd05aa8e32e97bb20f2a0df1728bbc2d771ae8f9a90463441e9" +dependencies = [ + "bitflags", + "cfg-if", + "libc", + "memoffset", +] + +[[package]] +name = "nom" +version = "7.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "ntapi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" +dependencies = [ + "winapi", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40bec70ba014595f99f7aa110b84331ffe1ee9aece7fe6f387cc7e3ecda4d456" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openssl" +version = "0.10.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-sys", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + +[[package]] +name = "parking_lot" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pin-project" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" + +[[package]] +name = "polling" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259" +dependencies = [ + "cfg-if", + "libc", + "log", + "wepoll-ffi", + "winapi", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall", + "thiserror", +] + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "rusqlite" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85127183a999f7db96d1a976a309eebbfb6ea3b0b400ddd8340190129de6eb7a" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "memchr", + "smallvec", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + +[[package]] +name = "ryu" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" + +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi", +] + +[[package]] +name = "scoped-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "security-framework" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f972498cf015f7c0746cac89ebe1d6ef10c293b94175a243a2d9442c163d9944" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha-1" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha-1" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.3", +] + +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + +[[package]] +name = "signal-hook" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "647c97df271007dcea485bb74ffdb57f2e683f1306c854f468a0c244badabf2d" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" + +[[package]] +name = "smallvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +dependencies = [ + "serde", +] + +[[package]] +name = "smol" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cf3b5351f3e783c1d79ab5fc604eeed8b8ae9abd36b166e8b87a089efd85e4" +dependencies = [ + "async-channel", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-net", + "async-process", + "blocking", + "futures-lite", + "once_cell", +] + +[[package]] +name = "socket2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "stderrlog" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45a53e2eff3e94a019afa6265e8ee04cb05b9d33fe9f5078b14e4e391d155a38" +dependencies = [ + "atty", + "chrono", + "log", + "termcolor", + "thread_local", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "structopt" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" +dependencies = [ + "clap", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ff7c592601f11445996a06f8ad0c27f094a58857c2f89e97974ab9235b92c52" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce653fb475565de9f6fb0614b28bca8df2c430c0cf84bcd9c843f15de5414cc" +dependencies = [ + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "once_cell", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "winapi", +] + +[[package]] +name = "tokio-macros" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "511de3f85caf1c98983545490c3d09685fa8eb634e57eec22bb4db271f46cbd8" +dependencies = [ + "futures-util", + "log", + "pin-project", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0edfdeb067411dba2044da6d1cb2df793dd35add7888d73c16e3381ded401764" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + +[[package]] +name = "tower-service" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" + +[[package]] +name = "tracing" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6b8ad3567499f98a1db7a752b07a7c8c7c7c34c332ec00effb2b0027974b7c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "tungstenite" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0b2d8558abd2e276b0a8df5c05a2ec762609344191e5fd23e292c910e9165b5" +dependencies = [ + "base64", + "byteorder", + "bytes", + "http", + "httparse", + "log", + "rand", + "sha-1 0.9.8", + "thiserror", + "url", + "utf-8", +] + +[[package]] +name = "twoway" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" +dependencies = [ + "memchr", +] + +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" + +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + +[[package]] +name = "unicode-xid" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "uuid" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfcd319456c4d6ea10087ed423473267e1a071f3bc0aa89f80d60997843c6f0" +dependencies = [ + "getrandom", + "serde", + "sha1_smol", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "warp" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cef4e1e9114a4b7f1ac799f16ce71c14de5778500c5450ec6b7b920c55b587e" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "headers", + "http", + "hyper", + "log", + "mime", + "mime_guess", + "multipart", + "percent-encoding", + "pin-project", + "scoped-tls", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-stream", + "tokio-tungstenite", + "tokio-util 0.6.9", + "tower-service", + "tracing", +] + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wepoll-ffi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" +dependencies = [ + "cc", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + +[[package]] +name = "xdg" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4583db5cbd4c4c0303df2d15af80f0539db703fa1c68802d4cbbd2dd0f88f6" +dependencies = [ + "dirs", +] + +[[package]] +name = "xdg-utils" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db9fefe62d5969721e2cfc529e6a760901cc0da422b6d67e7bfd18e69490dba6" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..fd216ce --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,6 @@ +[workspace] +members = [ + "core", + "cli", + "rest-http", +] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0ad25db --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..38b09c8 --- /dev/null +++ b/README.md @@ -0,0 +1,190 @@ +# Mailpot - WIP mailing list manager + +Crates: + +- `core` +- `cli` a command line tool to manage lists +- `rest-http` a REST http server to manage lists + +## Project goals + +- easy setup +- extensible through Rust API as a library +- extensible through HTTP REST API as an HTTP server, with webhooks +- basic management through CLI +- replaceable lightweight web archiver +- custom storage? +- useful for both newsletters, discussions + +## Initial setup + +Check where `mpot` expects your database file to be: + +```shell +$ cargo run --bin mpot -- db-location +Configuration file /home/user/.config/mailpot/config.toml doesn't exist +``` + +Uuugh, oops. + +```shell +$ mkdir -p /home/user/.config/mailpot +$ echo 'send_mail = { "type" = "ShellCommand", "value" = "/usr/bin/false" }' > /home/user/.config/mailpot/config.toml +$ cargo run --bin mpot -- db-location +/home/user/.local/share/mailpot/mpot.db +``` + +Now you can initialize the database file: + +```shell +$ mkdir -p /home/user/.local/share/mailpot/ +$ sqlite3 /home/user/.local/share/mailpot/mpot.db < ./core/src/schema.sql +``` + +## Examples + +```text +% mpot help +mailpot 0.1.0 +mini mailing list manager + +USAGE: + mpot [FLAGS] [OPTIONS] + +FLAGS: + -d, --debug Activate debug mode + -h, --help Prints help information + -V, --version Prints version information + +OPTIONS: + -c, --config Set config file + +SUBCOMMANDS: + create-list Create new list + db-location Prints database filesystem location + help Prints this message or the help of the given subcommand(s) + list Mailing list management + list-lists Lists all registered mailing lists + post Post message from STDIN to list +``` + +### Receiving mail + +```shell +$ cat list-request.eml | cargo run --bin mpot -- -vvvvvv post --dry-run +``` + +
output + +```shell +TRACE - Received envelope to post: Envelope { + Subject: "unsubscribe", + Date: "Tue, 04 Aug 2020 14:10:13 +0300", + From: [ + Address::Mailbox { + display_name: "Mxxxx Pxxxxxxxxxxxx", + address_spec: "exxxxx@localhost", + }, + ], + To: [ + Address::Mailbox { + display_name: "", + address_spec: "test-announce+request@localhost", + }, + ], + Message-ID: "", + In-Reply-To: None, + References: None, + Hash: 12581897380059220314, +} +TRACE - unsubscribe action for addresses [Address::Mailbox { display_name: "Mxxxx Pxxxxxxxxxxxx", address_spec: "exxxxx@localhost" }] in list [#2 test-announce] test announcements +TRACE - Is post related to list [#1 test] Test list ? false +``` +
+ +```shell +$ cat list-post.eml | cargo run --bin mpot -- -vvvvvv post --dry-run +``` + +
output + +```shell +TRACE - Received envelope to post: Envelope { + Subject: "[test-announce] new test releases", + Date: "Tue, 04 Aug 2020 14:10:13 +0300", + From: [ + Address::Mailbox { + display_name: "Mxxxx Pxxxxxxxxxxxx", + address_spec: "exxxxx@localhost", + }, + ], + To: [ + Address::Mailbox { + display_name: "", + address_spec: "test-announce@localhost", + }, + ], + Message-ID: "", + In-Reply-To: None, + References: None, + Hash: 10220641455578979007, +} +TRACE - Is post related to list [#1 test] Test list ? false +TRACE - Is post related to list [#2 test-announce] test announcements ? true +TRACE - Examining list "test announcements" +TRACE - List members [ + ListMembership { + list: 2, + address: "exxxxx@localhost", + name: None, + digest: false, + hide_address: false, + receive_duplicates: false, + receive_own_posts: true, + receive_confirmation: true, + enabled: true, + }, +] +TRACE - Running FixCRLF filter +TRACE - Running PostRightsCheck filter +TRACE - Running AddListHeaders filter +TRACE - Running FinalizeRecipients filter +TRACE - examining member ListMembership { list: 2, address: "exxxxx@localhost", name: None, digest: false, hide_address: false, receive_duplicates: false, receive_own_posts: true, receive_confirmation: true, enabled: true } +TRACE - member is submitter +TRACE - Member gets copy +TRACE - result Ok( + Post { + list: MailingList { + pk: 2, + name: "test announcements", + id: "test-announce", + address: "test-announce@localhost", + description: None, + archive_url: None, + }, + from: Address::Mailbox { + display_name: "Mxxxx Pxxxxxxxxxxxx", + address_spec: "exxxxx@localhost", + }, + members: 1, + bytes: 851, + policy: None, + to: [ + Address::Mailbox { + display_name: "", + address_spec: "test-announce@localhost", + }, + ], + action: Accept { + recipients: [ + Address::Mailbox { + display_name: "", + address_spec: "exxxxx@localhost", + }, + ], + digests: [], + }, + }, +) +``` +
diff --git a/cli/Cargo.toml b/cli/Cargo.toml new file mode 100644 index 0000000..b3743e0 --- /dev/null +++ b/cli/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "mailpot-cli" +version = "0.1.0" +authors = ["Manos Pitsidianakis "] +edition = "2018" +license = "LICENSE" +readme = "README.md" +description = "mailing list manager" +repository = "https://github.com/meli/mailpot" +keywords = ["mail", "mailing-lists" ] +categories = ["email"] +default-run = "mpot" + +[[bin]] +name = "mpot" +path = "src/main.rs" + +[dependencies] +mailpot = { version = "0.1.0", path = "../core" } +structopt = "0.3.16" +stderrlog = "^0.5" diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 0000000..f5e323d --- /dev/null +++ b/cli/README.md @@ -0,0 +1,5 @@ +# mailpot-cli + +```shell +cargo run --bin mpot -- help +``` diff --git a/cli/src/main.rs b/cli/src/main.rs new file mode 100644 index 0000000..f2302f4 --- /dev/null +++ b/cli/src/main.rs @@ -0,0 +1,472 @@ +/* + * This file is part of mailpot + * + * Copyright 2020 - Manos Pitsidianakis + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +extern crate mailpot; +extern crate stderrlog; + +pub use mailpot::config::*; +pub use mailpot::db::*; +pub use mailpot::errors::*; +pub use mailpot::mail::*; +pub use mailpot::models::changesets::*; +pub use mailpot::models::*; +pub use mailpot::*; +use std::path::PathBuf; +use structopt::StructOpt; + +#[derive(Debug, StructOpt)] +#[structopt( + name = "mailpot", + about = "mini mailing list manager", + author = "Manos Pitsidianakis " +)] +struct Opt { + /// Activate debug mode + #[structopt(short, long)] + debug: bool, + + /// Set config file + #[structopt(short, long, parse(from_os_str))] + #[allow(dead_code)] + config: Option, + #[structopt(flatten)] + cmd: Command, + /// Silence all output + #[structopt(short = "q", long = "quiet")] + quiet: bool, + /// Verbose mode (-v, -vv, -vvv, etc) + #[structopt(short = "v", long = "verbose", parse(from_occurrences))] + verbose: usize, + /// Timestamp (sec, ms, ns, none) + #[structopt(short = "t", long = "timestamp")] + ts: Option, +} + +#[derive(Debug, StructOpt)] +enum Command { + ///Prints database filesystem location + DbLocation, + ///Dumps database data to STDOUT + DumpDatabase, + ///Lists all registered mailing lists + ListLists, + ///Mailing list management + List { + ///Selects mailing list to operate on + list_id: String, + #[structopt(subcommand)] + cmd: ListCommand, + }, + ///Create new list + CreateList { + ///List name + #[structopt(long)] + name: String, + ///List ID + #[structopt(long)] + id: String, + ///List e-mail address + #[structopt(long)] + address: String, + ///List description + #[structopt(long)] + description: Option, + ///List archive URL + #[structopt(long)] + archive_url: Option, + }, + ///Post message from STDIN to list + Post { + #[structopt(long)] + dry_run: bool, + }, +} + +#[derive(Debug, StructOpt)] +enum ListCommand { + /// List members of list. + Members, + /// Add member to list. + AddMember { + /// E-mail address + #[structopt(long)] + address: String, + /// Name + #[structopt(long)] + name: Option, + /// Send messages as digest? + #[structopt(long)] + digest: bool, + /// Hide message from list when posting? + #[structopt(long)] + hide_address: bool, + /// Hide message from list when posting? + #[structopt(long)] + /// Receive confirmation email when posting? + receive_confirmation: Option, + #[structopt(long)] + /// Receive posts from list even if address exists in To or Cc header? + receive_duplicates: Option, + #[structopt(long)] + /// Receive own posts from list? + receive_own_posts: Option, + #[structopt(long)] + /// Is subscription enabled? + enabled: Option, + }, + /// Remove member from list. + RemoveMember { + #[structopt(long)] + /// E-mail address + address: String, + }, + /// Update membership info. + UpdateMembership { + address: String, + name: Option, + digest: Option, + hide_address: Option, + receive_duplicates: Option, + receive_own_posts: Option, + receive_confirmation: Option, + enabled: Option, + }, + /// Alias for update-membership --enabled true + EnableMembership { address: String }, + /// Alias for update-membership --enabled false + DisableMembership { address: String }, + /// Update mailing list details. + Update { + name: Option, + id: Option, + address: Option, + description: Option, + archive_url: Option, + }, + /// Show mailing list health status. + Health, + /// Show mailing list info. + Info, +} + +fn run_app(opt: Opt) -> Result<()> { + if opt.debug { + println!("DEBUG: {:?}", &opt); + } + Configuration::init()?; + use Command::*; + match opt.cmd { + DbLocation => { + println!("{}", Database::db_path()?.display()); + } + DumpDatabase => { + let db = Database::open_or_create_db()?; + let lists = db.list_lists()?; + let mut stdout = std::io::stdout(); + serde_json::to_writer_pretty(&mut stdout, &lists)?; + for l in &lists { + serde_json::to_writer_pretty(&mut stdout, &db.list_members(l.pk)?)?; + } + } + ListLists => { + let db = Database::open_or_create_db()?; + let lists = db.list_lists()?; + if lists.is_empty() { + println!("No lists found."); + } else { + for l in lists { + println!("- {} {:?}", l.id, l); + let list_owners = db.get_list_owners(l.pk)?; + if list_owners.is_empty() { + println!("\tList owners: None"); + } else { + println!("\tList owners:"); + for o in list_owners { + println!("\t- {}", o); + } + } + if let Some(s) = db.get_list_policy(l.pk)? { + println!("\tList policy: {}", s); + } else { + println!("\tList policy: None"); + } + println!(); + } + } + } + List { list_id, cmd } => { + let db = Database::open_or_create_db()?; + let mut lists = db.list_lists()?; + let list = if let Some(pos) = lists.iter().position(|l| l.id == list_id) { + lists.remove(pos) + } else { + return Err(format!("No list with id {} was found", list_id).into()); + }; + use ListCommand::*; + match cmd { + Members => { + let members = db.list_members(list.pk)?; + if members.is_empty() { + println!("No members found."); + } else { + println!("Members of list {}", list.id); + for l in members { + println!("- {}", &l); + } + } + } + AddMember { + address, + name, + digest, + hide_address, + receive_confirmation, + receive_duplicates, + receive_own_posts, + enabled, + } => { + db.add_member( + list.pk, + ListMembership { + pk: 0, + list: list.pk, + name, + address, + digest, + hide_address, + receive_confirmation: receive_confirmation.unwrap_or(true), + receive_duplicates: receive_duplicates.unwrap_or(true), + receive_own_posts: receive_own_posts.unwrap_or(false), + enabled: enabled.unwrap_or(true), + }, + )?; + } + RemoveMember { address } => { + loop { + println!( + "Are you sure you want to remove membership of {} from list {}? [Yy/n]", + address, list + ); + let mut input = String::new(); + std::io::stdin().read_line(&mut input)?; + if input.trim() == "Y" || input.trim() == "y" || input.trim() == "" { + break; + } else if input.trim() == "n" { + return Ok(()); + } + } + + db.remove_member(list.pk, &address)?; + } + Health => { + println!("{} health:", list); + let list_owners = db.get_list_owners(list.pk)?; + let list_policy = db.get_list_policy(list.pk)?; + if list_owners.is_empty() { + println!("\tList has no owners: you should add at least one."); + } + if list_policy.is_none() { + } else { + println!("\tList has no post policy: you should add one."); + } + } + Info => { + println!("{} info:", list); + let list_owners = db.get_list_owners(list.pk)?; + let list_policy = db.get_list_policy(list.pk)?; + let members = db.list_members(list.pk)?; + if members.is_empty() { + println!("No members."); + } else if members.len() == 1 { + println!("1 member."); + } else { + println!("{} members.", members.len()); + } + if list_owners.is_empty() { + println!("List owners: None"); + } else { + println!("List owners:"); + for o in list_owners { + println!("\t- {}", o); + } + } + if let Some(s) = list_policy { + println!("List policy: {}", s); + } else { + println!("List policy: None"); + } + } + UpdateMembership { + address, + name, + digest, + hide_address, + receive_duplicates, + receive_own_posts, + receive_confirmation, + enabled, + } => { + let name = if name + .as_ref() + .map(|s: &String| s.is_empty()) + .unwrap_or(false) + { + None + } else { + Some(name) + }; + let changeset = ListMembershipChangeset { + list: list.pk, + address, + name, + digest, + hide_address, + receive_duplicates, + receive_own_posts, + receive_confirmation, + enabled, + }; + db.update_member(changeset)?; + } + EnableMembership { address } => { + let changeset = ListMembershipChangeset { + list: list.pk, + address, + name: None, + digest: None, + hide_address: None, + receive_duplicates: None, + receive_own_posts: None, + receive_confirmation: None, + enabled: Some(true), + }; + db.update_member(changeset)?; + } + DisableMembership { address } => { + let changeset = ListMembershipChangeset { + list: list.pk, + address, + name: None, + digest: None, + hide_address: None, + receive_duplicates: None, + receive_own_posts: None, + receive_confirmation: None, + enabled: Some(false), + }; + db.update_member(changeset)?; + } + Update { + name, + id, + address, + description, + archive_url, + } => { + let description = if description + .as_ref() + .map(|s: &String| s.is_empty()) + .unwrap_or(false) + { + None + } else { + Some(description) + }; + let archive_url = if archive_url + .as_ref() + .map(|s: &String| s.is_empty()) + .unwrap_or(false) + { + None + } else { + Some(archive_url) + }; + let changeset = MailingListChangeset { + pk: list.pk, + name, + id, + address, + description, + archive_url, + }; + db.update_list(changeset)?; + } + } + } + CreateList { + name, + id, + address, + description, + archive_url, + } => { + let db = Database::open_or_create_db()?; + db.create_list(MailingList { + pk: 0, + name, + id, + description, + address, + archive_url, + })?; + } + Post { dry_run } => { + if opt.debug { + println!("post dry_run{:?}", dry_run); + } + + use melib::Envelope; + use std::io::Read; + + let mut input = String::new(); + std::io::stdin().read_to_string(&mut input)?; + match Envelope::from_bytes(input.as_bytes(), None) { + Ok(env) => { + let db = Database::open_or_create_db()?; + db.post(env, input.as_bytes(), dry_run)?; + } + Err(err) => { + eprintln!("Could not parse message: {}", err); + let p = Configuration::save_message(input)?; + eprintln!("Message saved at {}", p.display()); + return Err(err.into()); + } + } + } + } + + Ok(()) +} + +fn main() -> std::result::Result<(), i32> { + let opt = Opt::from_args(); + stderrlog::new() + .module(module_path!()) + .module("mailpot") + .quiet(opt.quiet) + .verbosity(opt.verbose) + .timestamp(opt.ts.unwrap_or(stderrlog::Timestamp::Off)) + .init() + .unwrap(); + if let Err(err) = run_app(opt) { + println!("{}", err); + std::process::exit(-1); + } + Ok(()) +} diff --git a/core/Cargo.toml b/core/Cargo.toml new file mode 100644 index 0000000..3669bc2 --- /dev/null +++ b/core/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "mailpot" +version = "0.1.0" +authors = ["Manos Pitsidianakis "] +edition = "2018" +license = "LICENSE" +readme = "README.md" +description = "mailing list manager" +repository = "https://github.com/meli/mailpot" +keywords = ["mail", "mailing-lists" ] +categories = ["email"] + +[dependencies] +chrono = { version = "^0.4", features = ["serde", ] } +error-chain = "0.12.4" +#melib = { version = "*", default-features = false, features = ["smtp", "unicode_algorithms"], path="../../meli/melib", branch = "master" } +melib = { version = "*", default-features = false, features = ["smtp", "unicode_algorithms"], git="https://github.com/meli/meli", branch = "master" } +rusqlite = {version = "^0.27" } +serde = { version = "^1", features = ["derive", ]} +serde_json = "^1" +toml = "^0.5" +log = "0.4" +xdg = "2.1.0" diff --git a/core/README.md b/core/README.md new file mode 100644 index 0000000..452b1bc --- /dev/null +++ b/core/README.md @@ -0,0 +1,7 @@ +# mailpot-core + +Initialize `sqlite3` database + +```shell +sqlite3 mpot.db < ./src/schema.sql +``` diff --git a/core/build.rs b/core/build.rs new file mode 100644 index 0000000..ce35cba --- /dev/null +++ b/core/build.rs @@ -0,0 +1,32 @@ +/* + * This file is part of mailpot + * + * Copyright 2020 - Manos Pitsidianakis + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +use std::io::Write; +use std::process::Command; + +fn main() { + println!("cargo:rerun-if-changed=src/schema.sql.m4"); + + let output = Command::new("m4") + .arg("./src/schema.sql.m4") + .output() + .unwrap(); + let mut file = std::fs::File::create("./src/schema.sql").unwrap(); + file.write_all(&output.stdout).unwrap(); +} diff --git a/core/src/config.rs b/core/src/config.rs new file mode 100644 index 0000000..2e02cc6 --- /dev/null +++ b/core/src/config.rs @@ -0,0 +1,109 @@ +/* + * This file is part of mailpot + * + * Copyright 2020 - Manos Pitsidianakis + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +use super::errors::*; +use chrono::prelude::*; +use std::cell::RefCell; +use std::io::{Read, Write}; +use std::os::unix::fs::PermissionsExt; +use std::path::PathBuf; + +thread_local!(pub static CONFIG: RefCell = RefCell::new(Configuration::new())); + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(tag = "type", content = "value")] +pub enum SendMail { + Smtp(melib::smtp::SmtpServerConf), + ShellCommand(String), +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Configuration { + pub send_mail: SendMail, + #[serde(default = "default_storage_fn")] + pub storage: String, +} + +impl Configuration { + pub(crate) fn new() -> Self { + Configuration { + send_mail: SendMail::ShellCommand(String::new()), + storage: "sqlite3".into(), + } + } + + pub fn init() -> Result<()> { + let path = + xdg::BaseDirectories::with_prefix("mailpot")?.place_config_file("config.toml")?; + if !path.exists() { + return Err(format!("Configuration file {} doesn't exist", path.display()).into()); + } + let mut s = String::new(); + let mut file = std::fs::File::open(&path)?; + file.read_to_string(&mut s)?; + let config: Configuration = toml::from_str(&s)?; + CONFIG.with(|f| { + *f.borrow_mut() = config; + }); + + Ok(()) + } + + pub fn data_directory() -> Result { + Ok(xdg::BaseDirectories::with_prefix("mailpot")?.get_data_home()) + } + + pub fn save_message_to_path(msg: &str, mut path: PathBuf) -> Result { + let now = Local::now().timestamp(); + path.push(format!("{}-failed.eml", now)); + + let mut file = std::fs::File::create(&path)?; + let metadata = file.metadata()?; + let mut permissions = metadata.permissions(); + + permissions.set_mode(0o600); // Read/write for owner only. + file.set_permissions(permissions)?; + file.write_all(msg.as_bytes())?; + file.flush()?; + Ok(path) + } + + pub fn save_message(msg: String) -> Result { + match Configuration::data_directory() + .and_then(|path| Self::save_message_to_path(&msg, path)) + { + Ok(p) => return Ok(p), + Err(err) => { + eprintln!("{}", err); + } + }; + match Self::save_message_to_path(&msg, PathBuf::from(".")) { + Ok(p) => return Ok(p), + Err(err) => { + eprintln!("{}", err); + } + } + let temp_path = std::env::temp_dir(); + Self::save_message_to_path(&msg, temp_path) + } +} + +fn default_storage_fn() -> String { + "sqlite3".to_string() +} diff --git a/core/src/db.rs b/core/src/db.rs new file mode 100644 index 0000000..9e3abdf --- /dev/null +++ b/core/src/db.rs @@ -0,0 +1,604 @@ +/* + * This file is part of mailpot + * + * Copyright 2020 - Manos Pitsidianakis + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +use super::*; +use melib::Envelope; +use models::changesets::*; +use rusqlite::Connection as DbConnection; +use rusqlite::OptionalExtension; +use std::path::PathBuf; + +const DB_NAME: &str = "mpot.db"; + +pub struct Database { + pub connection: DbConnection, +} + +impl Database { + pub fn list_lists(&self) -> Result>> { + let mut stmt = self.connection.prepare("SELECT * FROM mailing_lists;")?; + let list_iter = stmt.query_map([], |row| { + let pk = row.get("pk")?; + Ok(DbVal( + MailingList { + pk, + name: row.get("name")?, + id: row.get("id")?, + address: row.get("address")?, + description: row.get("description")?, + archive_url: row.get("archive_url")?, + }, + pk, + )) + })?; + + let mut ret = vec![]; + for list in list_iter { + let list = list?; + ret.push(list); + } + Ok(ret) + } + + pub fn get_list(&self, pk: i64) -> Result>> { + let mut stmt = self + .connection + .prepare("SELECT * FROM mailing_lists WHERE pk = ?;")?; + let ret = stmt + .query_row([&pk], |row| { + let pk = row.get("pk")?; + Ok(DbVal( + MailingList { + pk, + name: row.get("name")?, + id: row.get("id")?, + address: row.get("address")?, + description: row.get("description")?, + archive_url: row.get("archive_url")?, + }, + pk, + )) + }) + .optional()?; + + Ok(ret) + } + + pub fn create_list(&self, new_val: MailingList) -> Result> { + let mut stmt = self + .connection + .prepare("INSERT INTO mailing_lists(name, id, address, description, archive_url) VALUES(?, ?, ?, ?, ?) RETURNING *;")?; + let ret = stmt.query_row( + rusqlite::params![ + &new_val.name, + &new_val.id, + &new_val.address, + new_val.description.as_ref(), + new_val.archive_url.as_ref(), + ], + |row| { + let pk = row.get("pk")?; + Ok(DbVal( + MailingList { + pk, + name: row.get("name")?, + id: row.get("id")?, + address: row.get("address")?, + description: row.get("description")?, + archive_url: row.get("archive_url")?, + }, + pk, + )) + }, + )?; + + trace!("create_list {:?}.", &ret); + Ok(ret) + } + + pub fn update_list(&self, _change_set: MailingListChangeset) -> Result<()> { + /* + diesel::update(mailing_lists::table) + .set(&set) + .execute(&self.connection)?; + */ + Ok(()) + } + + pub fn get_list_policy(&self, pk: i64) -> Result>> { + let mut stmt = self + .connection + .prepare("SELECT * FROM post_policy WHERE list = ?;")?; + let ret = stmt + .query_row([&pk], |row| { + let pk = row.get("pk")?; + Ok(DbVal( + PostPolicy { + pk, + list: row.get("list")?, + announce_only: row.get("announce_only")?, + subscriber_only: row.get("subscriber_only")?, + approval_needed: row.get("approval_needed")?, + }, + pk, + )) + }) + .optional()?; + + Ok(ret) + } + + pub fn get_list_owners(&self, pk: i64) -> Result>> { + let mut stmt = self + .connection + .prepare("SELECT * FROM list_owner WHERE list = ?;")?; + let list_iter = stmt.query_map([&pk], |row| { + let pk = row.get("pk")?; + Ok(DbVal( + ListOwner { + pk, + list: row.get("list")?, + address: row.get("address")?, + name: row.get("name")?, + }, + pk, + )) + })?; + + let mut ret = vec![]; + for list in list_iter { + let list = list?; + ret.push(list); + } + Ok(ret) + } + + pub fn list_members(&self, pk: i64) -> Result>> { + let mut stmt = self + .connection + .prepare("SELECT * FROM membership WHERE list = ?;")?; + let list_iter = stmt.query_map([&pk], |row| { + let pk = row.get("pk")?; + Ok(DbVal( + ListMembership { + pk: row.get("pk")?, + list: row.get("list")?, + address: row.get("address")?, + name: row.get("name")?, + digest: row.get("digest")?, + hide_address: row.get("hide_address")?, + receive_duplicates: row.get("receive_duplicates")?, + receive_own_posts: row.get("receive_own_posts")?, + receive_confirmation: row.get("receive_confirmation")?, + enabled: row.get("enabled")?, + }, + pk, + )) + })?; + + let mut ret = vec![]; + for list in list_iter { + let list = list?; + ret.push(list); + } + Ok(ret) + } + + pub fn add_member( + &self, + list_pk: i64, + mut new_val: ListMembership, + ) -> Result> { + new_val.list = list_pk; + let mut stmt = self + .connection + .prepare("INSERT INTO membership(list, address, name, enabled, digest, hide_address, receive_duplicates, receive_own_posts, receive_confirmation) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *;")?; + let ret = stmt.query_row( + rusqlite::params![ + &new_val.list, + &new_val.address, + &new_val.name, + &new_val.enabled, + &new_val.digest, + &new_val.hide_address, + &new_val.receive_duplicates, + &new_val.receive_own_posts, + &new_val.receive_confirmation + ], + |row| { + let pk = row.get("pk")?; + Ok(DbVal( + ListMembership { + pk, + list: row.get("list")?, + address: row.get("address")?, + name: row.get("name")?, + digest: row.get("digest")?, + hide_address: row.get("hide_address")?, + receive_duplicates: row.get("receive_duplicates")?, + receive_own_posts: row.get("receive_own_posts")?, + receive_confirmation: row.get("receive_confirmation")?, + enabled: row.get("enabled")?, + }, + pk, + )) + }, + )?; + + trace!("add_member {:?}.", &ret); + Ok(ret) + } + + pub fn add_candidate_member(&self, list_pk: i64, mut new_val: ListMembership) -> Result { + new_val.list = list_pk; + let mut stmt = self + .connection + .prepare("INSERT INTO candidate_membership(list, address, name, accepted) VALUES(?, ?, ?, ?) RETURNING pk;")?; + let ret = stmt.query_row( + rusqlite::params![&new_val.list, &new_val.address, &new_val.name, &false,], + |row| { + let pk: i64 = row.get("pk")?; + Ok(pk) + }, + )?; + + trace!("add_candidate_member {:?}.", &ret); + Ok(ret) + } + + pub fn accept_candidate_member(&mut self, pk: i64) -> Result> { + let tx = self.connection.transaction()?; + let mut stmt = tx + .prepare("INSERT INTO membership(list, address, name, enabled, digest, hide_address, receive_duplicates, receive_own_posts, receive_confirmation) FROM (SELECT list, address, name FROM candidate_membership WHERE pk = ?) RETURNING *;")?; + let ret = stmt.query_row(rusqlite::params![&pk], |row| { + let pk = row.get("pk")?; + Ok(DbVal( + ListMembership { + pk, + list: row.get("list")?, + address: row.get("address")?, + name: row.get("name")?, + digest: row.get("digest")?, + hide_address: row.get("hide_address")?, + receive_duplicates: row.get("receive_duplicates")?, + receive_own_posts: row.get("receive_own_posts")?, + receive_confirmation: row.get("receive_confirmation")?, + enabled: row.get("enabled")?, + }, + pk, + )) + })?; + drop(stmt); + tx.execute( + "UPDATE candidate_membership SET accepted = ? WHERE pk = ?;", + [&pk], + )?; + tx.commit()?; + + trace!("accept_candidate_member {:?}.", &ret); + Ok(ret) + } + + pub fn remove_member(&self, list_pk: i64, address: &str) -> Result<()> { + self.connection.execute( + "DELETE FROM membership WHERE list_pk = ? AND address = ?;", + rusqlite::params![&list_pk, &address], + )?; + + Ok(()) + } + + pub fn update_member(&self, _change_set: ListMembershipChangeset) -> Result<()> { + /* + diesel::update(membership::table) + .set(&set) + .execute(&self.connection)?; + */ + Ok(()) + } + + pub fn db_path() -> Result { + let name = DB_NAME; + let data_dir = xdg::BaseDirectories::with_prefix("mailpot")?; + Ok(data_dir.place_data_file(name)?) + } + + pub fn open_db(db_path: PathBuf) -> Result { + if !db_path.exists() { + return Err("Database doesn't exist".into()); + } + Ok(Database { + connection: DbConnection::open(&db_path.to_str().unwrap())?, + }) + } + + pub fn open_or_create_db() -> Result { + let db_path = Self::db_path()?; + let mut set_mode = false; + if !db_path.exists() { + info!("Creating {} database in {}", DB_NAME, db_path.display()); + set_mode = true; + } + let conn = DbConnection::open(&db_path.to_str().unwrap())?; + if set_mode { + use std::os::unix::fs::PermissionsExt; + let file = std::fs::File::open(&db_path)?; + let metadata = file.metadata()?; + let mut permissions = metadata.permissions(); + + permissions.set_mode(0o600); // Read/write for owner only. + file.set_permissions(permissions)?; + } + + Ok(Database { connection: conn }) + } + + pub fn get_list_filters( + &self, + _list: &DbVal, + ) -> Vec> { + use crate::mail::message_filters::*; + vec![ + Box::new(FixCRLF), + Box::new(PostRightsCheck), + Box::new(AddListHeaders), + Box::new(FinalizeRecipients), + ] + } + + pub fn insert_post(&self, list_pk: i64, message: &[u8], env: &Envelope) -> Result { + let address = env.from()[0].to_string(); + let message_id = env.message_id_display(); + let mut stmt = self.connection.prepare( + "INSERT INTO post(list, address, message_id, message) VALUES(?, ?, ?, ?) RETURNING pk;", + )?; + let pk = stmt.query_row( + rusqlite::params![&list_pk, &address, &message_id, &message], + |row| { + let pk: i64 = row.get("pk")?; + Ok(pk) + }, + )?; + + trace!( + "insert_post list_pk {}, from {:?} message_id {:?} post_pk {}.", + list_pk, + address, + message_id, + pk + ); + Ok(pk) + } + + pub fn post(&self, env: Envelope, raw: &[u8], _dry_run: bool) -> Result<()> { + trace!("Received envelope to post: {:#?}", &env); + let tos = env.to().to_vec(); + if tos.is_empty() { + return Err("Envelope To: field is empty!".into()); + } + if env.from().is_empty() { + return Err("Envelope From: field is empty!".into()); + } + let mut lists = self.list_lists()?; + for t in &tos { + if let Some((addr, subaddr)) = t.subaddress("+") { + lists.retain(|list| { + if !addr.contains_address(&list.list_address()) { + return true; + } + if let Err(err) = self.request( + list, + match subaddr.as_str() { + "subscribe" | "request" if env.subject().trim() == "subscribe" => { + ListRequest::Subscribe + } + "unsubscribe" | "request" if env.subject().trim() == "unsubscribe" => { + ListRequest::Unsubscribe + } + "request" => ListRequest::Other(env.subject().trim().to_string()), + _ => { + trace!( + "unknown action = {} for addresses {:?} in list {}", + subaddr, + env.from(), + list + ); + ListRequest::Other(subaddr.trim().to_string()) + } + }, + &env, + raw, + ) { + info!("Processing request returned error: {}", err); + } + false + }); + } + } + + lists.retain(|list| { + trace!( + "Is post related to list {}? {}", + &list, + tos.iter().any(|a| a.contains_address(&list.list_address())) + ); + + tos.iter().any(|a| a.contains_address(&list.list_address())) + }); + if lists.is_empty() { + return Ok(()); + } + + let mut configuration = crate::config::Configuration::new(); + crate::config::CONFIG.with(|f| { + configuration = f.borrow().clone(); + }); + trace!("Configuration is {:#?}", &configuration); + use crate::mail::{ListContext, Post, PostAction}; + for mut list in lists { + trace!("Examining list {}", list.list_id()); + let post_pk = self.insert_post(list.pk, raw, &env)?; + let filters = self.get_list_filters(&list); + let memberships = self.list_members(list.pk)?; + trace!("List members {:#?}", &memberships); + let mut list_ctx = ListContext { + policy: self.get_list_policy(list.pk)?, + list_owners: self.get_list_owners(list.pk)?, + list: &mut list, + memberships: &memberships, + scheduled_jobs: vec![], + }; + let mut post = Post { + pk: post_pk, + from: env.from()[0].clone(), + bytes: raw.to_vec(), + to: env.to().to_vec(), + action: PostAction::Hold, + }; + let result = filters + .into_iter() + .fold(Ok((&mut post, &mut list_ctx)), |p, f| { + p.and_then(|(p, c)| f.feed(p, c)) + }); + trace!("result {:#?}", result); + + let Post { bytes, action, .. } = post; + match configuration.send_mail { + crate::config::SendMail::Smtp(ref smtp_conf) => { + let smtp_conf = smtp_conf.clone(); + use melib::futures; + use melib::smol; + use melib::smtp::*; + let mut conn = smol::future::block_on(smol::spawn( + SmtpConnection::new_connection(smtp_conf.clone()), + ))?; + match action { + PostAction::Accept => { + for job in list_ctx.scheduled_jobs.iter() { + if let crate::mail::MailJob::Send { + message_pk: _, + recipients, + } = job + { + futures::executor::block_on(conn.mail_transaction( + &String::from_utf8_lossy(&bytes), + Some(recipients), + ))?; + } + } + /* - Save digest metadata in database */ + } + PostAction::Reject { reason: _ } => { + /* - Notify submitter */ + //futures::executor::block_on(conn.mail_transaction(&post.bytes, b)).unwrap(); + } + PostAction::Defer { reason: _ } => { + /* - Notify submitter + * - Save in database */ + } + PostAction::Hold => { /* - Save in database */ } + } + } + _ => {} + } + } + + Ok(()) + } + + pub fn request( + &self, + list: &DbVal, + request: ListRequest, + env: &Envelope, + _raw: &[u8], + ) -> Result<()> { + match request { + ListRequest::Subscribe => { + trace!( + "subscribe action for addresses {:?} in list {}", + env.from(), + list + ); + + let list_policy = self.get_list_policy(list.pk)?; + let approval_needed = list_policy + .as_ref() + .map(|p| p.approval_needed) + .unwrap_or(false); + for f in env.from() { + let membership = ListMembership { + pk: 0, + list: list.pk, + address: f.get_email(), + name: f.get_display_name(), + digest: false, + hide_address: false, + receive_duplicates: true, + receive_own_posts: false, + receive_confirmation: true, + enabled: !approval_needed, + }; + if approval_needed { + match self.add_candidate_member(list.pk, membership) { + Ok(_) => {} + Err(_err) => {} + } + //FIXME: send notification to list-owner + } else if let Err(_err) = self.add_member(list.pk, membership) { + //FIXME: send failure notice to f + } else { + //FIXME: send success notice + } + } + } + ListRequest::Unsubscribe => { + trace!( + "unsubscribe action for addresses {:?} in list {}", + env.from(), + list + ); + for f in env.from() { + if let Err(_err) = self.remove_member(list.pk, &f.get_email()) { + //FIXME: send failure notice to f + } else { + //FIXME: send success notice to f + } + } + } + ListRequest::Other(ref req) if req == "owner" => { + trace!( + "list-owner mail action for addresses {:?} in list {}", + env.from(), + list + ); + //FIXME: mail to list-owner + } + ListRequest::Other(ref req) => { + trace!( + "unknown request action {} for addresses {:?} in list {}", + req, + env.from(), + list + ); + } + } + Ok(()) + } +} diff --git a/core/src/errors.rs b/core/src/errors.rs new file mode 100644 index 0000000..ae61ca5 --- /dev/null +++ b/core/src/errors.rs @@ -0,0 +1,30 @@ +/* + * This file is part of mailpot + * + * Copyright 2020 - Manos Pitsidianakis + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +// Create the Error, ErrorKind, ResultExt, and Result types +error_chain! { + foreign_links { + Sql(rusqlite::Error); + Io(::std::io::Error); + Xdg(xdg::BaseDirectoriesError); + Melib(melib::error::MeliError); + Configuration(toml::de::Error); + SerdeJson(serde_json::Error); + } +} diff --git a/core/src/lib.rs b/core/src/lib.rs new file mode 100644 index 0000000..f997cc3 --- /dev/null +++ b/core/src/lib.rs @@ -0,0 +1,38 @@ +/* + * This file is part of mailpot + * + * Copyright 2020 - Manos Pitsidianakis + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +// `error_chain!` can recurse deeply +#![recursion_limit = "1024"] +//#![warn(missing_docs)] + +use log::{info, trace}; +#[macro_use] +extern crate error_chain; +#[macro_use] +pub extern crate serde; + +pub use melib; +pub use serde_json; + +pub mod config; +pub mod mail; +pub mod models; +use models::*; +pub mod errors; +use errors::*; +pub mod db; diff --git a/core/src/mail.rs b/core/src/mail.rs new file mode 100644 index 0000000..6d8282c --- /dev/null +++ b/core/src/mail.rs @@ -0,0 +1,85 @@ +/* + * This file is part of mailpot + * + * Copyright 2020 - Manos Pitsidianakis + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +use super::*; +use melib::Address; +pub mod message_filters; + +#[derive(Debug)] +pub enum PostAction { + Hold, + Accept, + Reject { reason: String }, + Defer { reason: String }, +} + +#[derive(Debug)] +pub struct ListContext<'list> { + pub list: &'list MailingList, + pub list_owners: Vec>, + pub memberships: &'list [DbVal], + pub policy: Option>, + pub scheduled_jobs: Vec, +} + +///Post to be considered by the list's `PostFilter` stack. +pub struct Post { + pub pk: i64, + pub from: Address, + pub bytes: Vec, + pub to: Vec
, + pub action: PostAction, +} + +impl core::fmt::Debug for Post { + fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result { + fmt.debug_struct("Post") + .field("pk", &self.pk) + .field("from", &self.from) + .field("bytes", &format_args!("{} bytes", self.bytes.len())) + .field("to", &self.to.as_slice()) + .field("action", &self.action) + .finish() + } +} + +#[derive(Debug)] +pub enum MailJob { + Send { + message_pk: i64, + recipients: Vec
, + }, + Relay { + message: Vec, + recipients: Vec
, + }, + Error { + description: String, + }, + StoreDigest { + message_pk: i64, + recipients: Vec
, + }, + ConfirmSubscription { + recipient: Address, + }, + ConfirmUnsubscription { + recipient: Address, + }, +} diff --git a/core/src/mail/message_filters.rs b/core/src/mail/message_filters.rs new file mode 100644 index 0000000..dbe157a --- /dev/null +++ b/core/src/mail/message_filters.rs @@ -0,0 +1,206 @@ +/* + * This file is part of mailpot + * + * Copyright 2020 - Manos Pitsidianakis + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +use super::*; + +///Filter that modifies and/or verifies a post candidate. On rejection, return a string +///describing the error and optionally set `post.action` to `Reject` or `Defer` +pub trait PostFilter { + fn feed<'p, 'list>( + self: Box, + post: &'p mut Post, + ctx: &'p mut ListContext<'list>, + ) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()>; +} + +///Check that submitter can post to list, for now it accepts everything. +pub struct PostRightsCheck; +impl PostFilter for PostRightsCheck { + fn feed<'p, 'list>( + self: Box, + post: &'p mut Post, + ctx: &'p mut ListContext<'list>, + ) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()> { + trace!("Running PostRightsCheck filter"); + if let Some(ref policy) = ctx.policy { + if policy.announce_only { + trace!("post policy is announce_only"); + let owner_addresses = ctx + .list_owners + .iter() + .map(|lo| lo.into_address()) + .collect::>(); + trace!("Owner addresses are: {:#?}", &owner_addresses); + trace!("Envelope from is: {:?}", &post.from); + if !owner_addresses.iter().any(|addr| *addr == post.from) { + trace!("Envelope From does not include any owner"); + post.action = PostAction::Reject { + reason: "You are not allowed to post on this list.".to_string(), + }; + return Err(()); + } + } else if policy.subscriber_only { + trace!("post policy is subscriber_only"); + let email_from = post.from.get_email(); + trace!("post from is {:?}", &email_from); + trace!("post memberships are {:#?}", &ctx.memberships); + if !ctx.memberships.iter().any(|lm| lm.address == email_from) { + trace!("Envelope from is not subscribed to this list"); + post.action = PostAction::Reject { + reason: "Only subscribers can post to this list.".to_string(), + }; + return Err(()); + } + } else if policy.approval_needed { + trace!("post policy says approval_needed"); + post.action = PostAction::Defer { + reason: "Your posting has been deferred. Approval from the list's moderators is required before it is submitted.".to_string(), + }; + } + } + Ok((post, ctx)) + } +} + +///Ensure message contains only `\r\n` line terminators, required by SMTP. +pub struct FixCRLF; +impl PostFilter for FixCRLF { + fn feed<'p, 'list>( + self: Box, + post: &'p mut Post, + ctx: &'p mut ListContext<'list>, + ) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()> { + trace!("Running FixCRLF filter"); + use std::io::prelude::*; + let mut new_vec = Vec::with_capacity(post.bytes.len()); + for line in post.bytes.lines() { + new_vec.extend_from_slice(line.unwrap().as_bytes()); + new_vec.extend_from_slice(b"\r\n"); + } + post.bytes = new_vec; + Ok((post, ctx)) + } +} + +///Add `List-*` headers +pub struct AddListHeaders; +impl PostFilter for AddListHeaders { + fn feed<'p, 'list>( + self: Box, + post: &'p mut Post, + ctx: &'p mut ListContext<'list>, + ) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()> { + trace!("Running AddListHeaders filter"); + let (mut headers, body) = melib::email::parser::mail(&post.bytes).unwrap(); + let list_id = ctx.list.list_id(); + headers.push((&b"List-ID"[..], list_id.as_bytes())); + let list_post = ctx.list.list_post(); + let list_unsubscribe = ctx.list.list_unsubscribe(); + let list_archive = ctx.list.list_archive(); + if let Some(post) = list_post.as_ref() { + headers.push((&b"List-Post"[..], post.as_bytes())); + } + if let Some(unsubscribe) = list_unsubscribe.as_ref() { + headers.push((&b"List-Unsubscribe"[..], unsubscribe.as_bytes())); + } + if let Some(archive) = list_archive.as_ref() { + headers.push((&b"List-Archive"[..], archive.as_bytes())); + } + let mut new_vec = Vec::with_capacity( + headers + .iter() + .map(|(h, v)| h.len() + v.len() + ": \r\n".len()) + .sum::() + + "\r\n\r\n".len() + + body.len(), + ); + for (h, v) in headers { + new_vec.extend_from_slice(h); + new_vec.extend_from_slice(b": "); + new_vec.extend_from_slice(v); + new_vec.extend_from_slice(b"\r\n"); + } + new_vec.extend_from_slice(b"\r\n\r\n"); + new_vec.extend_from_slice(body); + + post.bytes = new_vec; + Ok((post, ctx)) + } +} + +///Adds `Archived-At` field, if configured. +pub struct ArchivedAtLink; +impl PostFilter for ArchivedAtLink { + fn feed<'p, 'list>( + self: Box, + post: &'p mut Post, + ctx: &'p mut ListContext<'list>, + ) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()> { + trace!("Running ArchivedAtLink filter"); + Ok((post, ctx)) + } +} + +///Assuming there are no more changes to be done on the post, it finalizes which list members +///will receive the post in `post.action` field. +pub struct FinalizeRecipients; +impl PostFilter for FinalizeRecipients { + fn feed<'p, 'list>( + self: Box, + post: &'p mut Post, + ctx: &'p mut ListContext<'list>, + ) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()> { + trace!("Running FinalizeRecipients filter"); + let mut recipients = vec![]; + let mut digests = vec![]; + let email_from = post.from.get_email(); + for member in ctx.memberships { + trace!("examining member {:?}", &member); + if member.address != email_from { + trace!("member is submitter"); + } + if member.digest { + if member.address != email_from || member.receive_own_posts { + trace!("Member gets digest"); + digests.push(member.into_address()); + } + continue; + } + if member.address != email_from || member.receive_own_posts { + trace!("Member gets copy"); + recipients.push(member.into_address()); + } + // TODO: + // - check for duplicates (To,Cc,Bcc) + // - send confirmation to submitter + } + ctx.scheduled_jobs.push(MailJob::Send { + message_pk: post.pk, + recipients, + }); + if !digests.is_empty() { + ctx.scheduled_jobs.push(MailJob::StoreDigest { + message_pk: post.pk, + recipients: digests, + }); + } + post.action = PostAction::Accept; + Ok((post, ctx)) + } +} diff --git a/core/src/models.rs b/core/src/models.rs new file mode 100644 index 0000000..330db8f --- /dev/null +++ b/core/src/models.rs @@ -0,0 +1,226 @@ +/* + * This file is part of mailpot + * + * Copyright 2020 - Manos Pitsidianakis + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +use super::*; +pub mod changesets; + +use melib::email::Address; + +pub struct DbVal(pub T, pub i64); + +impl std::ops::Deref for DbVal { + type Target = T; + fn deref(&self) -> &T { + &self.0 + } +} + +impl std::fmt::Display for DbVal +where + T: std::fmt::Display, +{ + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(fmt, "{}", self.0) + } +} + +impl std::fmt::Debug for DbVal +where + T: std::fmt::Debug, +{ + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(fmt, "{:?}", self.0) + } +} + +impl serde::Serialize for DbVal +where + T: serde::Serialize, +{ + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + self.0.serialize(serializer) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct MailingList { + pub pk: i64, + pub name: String, + pub id: String, + pub address: String, + pub description: Option, + pub archive_url: Option, +} + +impl std::fmt::Display for MailingList { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + if let Some(description) = self.description.as_ref() { + write!( + fmt, + "[#{} {}] {} <{}>: {}", + self.pk, self.id, self.name, self.address, description + ) + } else { + write!( + fmt, + "[#{} {}] {} <{}>", + self.pk, self.id, self.name, self.address + ) + } + } +} + +impl MailingList { + pub fn list_id(&self) -> String { + format!("\"{}\" <{}>", self.name, self.address) + } + + pub fn list_post(&self) -> Option { + Some(format!("", self.address)) + } + + pub fn list_unsubscribe(&self) -> Option { + let p = self.address.split('@').collect::>(); + Some(format!( + "", + p[0], p[1] + )) + } + + pub fn list_archive(&self) -> Option { + self.archive_url.as_ref().map(|url| format!("<{}>", url)) + } + + pub fn list_address(&self) -> Address { + Address::new(Some(self.name.clone()), self.address.clone()) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ListMembership { + pub pk: i64, + pub list: i64, + pub address: String, + pub name: Option, + pub digest: bool, + pub hide_address: bool, + pub receive_duplicates: bool, + pub receive_own_posts: bool, + pub receive_confirmation: bool, + pub enabled: bool, +} + +impl std::fmt::Display for ListMembership { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + fmt, + "{} [digest: {}, hide_address: {} {}]", + self.into_address(), + self.digest, + self.hide_address, + if self.enabled { + "enabled" + } else { + "not enabled" + }, + ) + } +} + +impl ListMembership { + pub fn into_address(&self) -> Address { + Address::new(self.name.clone(), self.address.clone()) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct PostPolicy { + pub pk: i64, + pub list: i64, + pub announce_only: bool, + pub subscriber_only: bool, + pub approval_needed: bool, +} + +impl std::fmt::Display for PostPolicy { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(fmt, "{:?}", self) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ListOwner { + pub pk: i64, + pub list: i64, + pub address: String, + pub name: Option, +} + +impl std::fmt::Display for ListOwner { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(fmt, "[#{} {}] {}", self.pk, self.list, self.into_address()) + } +} + +impl From for ListMembership { + fn from(val: ListOwner) -> ListMembership { + ListMembership { + pk: 0, + list: val.list, + address: val.address, + name: val.name, + digest: false, + hide_address: false, + receive_duplicates: true, + receive_own_posts: false, + receive_confirmation: true, + enabled: true, + } + } +} + +impl ListOwner { + pub fn into_address(&self) -> Address { + Address::new(self.name.clone(), self.address.clone()) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub enum ListRequest { + Subscribe, + Unsubscribe, + Other(String), +} + +impl std::fmt::Display for ListRequest { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(fmt, "{:?}", self) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct NewListPost<'s> { + pub list: i64, + pub address: &'s str, + pub message_id: &'s str, + pub message: &'s [u8], +} diff --git a/core/src/models/changesets.rs b/core/src/models/changesets.rs new file mode 100644 index 0000000..af4929e --- /dev/null +++ b/core/src/models/changesets.rs @@ -0,0 +1,80 @@ +/* + * This file is part of mailpot + * + * Copyright 2020 - Manos Pitsidianakis + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct MailingListChangeset { + pub pk: i64, + pub name: Option, + pub id: Option, + pub address: Option, + pub description: Option>, + pub archive_url: Option>, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ListMembershipChangeset { + pub list: i64, + pub address: String, + pub name: Option>, + pub digest: Option, + pub hide_address: Option, + pub receive_duplicates: Option, + pub receive_own_posts: Option, + pub receive_confirmation: Option, + pub enabled: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct PostPolicyChangeset { + pub pk: i64, + pub list: i64, + pub announce_only: Option, + pub subscriber_only: Option, + pub approval_needed: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ListOwnerChangeset { + pub pk: i64, + pub list: i64, + pub address: Option, + pub name: Option>, +} + +impl std::fmt::Display for MailingListChangeset { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(fmt, "{:?}", self) + } +} + +impl std::fmt::Display for ListMembershipChangeset { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(fmt, "{:?}", self) + } +} +impl std::fmt::Display for PostPolicyChangeset { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(fmt, "{:?}", self) + } +} +impl std::fmt::Display for ListOwnerChangeset { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(fmt, "{:?}", self) + } +} diff --git a/core/src/schema.sql b/core/src/schema.sql new file mode 100644 index 0000000..81d200b --- /dev/null +++ b/core/src/schema.sql @@ -0,0 +1,74 @@ +PRAGMA foreign_keys = true; +PRAGMA encoding = 'UTF-8'; + +CREATE TABLE IF NOT EXISTS mailing_lists ( + pk INTEGER PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + id TEXT NOT NULL, + address TEXT NOT NULL, + archive_url TEXT, + description TEXT +); + +CREATE TABLE IF NOT EXISTS list_owner ( + pk INTEGER PRIMARY KEY NOT NULL, + list INTEGER NOT NULL, + address TEXT NOT NULL, + name TEXT, + FOREIGN KEY (list) REFERENCES mailing_lists(pk) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS post_policy ( + pk INTEGER PRIMARY KEY NOT NULL, + list INTEGER NOT NULL UNIQUE, + announce_only BOOLEAN CHECK (announce_only in (0, 1)) NOT NULL DEFAULT 0, + subscriber_only BOOLEAN CHECK (subscriber_only in (0, 1)) NOT NULL DEFAULT 0, + approval_needed BOOLEAN CHECK (approval_needed in (0, 1)) NOT NULL DEFAULT 0, + CHECK(((approval_needed) OR (((announce_only) OR (subscriber_only)) AND NOT ((announce_only) AND (subscriber_only)))) AND NOT ((approval_needed) AND (((announce_only) OR (subscriber_only)) AND NOT ((announce_only) AND (subscriber_only))))), + FOREIGN KEY (list) REFERENCES mailing_lists(pk) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS membership ( + pk INTEGER PRIMARY KEY NOT NULL, + list INTEGER NOT NULL, + address TEXT NOT NULL, + name TEXT, + enabled BOOLEAN CHECK (enabled in (0, 1)) NOT NULL DEFAULT 1, + digest BOOLEAN CHECK (digest in (0, 1)) NOT NULL DEFAULT 0, + hide_address BOOLEAN CHECK (hide_address in (0, 1)) NOT NULL DEFAULT 0, + receive_duplicates BOOLEAN CHECK (receive_duplicates in (0, 1)) NOT NULL DEFAULT 1, + receive_own_posts BOOLEAN CHECK (receive_own_posts in (0, 1)) NOT NULL DEFAULT 0, + receive_confirmation BOOLEAN CHECK (receive_confirmation in (0, 1)) NOT NULL DEFAULT 1, + FOREIGN KEY (list) REFERENCES mailing_lists(pk) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS candidate_membership ( + pk INTEGER PRIMARY KEY NOT NULL, + list INTEGER NOT NULL, + address TEXT NOT NULL, + name TEXT, + accepted INTEGER, + FOREIGN KEY (list) REFERENCES mailing_lists(pk) ON DELETE CASCADE, + FOREIGN KEY (accepted) REFERENCES membership(pk) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS post ( + pk INTEGER PRIMARY KEY NOT NULL, + list INTEGER NOT NULL, + address TEXT NOT NULL, + message_id TEXT NOT NULL, + message BLOB NOT NULL, + FOREIGN KEY (list, address) REFERENCES membership(list, address) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS post_event ( + pk INTEGER PRIMARY KEY NOT NULL, + post INTEGER NOT NULL, + date INTEGER NOT NULL, + kind CHAR(1) CHECK (kind IN ('R', 'S', 'D', 'B', 'O')) NOT NULL, + content TEXT NOT NULL, + FOREIGN KEY (post) REFERENCES post(pk) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS mailing_lists_idx ON mailing_lists(id); +CREATE INDEX IF NOT EXISTS membership_idx ON membership(address); diff --git a/core/src/schema.sql.m4 b/core/src/schema.sql.m4 new file mode 100644 index 0000000..344e8a0 --- /dev/null +++ b/core/src/schema.sql.m4 @@ -0,0 +1,78 @@ +define(xor, `(($1) OR ($2)) AND NOT (($1) AND ($2))')dnl +define(BOOLEAN_TYPE, `$1 BOOLEAN CHECK ($1 in (0, 1)) NOT NULL')dnl +define(BOOLEAN_FALSE, `0')dnl +define(BOOLEAN_TRUE, `1')dnl +PRAGMA foreign_keys = true; +PRAGMA encoding = 'UTF-8'; + +CREATE TABLE IF NOT EXISTS mailing_lists ( + pk INTEGER PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + id TEXT NOT NULL, + address TEXT NOT NULL, + archive_url TEXT, + description TEXT +); + +CREATE TABLE IF NOT EXISTS list_owner ( + pk INTEGER PRIMARY KEY NOT NULL, + list INTEGER NOT NULL, + address TEXT NOT NULL, + name TEXT, + FOREIGN KEY (list) REFERENCES mailing_lists(pk) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS post_policy ( + pk INTEGER PRIMARY KEY NOT NULL, + list INTEGER NOT NULL UNIQUE, + BOOLEAN_TYPE(announce_only) DEFAULT BOOLEAN_FALSE(), + BOOLEAN_TYPE(subscriber_only) DEFAULT BOOLEAN_FALSE(), + BOOLEAN_TYPE(approval_needed) DEFAULT BOOLEAN_FALSE(), + CHECK(xor(approval_needed, xor(announce_only, subscriber_only))), + FOREIGN KEY (list) REFERENCES mailing_lists(pk) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS membership ( + pk INTEGER PRIMARY KEY NOT NULL, + list INTEGER NOT NULL, + address TEXT NOT NULL, + name TEXT, + BOOLEAN_TYPE(enabled) DEFAULT BOOLEAN_TRUE(), + BOOLEAN_TYPE(digest) DEFAULT BOOLEAN_FALSE(), + BOOLEAN_TYPE(hide_address) DEFAULT BOOLEAN_FALSE(), + BOOLEAN_TYPE(receive_duplicates) DEFAULT BOOLEAN_TRUE(), + BOOLEAN_TYPE(receive_own_posts) DEFAULT BOOLEAN_FALSE(), + BOOLEAN_TYPE(receive_confirmation) DEFAULT BOOLEAN_TRUE(), + FOREIGN KEY (list) REFERENCES mailing_lists(pk) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS candidate_membership ( + pk INTEGER PRIMARY KEY NOT NULL, + list INTEGER NOT NULL, + address TEXT NOT NULL, + name TEXT, + accepted INTEGER, + FOREIGN KEY (list) REFERENCES mailing_lists(pk) ON DELETE CASCADE, + FOREIGN KEY (accepted) REFERENCES membership(pk) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS post ( + pk INTEGER PRIMARY KEY NOT NULL, + list INTEGER NOT NULL, + address TEXT NOT NULL, + message_id TEXT NOT NULL, + message BLOB NOT NULL, + FOREIGN KEY (list, address) REFERENCES membership(list, address) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS post_event ( + pk INTEGER PRIMARY KEY NOT NULL, + post INTEGER NOT NULL, + date INTEGER NOT NULL, + kind CHAR(1) CHECK (kind IN ('R', 'S', 'D', 'B', 'O')) NOT NULL, + content TEXT NOT NULL, + FOREIGN KEY (post) REFERENCES post(pk) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS mailing_lists_idx ON mailing_lists(id); +CREATE INDEX IF NOT EXISTS membership_idx ON membership(address); diff --git a/rest-http/Cargo.toml b/rest-http/Cargo.toml new file mode 100644 index 0000000..f29d0ec --- /dev/null +++ b/rest-http/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "mailpot-http" +version = "0.1.0" +authors = ["Manos Pitsidianakis "] +edition = "2018" +license = "LICENSE" +readme = "README.md" +description = "mailing list manager" +repository = "https://github.com/meli/mailpot" +keywords = ["mail", "mailing-lists" ] +categories = ["email"] +default-run = "mpot-http" + +[[bin]] +name = "mpot-http" +path = "src/main.rs" + +[dependencies] +mailpot = { version = "0.1.0", path = "../core" } +tokio = { version = "1", features = ["full"] } +warp = "0.3" diff --git a/rest-http/README.md b/rest-http/README.md new file mode 100644 index 0000000..63a4750 --- /dev/null +++ b/rest-http/README.md @@ -0,0 +1,5 @@ +# mailpot REST http server + +```shell +cargo run --bin mpot-http +``` diff --git a/rest-http/src/main.rs b/rest-http/src/main.rs new file mode 100644 index 0000000..471f0ed --- /dev/null +++ b/rest-http/src/main.rs @@ -0,0 +1,99 @@ +/* + * This file is part of mailpot + * + * Copyright 2020 - Manos Pitsidianakis + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +extern crate mailpot; + +pub use mailpot::config::*; +pub use mailpot::db::*; +pub use mailpot::errors::*; +pub use mailpot::models::*; +pub use mailpot::*; + +use warp::Filter; + +/* +fn json_body() -> impl Filter + Clone { + // When accepting a body, we want a JSON body + // (and to reject huge payloads)... + warp::body::content_length_limit(1024 * 16).and(warp::body::json()) +} +*/ + +#[tokio::main] +async fn main() { + // GET /lists/:i64/policy + let policy = warp::path!("lists" / i64 / "policy").map(|list_pk| { + let db = Database::open_or_create_db().unwrap(); + db.get_list_policy(list_pk) + .ok() + .map(|l| warp::reply::json(&l.unwrap())) + .unwrap() + }); + + //get("/lists")] + let lists = warp::path!("lists").map(|| { + let db = Database::open_or_create_db().unwrap(); + let lists = db.list_lists().unwrap(); + warp::reply::json(&lists) + }); + + //get("/lists/")] + let lists_num = warp::path!("lists" / i64).map(|list_pk| { + let db = Database::open_or_create_db().unwrap(); + let list = db.get_list(list_pk).unwrap(); + warp::reply::json(&list) + }); + + //get("/lists//members")] + let lists_members = warp::path!("lists" / i64 / "members").map(|list_pk| { + let db = Database::open_or_create_db().unwrap(); + db.list_members(list_pk) + .ok() + .map(|l| warp::reply::json(&l)) + .unwrap() + }); + + //get("/lists//owners")] + let lists_owners = warp::path!("lists" / i64 / "owners").map(|list_pk| { + let db = Database::open_or_create_db().unwrap(); + db.get_list_owners(list_pk) + .ok() + .map(|l| warp::reply::json(&l)) + .unwrap() + }); + + //post("/lists//owners/add", data = "")] + let lists_owner_add = + warp::post().and(warp::path!("lists" / i64 / "owners" / "add").map(|_list_pk| "todo")); + + let routes = warp::get().and( + lists + .or(policy) + .or(lists_num) + .or(lists_members) + .or(lists_owners) + .or(lists_owner_add), + ); + + // Note that composing filters for many routes may increase compile times (because it uses a lot of generics). + // If you wish to use dynamic dispatch instead and speed up compile times while + // making it slightly slower at runtime, you can use Filter::boxed(). + + warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; +}