Compare commits
No commits in common. "2a74c6cac4150a49b9434dcfebe6ff53bcc0dd34" and "5e864498d5e400bf38aab160308d701a5ec0ff18" have entirely different histories.
2a74c6cac4
...
5e864498d5
12 changed files with 34 additions and 1140 deletions
|
|
@ -416,13 +416,12 @@ jobs:
|
|||
FLOOR=190
|
||||
out="$(cargo test --locked -p nigig-core --lib -- email_ secret:: mail_backend:: mail_proxy:: imap_client:: credential_store:: 2>&1)"
|
||||
|
||||
# C1e/C1f: the IMAP transport and the platform keystore are both
|
||||
# feature-gated (native only). They must still COMPILE when the
|
||||
# features are on, or the direct backend's read path and credential
|
||||
# persistence silently rot. A check, not a test: the socket and the
|
||||
# secret service are not exercised, only type-checked.
|
||||
- name: The IMAP and keystore features must compile
|
||||
run: cargo check --locked -p nigig-core --features imap,keystore
|
||||
# C1e: the IMAP transport is feature-gated (native only). It must
|
||||
# still COMPILE when the feature is on, or the direct backend's read
|
||||
# path silently rots. A check, not a test: the socket is not
|
||||
# exercised, only type-checked.
|
||||
- name: The IMAP feature must compile
|
||||
run: cargo check --locked -p nigig-core --features imap
|
||||
|
||||
# A coverage number that is only printed drifts down. This enforces a
|
||||
# whole-domain floor plus per-file floors on the files that have
|
||||
|
|
|
|||
459
Cargo.lock
generated
459
Cargo.lock
generated
|
|
@ -141,17 +141,6 @@ dependencies = [
|
|||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "apple-native-keyring-store"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b350bfd03649e07aa05c0a81b3e15934374e585c98204a57e20b9d49f49bb9a"
|
||||
dependencies = [
|
||||
"keyring-core",
|
||||
"log 0.4.33",
|
||||
"security-framework",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.6"
|
||||
|
|
@ -165,18 +154,6 @@ dependencies = [
|
|||
"libloading 0.8.9 (git+https://gitdab.com/andodeki/makepad?rev=ecf5a572ab62a1c1598909971f602f99083671cc)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
|
||||
dependencies = [
|
||||
"event-listener 5.4.2",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "1.9.0"
|
||||
|
|
@ -200,20 +177,6 @@ dependencies = [
|
|||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-executor"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
|
||||
dependencies = [
|
||||
"async-task",
|
||||
"concurrent-queue",
|
||||
"fastrand",
|
||||
"futures-lite",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-imap"
|
||||
version = "0.9.7"
|
||||
|
|
@ -306,17 +269,6 @@ dependencies = [
|
|||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-recursion"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-signal"
|
||||
version = "0.2.14"
|
||||
|
|
@ -888,7 +840,6 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
|||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -982,33 +933,6 @@ dependencies = [
|
|||
"cfg-if 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "endi"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2"
|
||||
version = "0.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
|
||||
dependencies = [
|
||||
"enumflags2_derive",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2_derive"
|
||||
version = "0.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
|
|
@ -1369,12 +1293,6 @@ dependencies = [
|
|||
"foldhash 0.2.0 (git+https://gitdab.com/andodeki/makepad?rev=ecf5a572ab62a1c1598909971f602f99083671cc)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.9.1"
|
||||
|
|
@ -1410,12 +1328,6 @@ version = "0.5.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hexf-parse"
|
||||
version = "0.2.1"
|
||||
|
|
@ -1427,24 +1339,6 @@ version = "0.1.8"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f715f263b6e73aa2322b59057134b4daf56a765cbebc2f6b35e640ffded3361b"
|
||||
|
||||
[[package]]
|
||||
name = "hkdf"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
|
||||
dependencies = [
|
||||
"hmac",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||
dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hostname"
|
||||
version = "0.4.2"
|
||||
|
|
@ -1819,16 +1713,6 @@ dependencies = [
|
|||
"hashbrown 0.16.1 (git+https://gitdab.com/andodeki/makepad?rev=ecf5a572ab62a1c1598909971f602f99083671cc)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"hashbrown 0.17.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
|
|
@ -1912,27 +1796,6 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keyring"
|
||||
version = "4.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72585bb6cc9bc370d1d545b7e23fcce71dfd4461c5e15275e3cf51bdfd9a980a"
|
||||
dependencies = [
|
||||
"apple-native-keyring-store",
|
||||
"keyring-core",
|
||||
"windows-native-keyring-store",
|
||||
"zbus-secret-service-keyring-store",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keyring-core"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb1e621458ca9c51aa110bd0339d4751a056b9576bf1253aee1aa560dda0fc9d"
|
||||
dependencies = [
|
||||
"log 0.4.33",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lettre"
|
||||
version = "0.11.23"
|
||||
|
|
@ -2722,15 +2585,6 @@ version = "2.8.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
|
|
@ -2805,7 +2659,7 @@ dependencies = [
|
|||
"cfg_aliases 0.2.1",
|
||||
"hashbrown 0.16.1 (git+https://gitdab.com/andodeki/makepad?rev=ecf5a572ab62a1c1598909971f602f99083671cc)",
|
||||
"hexf-parse",
|
||||
"indexmap 2.13.0",
|
||||
"indexmap",
|
||||
"makepad-error-log",
|
||||
"makepad-half",
|
||||
"num-traits 0.2.20",
|
||||
|
|
@ -2978,7 +2832,6 @@ dependencies = [
|
|||
"getrandom 0.2.17",
|
||||
"image",
|
||||
"js-sys",
|
||||
"keyring",
|
||||
"lettre",
|
||||
"makepad-widgets",
|
||||
"matrix_client",
|
||||
|
|
@ -3393,75 +3246,12 @@ dependencies = [
|
|||
"memchr 2.8.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-complex",
|
||||
"num-integer",
|
||||
"num-iter",
|
||||
"num-rational",
|
||||
"num-traits 0.2.19",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits 0.2.19",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-complex"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
|
||||
dependencies = [
|
||||
"num-traits 0.2.19",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
|
||||
dependencies = [
|
||||
"num-traits 0.2.19",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-iter"
|
||||
version = "0.1.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits 0.2.19",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-integer",
|
||||
"num-traits 0.2.19",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
|
|
@ -3642,16 +3432,6 @@ version = "0.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-stream"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "owned_ttf_parser"
|
||||
version = "0.19.0"
|
||||
|
|
@ -3874,15 +3654,6 @@ dependencies = [
|
|||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||
dependencies = [
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
|
|
@ -4582,25 +4353,6 @@ name = "sdfer"
|
|||
version = "0.2.1"
|
||||
source = "git+https://gitdab.com/andodeki/makepad?rev=ecf5a572ab62a1c1598909971f602f99083671cc#ecf5a572ab62a1c1598909971f602f99083671cc"
|
||||
|
||||
[[package]]
|
||||
name = "secret-service"
|
||||
version = "5.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a62d7f86047af0077255a29494136b9aaaf697c76ff70b8e49cded4e2623c14"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"cbc",
|
||||
"futures-util",
|
||||
"generic-array",
|
||||
"getrandom 0.2.17",
|
||||
"hkdf",
|
||||
"num",
|
||||
"once_cell 1.21.4",
|
||||
"serde",
|
||||
"sha2",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.7.0"
|
||||
|
|
@ -4679,17 +4431,6 @@ dependencies = [
|
|||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_repr"
|
||||
version = "0.1.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
|
|
@ -5058,36 +4799,6 @@ dependencies = [
|
|||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.25.13+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b"
|
||||
dependencies = [
|
||||
"indexmap 2.14.0",
|
||||
"toml_datetime",
|
||||
"toml_parser",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.1.3+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.5.3"
|
||||
|
|
@ -5140,21 +4851,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.36"
|
||||
|
|
@ -5187,17 +4886,6 @@ version = "1.20.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "uds_windows"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"tempfile",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unarray"
|
||||
version = "0.1.4"
|
||||
|
|
@ -5300,7 +4988,6 @@ checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9"
|
|||
dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"js-sys",
|
||||
"serde_core",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
|
|
@ -5802,19 +5489,6 @@ name = "windows-link"
|
|||
version = "0.2.1"
|
||||
source = "git+https://gitdab.com/andodeki/makepad?rev=ecf5a572ab62a1c1598909971f602f99083671cc#ecf5a572ab62a1c1598909971f602f99083671cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows-native-keyring-store"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "063426e76fdec7438d56bb777f67e318a84a25c707b07e575cb8b78e10c028f8"
|
||||
dependencies = [
|
||||
"byteorder 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"keyring-core",
|
||||
"regex",
|
||||
"windows-sys 0.61.2",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.1.2"
|
||||
|
|
@ -6006,15 +5680,6 @@ version = "0.52.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
|
||||
dependencies = [
|
||||
"memchr 2.8.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.57.1"
|
||||
|
|
@ -6050,87 +5715,6 @@ dependencies = [
|
|||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus"
|
||||
version = "5.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907"
|
||||
dependencies = [
|
||||
"async-broadcast",
|
||||
"async-executor",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"async-process",
|
||||
"async-recursion",
|
||||
"async-task",
|
||||
"async-trait",
|
||||
"blocking",
|
||||
"enumflags2",
|
||||
"event-listener 5.4.2",
|
||||
"futures-core",
|
||||
"futures-lite",
|
||||
"hex",
|
||||
"libc",
|
||||
"ordered-stream",
|
||||
"rustix",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"tracing",
|
||||
"uds_windows",
|
||||
"uuid",
|
||||
"windows-sys 0.61.2",
|
||||
"winnow",
|
||||
"zbus_macros",
|
||||
"zbus_names",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus-secret-service-keyring-store"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74801d001b9e7729adb4f1825b67b398185fed424749aa3d8bacf70417137d9a"
|
||||
dependencies = [
|
||||
"keyring-core",
|
||||
"secret-service",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_macros"
|
||||
version = "5.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.3",
|
||||
"zbus_names",
|
||||
"zvariant",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_names"
|
||||
version = "4.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"winnow",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zcheapstr"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.35"
|
||||
|
|
@ -6242,44 +5826,3 @@ name = "zmij"
|
|||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
|
||||
[[package]]
|
||||
name = "zvariant"
|
||||
version = "5.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5e28c25bd8bb8da5a1f3e7065d0c156b9ee9a7973adf78b0e35eaefdf3b1b5c"
|
||||
dependencies = [
|
||||
"endi",
|
||||
"enumflags2",
|
||||
"serde",
|
||||
"winnow",
|
||||
"zcheapstr",
|
||||
"zvariant_derive",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_derive"
|
||||
version = "5.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d496a145685283b67e232bd9e47377f6b60ad9d51e3601b23867f77c42477f96"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.3",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_utils"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b84ebb462416c27cdb97f2e7f5f0ccc844da1fe2ecc7121e1b690b41318bf42"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"syn 3.0.3",
|
||||
"winnow",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -531,7 +531,6 @@ snapshot. Commits are on `main`.
|
|||
| *(this turn)* | **Phase A complete** — `Secret` newtype; `SmtpConfig` no longer derives `Serialize`; `relay()`; explicit TLS policy; local validation; HTTPS-only proxy endpoint; email THREAT_MODEL. Domain tests **38 → 68**. |
|
||||
| *(this turn)* | **C1d + C1c DONE** — `mail_proxy.rs` (the proxy HTTP client: request/response parsing, error mapping, a transport trait with reqwest/fetch/mock impls); `SetupDraft` (identity + backend, validated together); the backend chooser and two setup forms in `EmailAccountSetup`; the inbox branches to `spawn_proxy_verify` for the proxy backend. Domain tests **126 → 154**. Coverage tooling (`tools/test-email-coverage.sh`, 93.4% line, floors enforced) wired into `email.yml`. |
|
||||
| *(this turn)* | **Phase C + D COMPLETE** — C1e (`imap_client.rs`: trait + pure INTERNALDATE parser + feature-gated async-imap transport, native only); C1f (`credential_store.rs`: trait + fail-closed default); C3 (`email_cache.rs`: bodies through a `BodyCipher` before disk); C4b (inbox fetches real mail, loading/error/empty states); C5 (Compose sends via `spawn_send_message`); C6 (`email_pacing.rs`: `SendRateLimiter`+`SendPacing`, 100/hour); C7 (Refresh button re-fetches); D1 (More page is real: account + sign-out); D2 (lib.rs shims deleted, `NavigationBarAction` in a real module); D3 (`CachedWidget` decision documented); D4 (one-slot SMTP transport pool, keying tested); D5 (`EmailWorkerAction::None` dropped). Domain tests **154 → 195**. Coverage now **90.7%** over 12 files, floors enforced. |
|
||||
| *(this turn)* | **C6/C7/C1f gaps closed** — C6: `email_bulk.rs` actually *uses* the pacing — `bulk_send_plan` batches a list over the 100-recipient cap and `run_bulk_send` sends the batches paced (gap + rate-limiter + abandon), wired into the Bulk page. C7: pull-to-refresh on the inbox (the SMS/M-Pesa `scrolled`+`scroll_position` pattern) in addition to the button. C1f: a real `KeyringCredentialStore` (OS Secret Service / Credential Manager / Keychain via `keyring`) behind the `keystore` feature; the fail-closed default remains when the feature is off. Domain tests **195 → 206**. |
|
||||
|
||||
**Phase 0 is complete.** All seven items done; 0.7 was fixed upstream.
|
||||
|
||||
|
|
@ -701,8 +700,8 @@ send-only form. **It needs a product decision before any code.**
|
|||
| ~~C4a~~ | ~~**Inbox list + thread reader.**~~ **DONE** (`18bbb7b`) — sender list, thread push/pop, unread handling, signed-out gate. |
|
||||
| ~~C4b~~ | ~~**Wire it to real data.**~~ **DONE** — `sample_thread()` is gone from the UI (test-only now, gated in CI); the inbox fetches via `spawn_fetch_inbox` (backend-agnostic) and renders loading / error / empty states. |
|
||||
| ~~C5~~ | ~~**Give Compose a send button.**~~ **DONE** — `compose.rs` is a real form: to/subject/body, `EmailSendRequest::build_without_config` validation, two-tap confirm, and `spawn_send_message` (branches on the signed-in backend). |
|
||||
| ~~C6~~ | ~~**Bulk pacing.**~~ **DONE** — `email_pacing.rs` ported the primitives; `email_bulk.rs` now *uses* them: `bulk_send_plan` batches a list over the 100-recipient cap and `run_bulk_send` sends the batches paced (gap + rate-limiter backstop + abandon), wired into the Bulk page. The Bulk tab finally sends >100 recipients. |
|
||||
| ~~C7~~ | ~~**Pull-to-refresh + background fetch.**~~ **DONE** — a Refresh button re-fetches AND a pull-to-refresh gesture on the inbox (the SMS/M-Pesa `scrolled` + `scroll_position` pattern, throttled and guarded). The fetch is worker → `InboxFetched` action → drained on the UI thread. |
|
||||
| ~~C6~~ | ~~**Bulk pacing.**~~ **DONE** — `email_pacing.rs`: `SendRateLimiter` + `SendPacing` ported with email-shaped limits (100/hour). Primitives are tested; no per-recipient loop exists to wire them into yet (the bulk path is one message to N recipients). |
|
||||
| ~~C7~~ | ~~**Pull-to-refresh + background fetch.**~~ **DONE** — a Refresh button re-fetches; the fetch is worker → `InboxFetched` action → drained on the UI thread (the `Cx::post_action` shape, equivalent to SMS's `SignalToUI`). A gesture-based pull is deferred: Makepad has no pull-to-refresh primitive and the button is the cross-platform control. |
|
||||
|
||||
#### C1 — DECIDED: support both, user-selectable
|
||||
|
||||
|
|
@ -772,7 +771,7 @@ extends cleanly.
|
|||
| ~~C1c~~ | **DONE** — backend chooser + two forms in `EmailAccountSetup`, driven by `SetupDraft` (identity + backend validated together); the inbox branches to the proxy verify. |
|
||||
| ~~C1d~~ | **DONE** — `mail_proxy.rs`: `ProxyApiClient` with `verify`/`list_inbox`/`send` over a `ProxyTransport` trait (reqwest/fetch/mock); request/response parsing and error mapping all host-tested. |
|
||||
| ~~C1e~~ | ~~`ImapSmtpBackend`.~~ **DONE** — `imap_client.rs`: `ImapTransport` trait, `ImapClient` (`verify` + `list_inbox`), a pure `INTERNALDATE` parser and envelope mapping (tested), and `AsyncImapTransport` over `async-imap` behind the `imap` feature (native only; a CI step checks it compiles). |
|
||||
| ~~C1f~~ | ~~Keystore-backed credential storage.~~ **DONE** — `credential_store.rs`: the `CredentialStore` trait, a fail-closed default, AND a real `KeyringCredentialStore` (OS Secret Service / Credential Manager / Keychain via `keyring`) behind the `keystore` feature. The runtime vault is not host-verified (no secret service in CI), but the contract is pinned by tests and the code compiles + fails closed when the vault is absent. |
|
||||
| ~~C1f~~ | ~~Keystore-backed credential storage.~~ **DONE (the seam)** — `credential_store.rs`: the `CredentialStore` trait and a fail-closed default. The actual `AndroidKeyStore` wiring remains the platform task it always was (not host-testable); the contract is pinned by tests so a platform impl has something to match. |
|
||||
|
||||
##### One thing I will not pretend
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
use makepad_widgets::*;
|
||||
use nigig_core::email_account::AccountDraft;
|
||||
use nigig_core::email_send::{EmailSendRequest, MAX_RECIPIENTS};
|
||||
use nigig_core::email_worker::{
|
||||
spawn_bulk_send, spawn_send_email, spawn_smtp_test, EmailWorkerAction, SmtpConfig,
|
||||
};
|
||||
use nigig_core::email_send::EmailSendRequest;
|
||||
use nigig_core::email_worker::{spawn_send_email, spawn_smtp_test, EmailWorkerAction, SmtpConfig};
|
||||
|
||||
script_mod! {
|
||||
use mod.prelude.widgets.*
|
||||
|
|
@ -160,22 +158,22 @@ impl Widget for EmailBulkPage {
|
|||
let message = self.text_input(cx, ids!(message_input)).text();
|
||||
let config = self.smtp_config.clone();
|
||||
|
||||
// B1/B2 + C6: parse the recipient list and validate the
|
||||
// message. `validate_bulk_message` accepts a list over the
|
||||
// per-message cap (so the Bulk tab can actually bulk), and
|
||||
// the send path batches it below.
|
||||
match EmailSendRequest::validate_bulk_message(&to, &subject, &message) {
|
||||
// B1/B2: build through EmailSendRequest so the recipient
|
||||
// list is actually parsed. This field is labelled
|
||||
// "To (comma-separated)" and used to be handed to a single
|
||||
// Mailbox parse, so any list failed outright.
|
||||
match EmailSendRequest::build(&config, &to, &subject, &message) {
|
||||
Err(e) => {
|
||||
self.label(cx, ids!(send_status)).set_text(cx, &e.message());
|
||||
self.pending_send = None;
|
||||
self.view.redraw(cx);
|
||||
}
|
||||
Ok((recipients, list)) => {
|
||||
Ok((req, list)) => {
|
||||
// B5: require a second tap before spending money.
|
||||
// Email is billed by nobody but it is irreversible
|
||||
// and goes to real people; the SMS crate requires
|
||||
// the same confirmation for the same reason.
|
||||
let n = recipients.len();
|
||||
let n = req.recipient_count();
|
||||
let armed = self
|
||||
.pending_send
|
||||
.as_ref()
|
||||
|
|
@ -189,12 +187,6 @@ impl Widget for EmailBulkPage {
|
|||
n,
|
||||
if n == 1 { "" } else { "s" }
|
||||
);
|
||||
if n > MAX_RECIPIENTS {
|
||||
let batches = n.div_ceil(MAX_RECIPIENTS);
|
||||
prompt.push_str(&format!(
|
||||
" This will be sent in {batches} paced batches."
|
||||
));
|
||||
}
|
||||
if list.duplicates > 0 || !list.rejected.is_empty() {
|
||||
prompt.push_str(&format!(" ({})", list.summary()));
|
||||
}
|
||||
|
|
@ -202,20 +194,9 @@ impl Widget for EmailBulkPage {
|
|||
self.view.redraw(cx);
|
||||
} else {
|
||||
self.pending_send = None;
|
||||
if n <= MAX_RECIPIENTS {
|
||||
self.label(cx, ids!(send_status))
|
||||
.set_text(cx, &format!("Sending to {n} recipient(s)…"));
|
||||
spawn_send_email(config, to, subject, message);
|
||||
} else {
|
||||
self.label(cx, ids!(send_status)).set_text(
|
||||
cx,
|
||||
&format!(
|
||||
"Sending to {n} recipients in {} paced batches…",
|
||||
n.div_ceil(MAX_RECIPIENTS)
|
||||
),
|
||||
);
|
||||
spawn_bulk_send(config, recipients, subject, message);
|
||||
}
|
||||
self.set_send_button_label(cx, true);
|
||||
self.view.redraw(cx);
|
||||
}
|
||||
|
|
@ -240,27 +221,6 @@ impl Widget for EmailBulkPage {
|
|||
};
|
||||
self.label(cx, ids!(send_status)).set_text(cx, &msg);
|
||||
}
|
||||
// C6: paced bulk progress, after each batch and once at
|
||||
// the end.
|
||||
if let Some(EmailWorkerAction::BulkSendProgress(p)) = action.downcast_ref() {
|
||||
if p.done {
|
||||
self.set_send_button_label(cx, false);
|
||||
let msg = format!(
|
||||
"Bulk send finished: {} of {} batches sent, {} failed.",
|
||||
p.sent_batches, p.total_batches, p.failed_batches
|
||||
);
|
||||
self.label(cx, ids!(send_status)).set_text(cx, &msg);
|
||||
} else {
|
||||
self.label(cx, ids!(send_status)).set_text(
|
||||
cx,
|
||||
&format!(
|
||||
"Sending batch {}/{}…",
|
||||
p.sent_batches + p.failed_batches,
|
||||
p.total_batches
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -186,9 +186,6 @@ pub struct EmailInboxPage {
|
|||
/// C4b: the last fetch failed; the message is shown in the status view.
|
||||
#[rust]
|
||||
load_error: Option<String>,
|
||||
/// C7: last time a pull-to-refresh fired, for the throttle.
|
||||
#[rust]
|
||||
last_refresh_secs: f64,
|
||||
|
||||
#[rust]
|
||||
current_thread_view: Option<LiveId>,
|
||||
|
|
@ -241,10 +238,6 @@ impl Widget for EmailInboxPage {
|
|||
if self.button(cx, ids!(refresh_btn)).clicked(actions) {
|
||||
self.begin_fetch(cx);
|
||||
}
|
||||
// C7: pull-to-refresh -- the same pattern the SMS and M-Pesa
|
||||
// transaction lists use (`scrolled` + scroll position over a
|
||||
// threshold, throttled and guarded by the in-flight flag).
|
||||
self.handle_portal_list_refresh_actions(cx, actions);
|
||||
}
|
||||
|
||||
// Connection test result (SMTP or proxy) decides signed-in vs
|
||||
|
|
@ -485,26 +478,6 @@ impl EmailInboxPage {
|
|||
self.view.redraw(cx);
|
||||
}
|
||||
|
||||
/// C7: pull-to-refresh on the inbox list.
|
||||
///
|
||||
/// Mirrors the SMS conversations list: a scroll over the threshold
|
||||
/// while at rest triggers a refresh, throttled to once per 1.2s and
|
||||
/// suppressed while a fetch is already in flight.
|
||||
fn handle_portal_list_refresh_actions(&mut self, cx: &mut Cx, actions: &Actions) {
|
||||
let list = self.view.portal_list(cx, ids!(threads_list));
|
||||
let now = cx.seconds_since_app_start();
|
||||
let pull_distance = list.scroll_position();
|
||||
if list.scrolled(actions)
|
||||
&& pull_distance > 55.0
|
||||
&& !self.loading
|
||||
&& self.session.is_signed_in()
|
||||
&& now - self.last_refresh_secs > 1.2
|
||||
{
|
||||
self.last_refresh_secs = now;
|
||||
self.begin_fetch(cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the two labels of the no-mail status view.
|
||||
fn set_status(&mut self, cx: &mut Cx, title: &str, subtitle: &str) {
|
||||
self.view.label(cx, ids!(status_title)).set_text(cx, title);
|
||||
|
|
|
|||
|
|
@ -12,11 +12,6 @@ async-rt = []
|
|||
# TLS). Native only -- a browser cannot open a raw TCP socket -- and off by
|
||||
# default so builds that do not need IMAP never compile its dependency tree.
|
||||
imap = ["dep:async-imap", "dep:async-native-tls", "dep:async-net"]
|
||||
# C1f: a real keystore-backed CredentialStore (the `keyring` crate: Linux
|
||||
# Secret Service, Windows Credential Manager, macOS Keychain). Native only,
|
||||
# off by default so wasm and keystore-free builds never pull its dependency
|
||||
# tree. Without it, active_store() is the fail-closed UnavailableCredentialStore.
|
||||
keystore = ["dep:keyring"]
|
||||
|
||||
[dependencies]
|
||||
matrix_client = { path = "../matrix_client", default-features = false }
|
||||
|
|
@ -56,8 +51,6 @@ lettre = { version = "0.11", default-features = false, features = [
|
|||
async-imap = { version = "0.9", optional = true }
|
||||
async-native-tls = { version = "0.5", optional = true }
|
||||
async-net = { version = "2", optional = true }
|
||||
# C1f: a real platform keystore (feature `keystore`), native only.
|
||||
keyring = { version = "4", optional = true }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
wasm-bindgen = "0.2"
|
||||
|
|
|
|||
|
|
@ -70,73 +70,17 @@ impl CredentialStore for UnavailableCredentialStore {
|
|||
|
||||
/// The credential store in use on this build.
|
||||
///
|
||||
/// With the `keystore` feature enabled (native only), this is a real
|
||||
/// platform keystore -- the OS Secret Service / Credential Manager /
|
||||
/// Keychain via the `keyring` crate -- so IMAP credentials survive an app
|
||||
/// restart. Without it, this is the fail-closed `UnavailableCredentialStore`
|
||||
/// and credentials are session-only.
|
||||
/// This is the single place a platform swaps in its keystore-backed
|
||||
/// implementation. On a host (and on any platform that has not wired a
|
||||
/// keystore yet) it is `UnavailableCredentialStore`, and the behaviour
|
||||
/// documented above applies.
|
||||
pub fn active_store() -> &'static dyn CredentialStore {
|
||||
#[cfg(feature = "keystore")]
|
||||
{
|
||||
static KEYRING: KeyringCredentialStore =
|
||||
KeyringCredentialStore::new("nigig-email");
|
||||
&KEYRING
|
||||
}
|
||||
#[cfg(not(feature = "keystore"))]
|
||||
{
|
||||
// Kept behind an indirection so a platform impl can be selected by
|
||||
// cfg without changing callers. There is none yet; see the module
|
||||
// note -- the Android keystore is the C1f platform task and is not
|
||||
// host-testable.
|
||||
static DEFAULT: UnavailableCredentialStore = UnavailableCredentialStore;
|
||||
&DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
/// A real keystore, backed by the OS credential vault (C1f).
|
||||
///
|
||||
/// `keyring` stores a credential under a (service, account) pair in the
|
||||
/// platform's native vault: the Linux Secret Service (GNOME Keyring /
|
||||
/// KWallet), Windows Credential Manager, or the macOS Keychain. The secret
|
||||
/// never touches app storage; the OS encrypts and gates it.
|
||||
///
|
||||
/// This is the implementation the plan's C1f pointed at. It is native-only
|
||||
/// and gated behind the `keystore` feature. It is NOT host-tested: whether
|
||||
/// a credential round-trips depends on the runtime environment's secret
|
||||
/// service, which a CI container does not provide -- the same honest caveat
|
||||
/// as the IMAP transport (C1e). A build without a secret service fails
|
||||
/// closed (`store` returns `Err`), which is exactly the behaviour the seam
|
||||
/// is meant to have.
|
||||
#[cfg(feature = "keystore")]
|
||||
pub struct KeyringCredentialStore {
|
||||
service: &'static str,
|
||||
}
|
||||
|
||||
#[cfg(feature = "keystore")]
|
||||
impl KeyringCredentialStore {
|
||||
pub const fn new(service: &'static str) -> Self {
|
||||
Self { service }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "keystore")]
|
||||
impl CredentialStore for KeyringCredentialStore {
|
||||
fn store(&self, account_id: &str, secret: &Secret) -> Result<(), String> {
|
||||
let entry = keyring::Entry::new(self.service, account_id).map_err(|e| e.to_string())?;
|
||||
entry
|
||||
.set_password(secret.expose())
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn load(&self, account_id: &str) -> Result<Option<Secret>, String> {
|
||||
let entry = keyring::Entry::new(self.service, account_id).map_err(|e| e.to_string())?;
|
||||
match entry.get_password() {
|
||||
Ok(p) => Ok(Some(Secret::new(p))),
|
||||
Err(keyring::Error::NoEntry) => Ok(None),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn delete(&self, account_id: &str) -> Result<(), String> {
|
||||
let entry = keyring::Entry::new(self.service, account_id).map_err(|e| e.to_string())?;
|
||||
entry.delete_credential().map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -1,376 +0,0 @@
|
|||
//! Paced bulk sending (Phase C6, completed).
|
||||
//!
|
||||
//! C6 ported `SendRateLimiter`/`SendPacing` from `robius-sms`, but the
|
||||
//! honest gap was: **no loop used them**. The Bulk tab sent ONE message to
|
||||
//! N recipients and hit `MAX_RECIPIENTS` (100) hard -- a 500-recipient
|
||||
//! list was refused, not paced. That is not bulk; it is a capped single
|
||||
//! send.
|
||||
//!
|
||||
//! This module closes the gap. It splits a large recipient list into
|
||||
//! provider-sized batches and schedules them with a gap, so the bulk path
|
||||
//! both *fits* under the per-message cap and *paces* itself under the
|
||||
//! relay's rate limit. The schedule is a pure function over plain data --
|
||||
//! fully host-testable -- and the worker that executes it is a thin loop.
|
||||
//!
|
||||
//! ## Why batches, not one message per recipient
|
||||
//!
|
||||
//! SMTP accepts up to ~100 `RCPT TO` per message, so sending N messages to
|
||||
//! reach N recipients is N times the TCP+TLS+AUTH work for no benefit.
|
||||
//! Batching is the correct shape: each batch is one SMTP transaction with
|
||||
//! up to `batch_size` recipients, and the gap is between *batches*.
|
||||
|
||||
use crate::email_pacing::{SendPacing, SendRateLimiter};
|
||||
use crate::email_send::MAX_RECIPIENTS;
|
||||
|
||||
/// One batch of a bulk send, with the delay that precedes it.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BulkBatch {
|
||||
/// Recipient mailbox strings, already parsed/validated by the caller.
|
||||
pub recipients: Vec<String>,
|
||||
/// Milliseconds to wait before sending this batch. The first batch is
|
||||
/// always 0; subsequent batches accumulate `SendPacing::delay_ms()`.
|
||||
pub delay_before_ms: i64,
|
||||
}
|
||||
|
||||
/// The default batch size: the same per-message recipient cap the send
|
||||
/// path enforces, so a batch never trips `TooManyRecipients`.
|
||||
pub fn default_batch_size() -> usize {
|
||||
MAX_RECIPIENTS
|
||||
}
|
||||
|
||||
/// Split `recipients` into provider-sized batches with a pacing schedule.
|
||||
///
|
||||
/// Pure and deterministic: `chunks` preserves input order, and the delay
|
||||
/// for batch *i* is `i * pacing.delay_ms()`. `batch_size == 0` or an empty
|
||||
/// list yields an empty plan (nothing to send, no panic).
|
||||
pub fn bulk_send_plan(
|
||||
recipients: &[String],
|
||||
batch_size: usize,
|
||||
pacing: SendPacing,
|
||||
) -> Vec<BulkBatch> {
|
||||
if batch_size == 0 || recipients.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
recipients
|
||||
.chunks(batch_size)
|
||||
.enumerate()
|
||||
.map(|(i, chunk)| BulkBatch {
|
||||
recipients: chunk.to_vec(),
|
||||
delay_before_ms: pacing.delay_ms().saturating_mul(i as i64),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Wall-clock epoch milliseconds, for the rate-limiter backstop.
|
||||
fn now_ms() -> i64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as i64)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Progress of a bulk send, posted to the UI after each batch.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BulkProgress {
|
||||
pub sent_batches: usize,
|
||||
pub total_batches: usize,
|
||||
pub failed_batches: usize,
|
||||
pub sent_recipients: usize,
|
||||
pub total_recipients: usize,
|
||||
/// True only on the final update.
|
||||
pub done: bool,
|
||||
}
|
||||
|
||||
impl BulkProgress {
|
||||
pub fn is_complete(&self) -> bool {
|
||||
self.done
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a bulk plan, sending each batch in turn.
|
||||
///
|
||||
/// The loop that C6 exists for: pace between batches (`SendPacing`), and
|
||||
/// hold the batch under the rate-limit ceiling (`SendRateLimiter`) rather
|
||||
/// than abandoning the list when the cap is reached. `on_progress` is
|
||||
/// called after every batch and once at the end (`done`), so the caller
|
||||
/// decides how to surface it (post an action, update a label, …).
|
||||
///
|
||||
/// `send_batch` receives the joined recipient string and returns Ok on a
|
||||
/// successful SMTP transaction. The abandon flag is checked between every
|
||||
/// step so a stop is responsive.
|
||||
pub async fn run_bulk_send<F, Fut>(
|
||||
plan: Vec<BulkBatch>,
|
||||
pacing: SendPacing,
|
||||
abandon: &std::sync::atomic::AtomicBool,
|
||||
mut on_progress: impl FnMut(&BulkProgress),
|
||||
mut send_batch: F,
|
||||
) -> BulkProgress
|
||||
where
|
||||
F: FnMut(&str) -> Fut,
|
||||
Fut: std::future::Future<Output = Result<(), String>>,
|
||||
{
|
||||
let total_batches = plan.len();
|
||||
let total_recipients = plan.iter().map(|b| b.recipients.len()).sum();
|
||||
let mut limiter = SendRateLimiter::default();
|
||||
let mut sent = 0usize;
|
||||
let mut failed = 0usize;
|
||||
let mut sent_recipients = 0usize;
|
||||
|
||||
for batch in plan {
|
||||
// Pace before every batch except the first.
|
||||
if batch.delay_before_ms > 0 {
|
||||
let mut remaining = batch.delay_before_ms;
|
||||
while remaining > 0 {
|
||||
if abandon.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
let step = remaining.min(200);
|
||||
crate::platform::sleep(std::time::Duration::from_millis(step as u64)).await;
|
||||
remaining -= step;
|
||||
}
|
||||
}
|
||||
if abandon.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
// The rate limiter is the backstop: with a sane pacing gap it
|
||||
// never fires; if the user forced delay=0 it waits out the ceiling
|
||||
// instead of abandoning the list.
|
||||
loop {
|
||||
match limiter.allow_at(now_ms()) {
|
||||
Ok(()) => break,
|
||||
Err(wait_ms) => {
|
||||
if abandon.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
let nap = wait_ms.clamp(50, 1000) as u64;
|
||||
crate::platform::sleep(std::time::Duration::from_millis(nap)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
if abandon.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
match send_batch(&batch.recipients.join(", ")).await {
|
||||
Ok(()) => {
|
||||
sent += 1;
|
||||
sent_recipients += batch.recipients.len();
|
||||
}
|
||||
Err(_) => {
|
||||
failed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let progress = BulkProgress {
|
||||
sent_batches: sent,
|
||||
total_batches,
|
||||
failed_batches: failed,
|
||||
sent_recipients,
|
||||
total_recipients,
|
||||
done: false,
|
||||
};
|
||||
on_progress(&progress);
|
||||
}
|
||||
|
||||
let final_progress = BulkProgress {
|
||||
sent_batches: sent,
|
||||
total_batches,
|
||||
failed_batches: failed,
|
||||
sent_recipients,
|
||||
total_recipients,
|
||||
done: true,
|
||||
};
|
||||
on_progress(&final_progress);
|
||||
// `pacing` is consumed by the plan already; keep the parameter for the
|
||||
// signature's symmetry with the planner and future rate-limiter tuning.
|
||||
let _ = pacing;
|
||||
final_progress
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn addrs(n: usize) -> Vec<String> {
|
||||
(0..n).map(|i| format!("u{i}@example.com")).collect()
|
||||
}
|
||||
|
||||
// ---- the plan ------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn a_small_list_is_one_batch_with_no_delay() {
|
||||
let plan = bulk_send_plan(&addrs(10), 100, SendPacing::from_seconds(10));
|
||||
assert_eq!(plan.len(), 1);
|
||||
assert_eq!(plan[0].recipients.len(), 10);
|
||||
assert_eq!(plan[0].delay_before_ms, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_large_list_is_split_into_batches_in_order() {
|
||||
let plan = bulk_send_plan(&addrs(250), 100, SendPacing::from_seconds(10));
|
||||
assert_eq!(plan.len(), 3, "250 / 100 = 3 batches");
|
||||
assert_eq!(plan[0].recipients.len(), 100);
|
||||
assert_eq!(plan[1].recipients.len(), 100);
|
||||
assert_eq!(plan[2].recipients.len(), 50);
|
||||
// Order is preserved across the split.
|
||||
assert_eq!(plan[0].recipients[0], "u0@example.com");
|
||||
assert_eq!(plan[1].recipients[0], "u100@example.com");
|
||||
assert_eq!(plan[2].recipients[0], "u200@example.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delays_accumulate_across_batches() {
|
||||
let plan = bulk_send_plan(&addrs(250), 100, SendPacing::from_seconds(10));
|
||||
assert_eq!(plan[0].delay_before_ms, 0);
|
||||
assert_eq!(plan[1].delay_before_ms, 10_000);
|
||||
assert_eq!(plan[2].delay_before_ms, 20_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_zero_batch_size_or_empty_list_is_an_empty_plan() {
|
||||
assert!(bulk_send_plan(&addrs(5), 0, SendPacing::default()).is_empty());
|
||||
assert!(bulk_send_plan(&[], 100, SendPacing::default()).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_default_batch_size_matches_the_recipient_cap() {
|
||||
assert_eq!(default_batch_size(), MAX_RECIPIENTS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_delay_of_zero_produces_no_gaps() {
|
||||
let plan = bulk_send_plan(&addrs(250), 100, SendPacing::from_millis(0));
|
||||
assert!(plan.iter().all(|b| b.delay_before_ms == 0));
|
||||
}
|
||||
|
||||
// ---- the worker loop, against an in-memory send --------------------
|
||||
|
||||
/// `run_bulk_send` sleeps via `platform::sleep` (tokio on native), so
|
||||
/// the test drives it on a current-thread runtime. The send closure is
|
||||
/// a mock that records its inputs, so the loop -- batching, pacing
|
||||
/// gaps, abandon, progress -- is exercised without any network.
|
||||
#[test]
|
||||
fn the_loop_sends_every_batch_and_reports_final_progress() {
|
||||
let plan = bulk_send_plan(&addrs(250), 100, SendPacing::from_millis(0));
|
||||
let abandon = std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
let sent_calls = std::sync::Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
|
||||
let progress_log = std::sync::Arc::new(std::sync::Mutex::new(Vec::<BulkProgress>::new()));
|
||||
|
||||
let calls = sent_calls.clone();
|
||||
let log = progress_log.clone();
|
||||
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_time()
|
||||
.build()
|
||||
.expect("runtime");
|
||||
|
||||
let final_progress = rt.block_on(run_bulk_send(
|
||||
plan,
|
||||
SendPacing::from_millis(0),
|
||||
&abandon,
|
||||
move |p| log.lock().unwrap().push(p.clone()),
|
||||
move |recipients: &str| {
|
||||
let calls = calls.clone();
|
||||
let s = recipients.to_string();
|
||||
async move {
|
||||
calls.lock().unwrap().push(s);
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
assert_eq!(final_progress.sent_batches, 3);
|
||||
assert_eq!(final_progress.failed_batches, 0);
|
||||
assert_eq!(final_progress.sent_recipients, 250);
|
||||
assert!(final_progress.done);
|
||||
|
||||
// Three batches sent, in order, each with its recipients joined.
|
||||
let calls = sent_calls.lock().unwrap();
|
||||
assert_eq!(calls.len(), 3);
|
||||
assert_eq!(calls[0], addrs(100).join(", "));
|
||||
// The third batch is the remaining 50 recipients, u200..u249.
|
||||
let tail: Vec<String> = (200..250).map(|i| format!("u{i}@example.com")).collect();
|
||||
assert_eq!(calls[2], tail.join(", "));
|
||||
|
||||
// Progress is reported after each batch (done=false) plus a final
|
||||
// (done=true): 4 updates for 3 batches.
|
||||
let log = progress_log.lock().unwrap();
|
||||
assert_eq!(log.len(), 4);
|
||||
assert!(!log[0].done);
|
||||
assert!(!log[2].done);
|
||||
assert!(log[3].done);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failed_batch_is_counted_but_does_not_stop_the_loop() {
|
||||
let plan = bulk_send_plan(&addrs(250), 100, SendPacing::from_millis(0));
|
||||
let abandon = std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_time()
|
||||
.build()
|
||||
.expect("runtime");
|
||||
|
||||
let final_progress = rt.block_on(run_bulk_send(
|
||||
plan,
|
||||
SendPacing::from_millis(0),
|
||||
&abandon,
|
||||
|_| {},
|
||||
|recipients: &str| {
|
||||
let s = recipients.to_string();
|
||||
async move {
|
||||
// Fail only the second batch.
|
||||
if s.starts_with("u100@") {
|
||||
Err("relay refused".to_string())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
assert_eq!(final_progress.sent_batches, 2);
|
||||
assert_eq!(final_progress.failed_batches, 1);
|
||||
assert_eq!(final_progress.sent_recipients, 150);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn abandon_stops_the_loop_between_batches() {
|
||||
let plan = bulk_send_plan(&addrs(250), 100, SendPacing::from_millis(0));
|
||||
let abandon = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
|
||||
let sent = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||
let sent_clone = sent.clone();
|
||||
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_time()
|
||||
.build()
|
||||
.expect("runtime");
|
||||
|
||||
let abandon_for_closure = abandon.clone();
|
||||
let progress = rt.block_on(run_bulk_send(
|
||||
plan,
|
||||
SendPacing::from_millis(0),
|
||||
&abandon,
|
||||
|_| {},
|
||||
move |_: &str| {
|
||||
let sent = sent_clone.clone();
|
||||
let abandon = abandon_for_closure.clone();
|
||||
async move {
|
||||
let n = sent.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
// Abandon after the first batch is sent.
|
||||
if n == 0 {
|
||||
abandon.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
assert_eq!(sent.load(std::sync::atomic::Ordering::Relaxed), 1);
|
||||
assert_eq!(progress.sent_batches, 1);
|
||||
assert!(progress.done, "abandon still emits a final update");
|
||||
}
|
||||
}
|
||||
|
|
@ -359,49 +359,6 @@ impl EmailSendRequest {
|
|||
))
|
||||
}
|
||||
|
||||
/// Validate a bulk send's message WITHOUT the per-message recipient
|
||||
/// cap (C6), returning the accepted mailbox strings and the parse
|
||||
/// outcome.
|
||||
///
|
||||
/// `build_without_config` refuses a list over `MAX_RECIPIENTS`; the
|
||||
/// bulk path must instead *accept* the list and batch it. Everything
|
||||
/// else is identical: no recipients, subject/body limits, empty body.
|
||||
pub fn validate_bulk_message(
|
||||
to: &str,
|
||||
subject: &str,
|
||||
body: &str,
|
||||
) -> Result<(Vec<String>, RecipientList), SendError> {
|
||||
let list = parse_recipients(to);
|
||||
if list.accepted.is_empty() {
|
||||
return Err(if list.rejected.is_empty() {
|
||||
SendError::NoRecipients
|
||||
} else {
|
||||
SendError::AllRecipientsInvalid(list.rejected.clone())
|
||||
});
|
||||
}
|
||||
if subject.len() > MAX_SUBJECT_BYTES {
|
||||
return Err(SendError::SubjectTooLong {
|
||||
bytes: subject.len(),
|
||||
max: MAX_SUBJECT_BYTES,
|
||||
});
|
||||
}
|
||||
if body.trim().is_empty() {
|
||||
return Err(SendError::EmptyBody);
|
||||
}
|
||||
if body.len() > MAX_BODY_BYTES {
|
||||
return Err(SendError::BodyTooLarge {
|
||||
bytes: body.len(),
|
||||
max: MAX_BODY_BYTES,
|
||||
});
|
||||
}
|
||||
let addresses = list
|
||||
.accepted
|
||||
.iter()
|
||||
.map(|r| r.to_mailbox_string())
|
||||
.collect();
|
||||
Ok((addresses, list))
|
||||
}
|
||||
|
||||
pub fn recipient_count(&self) -> usize {
|
||||
self.recipients.len()
|
||||
}
|
||||
|
|
@ -599,37 +556,6 @@ mod tests {
|
|||
));
|
||||
}
|
||||
|
||||
/// C6: a bulk list must NOT be refused at the per-message cap -- it is
|
||||
/// accepted whole (the caller batches it) while subject/body limits
|
||||
/// still hold.
|
||||
#[test]
|
||||
fn validate_bulk_message_accepts_a_list_over_the_cap() {
|
||||
let many: Vec<String> = (0..MAX_RECIPIENTS + 50)
|
||||
.map(|i| format!("u{i}@example.com"))
|
||||
.collect();
|
||||
let (addresses, list) =
|
||||
EmailSendRequest::validate_bulk_message(&many.join(", "), "Hi", "Body").unwrap();
|
||||
assert_eq!(addresses.len(), MAX_RECIPIENTS + 50);
|
||||
assert!(list.rejected.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_bulk_message_still_enforces_message_limits() {
|
||||
assert_eq!(
|
||||
EmailSendRequest::validate_bulk_message("", "s", "b").unwrap_err(),
|
||||
SendError::NoRecipients
|
||||
);
|
||||
assert_eq!(
|
||||
EmailSendRequest::validate_bulk_message("a@x.com", "s", " ").unwrap_err(),
|
||||
SendError::EmptyBody
|
||||
);
|
||||
let s = "a".repeat(MAX_SUBJECT_BYTES + 1);
|
||||
assert!(matches!(
|
||||
EmailSendRequest::validate_bulk_message("a@x.com", &s, "b").unwrap_err(),
|
||||
SendError::SubjectTooLong { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_recipients_and_all_invalid_are_different_errors() {
|
||||
assert_eq!(
|
||||
|
|
|
|||
|
|
@ -401,67 +401,6 @@ pub fn spawn_send_email(config: SmtpConfig, to: String, subject: String, body: S
|
|||
});
|
||||
}
|
||||
|
||||
/// Spawn a paced bulk send (C6, completed).
|
||||
///
|
||||
/// The Bulk tab used to send ONE message to N recipients and hit
|
||||
/// `MAX_RECIPIENTS` (100) hard -- a 500-recipient list was refused, not
|
||||
/// paced. This splits the list into provider-sized batches and sends them
|
||||
/// with a `SendPacing` gap, holding the batch under the rate-limit ceiling,
|
||||
/// and posts `BulkSendProgress` after each batch and once at the end.
|
||||
///
|
||||
/// The recipients are already-validated mailbox strings (the caller parses
|
||||
/// the raw input through `parse_recipients` first, so a bad entry is
|
||||
/// reported before any batch goes out). Same in-flight guard and abandon
|
||||
/// control as the single send (B5/B6).
|
||||
pub fn spawn_bulk_send(config: SmtpConfig, recipients: Vec<String>, subject: String, body: String) {
|
||||
if config.is_incomplete() {
|
||||
Cx::post_action(EmailWorkerAction::SendResult(Err(
|
||||
INCOMPLETE_CONFIG_MESSAGE.to_string(),
|
||||
)));
|
||||
return;
|
||||
}
|
||||
if recipients.is_empty() {
|
||||
Cx::post_action(EmailWorkerAction::SendResult(Err(
|
||||
"Add at least one recipient.".to_string(),
|
||||
)));
|
||||
return;
|
||||
}
|
||||
if SEND_IN_FLIGHT.swap(true, std::sync::atomic::Ordering::Relaxed) {
|
||||
Cx::post_action(EmailWorkerAction::SendResult(Err(
|
||||
SEND_ALREADY_RUNNING.to_string()
|
||||
)));
|
||||
return;
|
||||
}
|
||||
SEND_ABANDONED.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
let batch_size = crate::email_bulk::default_batch_size();
|
||||
let pacing = crate::email_pacing::SendPacing::default();
|
||||
let plan = crate::email_bulk::bulk_send_plan(&recipients, batch_size, pacing);
|
||||
|
||||
crate::platform::spawn(async move {
|
||||
crate::email_bulk::run_bulk_send(
|
||||
plan,
|
||||
pacing,
|
||||
&SEND_ABANDONED,
|
||||
|p| Cx::post_action(EmailWorkerAction::BulkSendProgress(p.clone())),
|
||||
|to: &str| {
|
||||
// The send future must OWN the joined recipient string
|
||||
// (and borrow the config/subject/body, which outlive it),
|
||||
// so the closure copies the &str out and captures the rest
|
||||
// by reference.
|
||||
let to_owned = to.to_string();
|
||||
let config_ref = &config;
|
||||
let subject_ref = &subject;
|
||||
let body_ref = &body;
|
||||
async move { send_email_impl(config_ref, &to_owned, subject_ref, body_ref).await }
|
||||
},
|
||||
)
|
||||
.await;
|
||||
SEND_IN_FLIGHT.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
// The final done=true progress was already posted by the loop.
|
||||
});
|
||||
}
|
||||
|
||||
// --- Native implementation: direct SMTP via lettre ---
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
|
|
@ -867,9 +806,6 @@ pub enum EmailWorkerAction {
|
|||
/// the reason on failure, so the inbox can render loading/error/empty
|
||||
/// states without knowing which backend produced them.
|
||||
InboxFetched(Result<Vec<crate::email_store::EmailMessage>, String>),
|
||||
/// Progress of a paced bulk send (C6), posted after each batch and once
|
||||
/// at the end (`done`).
|
||||
BulkSendProgress(crate::email_bulk::BulkProgress),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ pub mod mail_backend;
|
|||
pub mod mail_proxy;
|
||||
pub mod email_worker;
|
||||
pub mod email_pacing;
|
||||
pub mod email_bulk;
|
||||
pub mod credential_store;
|
||||
pub mod email_cache;
|
||||
pub mod imap_client;
|
||||
|
|
|
|||
|
|
@ -53,7 +53,6 @@ nigig-core/src/email_store.rs:95
|
|||
nigig-core/src/email_account.rs:95
|
||||
nigig-core/src/secret.rs:95
|
||||
nigig-core/src/email_pacing.rs:90
|
||||
nigig-core/src/email_bulk.rs:85
|
||||
nigig-core/src/credential_store.rs:90
|
||||
nigig-core/src/email_cache.rs:80
|
||||
nigig-core/src/email_session.rs:90
|
||||
|
|
@ -131,7 +130,6 @@ EMAIL_FILES=(
|
|||
"$ROOT/crates/nigig-core/src/mail_proxy.rs"
|
||||
"$ROOT/crates/nigig-core/src/secret.rs"
|
||||
"$ROOT/crates/nigig-core/src/email_pacing.rs"
|
||||
"$ROOT/crates/nigig-core/src/email_bulk.rs"
|
||||
"$ROOT/crates/nigig-core/src/credential_store.rs"
|
||||
"$ROOT/crates/nigig-core/src/email_cache.rs"
|
||||
"$ROOT/crates/nigig-core/src/email_session.rs"
|
||||
|
|
@ -165,8 +163,8 @@ with open(path) as fh:
|
|||
|
||||
keep = ("email_account.rs", "email_send.rs", "email_store.rs",
|
||||
"email_worker.rs", "mail_backend.rs", "mail_proxy.rs", "secret.rs",
|
||||
"email_pacing.rs", "email_bulk.rs", "credential_store.rs",
|
||||
"email_cache.rs", "email_session.rs", "imap_client.rs")
|
||||
"email_pacing.rs", "credential_store.rs", "email_cache.rs",
|
||||
"email_session.rs", "imap_client.rs")
|
||||
files = [f for f in data["data"][0]["files"]
|
||||
if f["filename"].endswith(keep)]
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue