commit 3774a378f3a65f4f889e3a10b9954f938529ccf9 Author: Dennis Nemec Date: Mon Jun 1 18:02:07 2026 +0200 Initial: Holzleitner Mail-Client (Rust, Windows-Service) Polling-Client, der beim Backend die noch nicht versendeten ausgelieferten Belege abfragt (GET /admin/delivered-belegnummern), ERPframe per CLI zum Mailversand anstößt (_SV_MAIL_VERSAND) und die Belege anschließend als versendet markiert (POST /admin/mark-mail-sent). Authentifizierung gegen das Backend per X-Admin-Api-Key. Enthält: Config-Laden (config.json, Vorlage config.example.json), Logging, Windows-Service-Wrapper (install-/uninstall-service.ps1). Nicht im Repo (.gitignore): config.json (Secrets: Admin-Key + ERPframe- Passwort), target/, logs/. config.example.json trägt nur Platzhalter. Co-Authored-By: Claude Opus 4.8 (1M context) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b29d1f --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/target +# enthält Secrets (API-Key, ERPframe-Passwort) — nicht committen +/config.json +/logs +.DS_Store diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..7dfcfd0 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1603 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "holzleitner-mailclient" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "reqwest", + "serde", + "serde_json", + "tokio", + "windows-service", +] + +[[package]] +name = "http" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "log" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[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 = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.11.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-service" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd9db37ecb5b13762d95468a2fc6009d4b2c62801243223aabd44fca13ad13c8" +dependencies = [ + "bitflags 1.3.2", + "widestring", + "windows-sys 0.45.0", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..b50166c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "holzleitner-mailclient" +version = "0.1.0" +edition = "2021" +description = "Pollt die noch nicht versendeten Belegnummern vom Holzleitner-Backend und stößt ERPFRAME.EXE an (das die Mails verschickt). Läuft als langlaufender Prozess unter Windows." + +[[bin]] +name = "holzleitner-mailclient" +path = "src/main.rs" + +[dependencies] +tokio = { version = "1", features = ["rt-multi-thread", "macros", "time", "signal", "process"] } +# rustls statt OpenSSL → keine C/OpenSSL-Abhängigkeit, einfacher für Windows-Builds. +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +chrono = "0.4" +anyhow = "1" + +# Windows-Dienst-Integration (SCM). Nur unter Windows kompiliert; auf anderen +# Plattformen (z. B. Mac für den Kompiliertest) wird das Modul ausgeblendet. +[target.'cfg(windows)'.dependencies] +windows-service = "0.6" + +[profile.release] +strip = true +lto = true diff --git a/README.md b/README.md new file mode 100644 index 0000000..56253cf --- /dev/null +++ b/README.md @@ -0,0 +1,108 @@ +# Holzleitner Mail-Versende-Client + +Langlaufender Windows-Client. Pollt alle 5 Minuten die noch **nicht versendeten** +Belegnummern vom Backend und stößt **ERPFRAME.EXE** an, das die Mails verschickt. + +## Ablauf je Tick (Default alle 300 s) + +1. `GET {backend}/admin/delivered-belegnummern` — Header `X-Admin-Api-Key`. + Liefert nur Belege, deren Liefermail noch offen ist (`mail_sent_at IS NULL`). +2. Sind welche offen: **ein** Aufruf + `ERPFRAME.EXE ` + (alle Belegnummern kommagetrennt als ein Argument) und auf Ende warten. +3. **Nur bei ExitCode 0**: `POST {backend}/admin/mark-mail-sent` mit den + Belegnummern → markiert sie server-seitig als versendet (Dedup). + +Reihenfolge **erst ERPframe, dann markieren**: schlägt ERPframe fehl, bleiben +die Belege offen und werden beim nächsten Tick erneut versucht. Alle Fehler +werden geloggt, die Schleife läuft weiter. + +## Konfiguration + +`config.json` **neben der EXE** (Vorlage: `config.example.json`). Enthält +Secrets → nicht ins Git (siehe `.gitignore`). + +| Feld | Bedeutung | +|---|---| +| `backend_base_url` | z. B. `http://10.168.10.2:3000` (ohne `/admin`) | +| `admin_api_key` | Wert für Header `X-Admin-Api-Key` | +| `poll_interval_secs` | Poll-Intervall (Default 300) | +| `request_timeout_secs` | HTTP-Timeout je Request (Default 30) | +| `erp_timeout_secs` | Timeout für den ERPframe-Prozess; 0 = unbegrenzt (Default 600) | +| `belegnummer_separator` | Trennzeichen für das Belege-Argument (Default `,`) | +| `log_dir` | Log-Verzeichnis (relativ ⇒ relativ zur EXE; Default `logs`) | +| `erpframe.exe_path` | voller Pfad zu `ERPFRAME.EXE` | +| `erpframe.mode` | erstes Argument (Default `AUTOSTARTUP`) | +| `erpframe.app_name` / `user` / `password` / `command` | wie im PowerShell-Skript | +| `erpframe.working_dir` | Arbeitsverzeichnis (`null` ⇒ EXE-Verzeichnis) | + +Logs: `/mailclient_YYYY-MM-DD.log` (+ stdout). + +## Build + +Das Programm nutzt **rustls** (kein OpenSSL) → keine C-Abhängigkeiten. + +### Auf einem Windows-Rechner (empfohlen, MSVC) +``` +cargo build --release +``` +→ `target\release\holzleitner-mailclient.exe` + +### Cross-Compile vom Mac/Linux (GNU-Target) +``` +rustup target add x86_64-pc-windows-gnu +brew install mingw-w64 # macOS; Linux: apt install gcc-mingw-w64 +cargo build --release --target x86_64-pc-windows-gnu +``` +→ `target/x86_64-pc-windows-gnu/release/holzleitner-mailclient.exe` + +### In einer Windows-VM auf Apple Silicon (ARM64) +Die VM ist **ARM64** — ein einfaches `cargo build` erzeugt eine ARM64-EXE, die +auf dem **x64**-Server NICHT läuft. Immer explizit x64 bauen: +``` +rustup target add x86_64-pc-windows-msvc +cargo build --release --target x86_64-pc-windows-msvc +``` +**Nicht auf einem Shared-Drive (`Z:`) bauen** (Build bricht mit `os error 87` ab, +weil das Shared-FS die Temp-Datei-Operationen nicht unterstützt) — die Quelle +darf dort liegen, aber `target/` auf lokales NTFS legen: +``` +$env:CARGO_TARGET_DIR = "C:\cargo-target\mailclient" +``` + +## Deployment als Windows-Dienst + +Die EXE ist **dienstfähig** — sie spricht direkt mit dem Service Control Manager +(sauberes Start/Stop, Autostart, Auto-Restart bei Absturz). + +1. EXE + `config.json` in ein Verzeichnis legen, z. B. `C:\Holzleitner\Mailclient\`. + `config.json` muss **neben der EXE** liegen (der Dienst startet mit + Arbeitsverzeichnis `C:\Windows\System32`). +2. PowerShell **als Administrator** öffnen und registrieren: + ```powershell + cd C:\Holzleitner\Mailclient + .\install-service.ps1 + # optional unter einem bestimmten Konto (z. B. für ERPframe-DB-/Netzzugriff): + .\install-service.ps1 -Credential (Get-Credential) + ``` + Registriert den Dienst **„Holzleitner App Mails"** (interner Name + `HolzleitnerAppMails`), verzögerter Autostart, Auto-Restart, und startet ihn. +3. Verwalten — in `services.msc` erscheint er als **„Holzleitner App Mails"**: + ```powershell + Get-Service HolzleitnerAppMails + Stop-Service HolzleitnerAppMails + Start-Service HolzleitnerAppMails + ``` +4. Entfernen: + ```powershell + .\uninstall-service.ps1 + ``` + +### Konsolenmodus (zum Testen) +Direkter Start (Doppelklick / Konsole) läuft automatisch im Konsolenmodus +(der SCM-Dispatcher schlägt fehl → Fallback). Erzwingen mit: +```powershell +.\holzleitner-mailclient.exe --console +``` +Beenden mit Ctrl-C (sauberer Shutdown). Da die Binary selbst alle 5 Min pollt, +**keinen** zusätzlichen 5-Minuten-Task einrichten — ein Dauerprozess genügt. diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..f6582ca --- /dev/null +++ b/config.example.json @@ -0,0 +1,18 @@ +{ + "backend_base_url": "http://10.168.10.2:3000", + "admin_api_key": "", + "poll_interval_secs": 300, + "request_timeout_secs": 30, + "erp_timeout_secs": 600, + "belegnummer_separator": ",", + "log_dir": "logs", + "erpframe": { + "exe_path": "C:\\Program Files\\GSD\\ERPframe\\ERPFRAME.EXE", + "mode": "AUTOSTARTUP", + "app_name": "HOLZ_SQL_TEST_APP", + "user": "SYSTEM", + "password": "GEHEIM", + "command": "_SV_MAIL_VERSAND", + "working_dir": null + } +} diff --git a/install-service.ps1 b/install-service.ps1 new file mode 100644 index 0000000..8472ce1 --- /dev/null +++ b/install-service.ps1 @@ -0,0 +1,80 @@ +#Requires -RunAsAdministrator +<# +.SYNOPSIS + Registriert den Holzleitner-Mailclient als Windows-Dienst "Holzleitner App Mails". + +.DESCRIPTION + Muss als Administrator ausgeführt werden. Erwartet die kompilierte EXE + (Default: neben diesem Skript) und eine config.json IM SELBEN Verzeichnis + wie die EXE (der Dienst startet mit Arbeitsverzeichnis C:\Windows\System32, + daher werden config.json/logs relativ zur EXE aufgelöst). + +.PARAMETER ExePath + Pfad zu holzleitner-mailclient.exe. Default: neben diesem Skript. + +.PARAMETER Credential + Optionales Dienstkonto (z. B. der ERPframe-/Domänen-Benutzer). Ohne Angabe + läuft der Dienst als LocalSystem. Für ERPframe ist oft ein echtes + Benutzerkonto nötig (DB-/Netzwerkzugriff). Das Konto braucht das Recht + "Als Dienst anmelden". + +.EXAMPLE + .\install-service.ps1 + +.EXAMPLE + .\install-service.ps1 -ExePath "C:\HolzleitnerMail\holzleitner-mailclient.exe" -Credential (Get-Credential) +#> +[CmdletBinding()] +param( + [string]$ExePath, + [string]$ServiceName = "HolzleitnerAppMails", + [string]$DisplayName = "Holzleitner App Mails", + [pscredential]$Credential +) +$ErrorActionPreference = 'Stop' + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +if (-not $ExePath) { $ExePath = Join-Path $ScriptDir 'holzleitner-mailclient.exe' } +if (-not (Test-Path $ExePath)) { throw "EXE nicht gefunden: $ExePath" } +$ExePath = (Resolve-Path $ExePath).Path +$ExeDir = Split-Path $ExePath -Parent + +if (-not (Test-Path (Join-Path $ExeDir 'config.json'))) { + Write-Warning "config.json fehlt neben der EXE ($ExeDir) - der Dienst startet sonst nicht korrekt." +} + +# Vorhandenen Dienst stoppen & entfernen (Neuinstallation) +$existing = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue +if ($existing) { + Write-Host "Dienst '$ServiceName' existiert bereits - stoppe & entferne..." + if ($existing.Status -ne 'Stopped') { Stop-Service -Name $ServiceName -Force } + & sc.exe delete $ServiceName | Out-Null + Start-Sleep -Seconds 2 +} + +# binPath in doppelten Quotes (Pfad kann Leerzeichen enthalten) +$bin = "`"$ExePath`"" + +$params = @{ + Name = $ServiceName + DisplayName = $DisplayName + BinaryPathName = $bin + StartupType = 'Automatic' + Description = 'Pollt offene Belegnummern vom Holzleitner-Backend und stoesst ERPframe zum Mailversand an (alle 5 Min).' +} +if ($Credential) { $params['Credential'] = $Credential } + +Write-Host "Registriere Dienst '$DisplayName' ($ServiceName) -> $ExePath" +New-Service @params | Out-Null + +# Auto-Restart bei Absturz: 3x mit 60s Abstand, Fehlerzaehler nach 1 Tag zuruecksetzen +& sc.exe failure $ServiceName reset= 86400 actions= restart/60000/restart/60000/restart/60000 | Out-Null +# Verzoegerter Autostart (startet nach den System-Diensten) +& sc.exe config $ServiceName start= delayed-auto | Out-Null + +Write-Host "Starte Dienst..." +Start-Service -Name $ServiceName +Start-Sleep -Seconds 1 +Get-Service -Name $ServiceName | Format-List Name, DisplayName, Status, StartType +Write-Host "Logs: $(Join-Path $ExeDir 'logs')" +Write-Host "Fertig." diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..b8dd366 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,89 @@ +//! Konfiguration aus `config.json` (liegt neben der EXE). + +use std::path::Path; + +use anyhow::{Context, Result}; +use serde::Deserialize; + +#[derive(Debug, Clone, Deserialize)] +pub struct Config { + /// Basis-URL des Backends, z. B. `http://10.168.10.2:3000` (ohne /admin). + pub backend_base_url: String, + /// Wert für den Header `X-Admin-Api-Key`. + pub admin_api_key: String, + + /// Poll-Intervall in Sekunden (Default 300 = 5 Min). + #[serde(default = "d_poll")] + pub poll_interval_secs: u64, + /// Timeout je HTTP-Request in Sekunden (Default 30). + #[serde(default = "d_timeout")] + pub request_timeout_secs: u64, + /// Timeout für den ERPframe-Prozess in Sekunden; 0 = unbegrenzt warten + /// (Default 600 = 10 Min). Verhindert, dass ein hängender ERPframe-Aufruf + /// die Schleife dauerhaft blockiert. + #[serde(default = "d_erp_timeout")] + pub erp_timeout_secs: u64, + /// Trennzeichen, mit dem die Belegnummern zu EINEM ERPframe-Argument + /// verbunden werden (Default ","). + #[serde(default = "d_sep")] + pub belegnummer_separator: String, + /// Log-Verzeichnis; relativ ⇒ relativ zum EXE-Verzeichnis (Default "logs"). + #[serde(default = "d_logdir")] + pub log_dir: String, + + pub erpframe: ErpframeConfig, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct ErpframeConfig { + /// Voller Pfad zu ERPFRAME.EXE. + pub exe_path: String, + /// Erstes Argument (wie im PowerShell-Skript), Default "AUTOSTARTUP". + #[serde(default = "d_mode")] + pub mode: String, + pub app_name: String, + pub user: String, + pub password: String, + /// Das ERPframe-Kommando/Makro, das die Mails versendet. + pub command: String, + /// Arbeitsverzeichnis; `null` ⇒ EXE-Verzeichnis. + #[serde(default)] + pub working_dir: Option, +} + +impl Config { + pub fn load(path: &Path) -> Result { + let raw = std::fs::read_to_string(path) + .with_context(|| format!("Datei lesen: {}", path.display()))?; + let cfg: Config = serde_json::from_str(&raw).context("config.json parsen")?; + if cfg.backend_base_url.trim().is_empty() { + anyhow::bail!("backend_base_url ist leer"); + } + if cfg.admin_api_key.trim().is_empty() { + anyhow::bail!("admin_api_key ist leer"); + } + if cfg.erpframe.exe_path.trim().is_empty() { + anyhow::bail!("erpframe.exe_path ist leer"); + } + Ok(cfg) + } +} + +fn d_poll() -> u64 { + 300 +} +fn d_timeout() -> u64 { + 30 +} +fn d_erp_timeout() -> u64 { + 600 +} +fn d_sep() -> String { + ",".into() +} +fn d_logdir() -> String { + "logs".into() +} +fn d_mode() -> String { + "AUTOSTARTUP".into() +} diff --git a/src/logging.rs b/src/logging.rs new file mode 100644 index 0000000..2047fc9 --- /dev/null +++ b/src/logging.rs @@ -0,0 +1,45 @@ +//! Schlanker Datei-Logger: schreibt zeitgestempelte Zeilen in ein Tageslog +//! (`/mailclient_YYYY-MM-DD.log`) UND auf stdout — analog zum +//! PowerShell-Skript, ohne zusätzliche Logging-Crates. + +use std::fs; +use std::io::Write; +use std::path::PathBuf; + +pub struct Logger { + dir: PathBuf, +} + +impl Logger { + pub fn new(dir: impl Into) -> std::io::Result { + let dir = dir.into(); + fs::create_dir_all(&dir)?; + Ok(Self { dir }) + } + + fn write(&self, level: &str, msg: &str) { + let now = chrono::Local::now(); + let line = format!( + "[{}] [{}] {}", + now.format("%Y-%m-%d %H:%M:%S"), + level, + msg + ); + println!("{line}"); + let file = self.dir.join(format!("mailclient_{}.log", now.format("%Y-%m-%d"))); + if let Ok(mut f) = fs::OpenOptions::new().create(true).append(true).open(&file) { + let _ = writeln!(f, "{line}"); + } + } + + pub fn info(&self, msg: &str) { + self.write("INFO", msg); + } + #[allow(dead_code)] // Teil der Logger-API, aktuell nicht genutzt + pub fn warn(&self, msg: &str) { + self.write("WARN", msg); + } + pub fn error(&self, msg: &str) { + self.write("ERROR", msg); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..724169a --- /dev/null +++ b/src/main.rs @@ -0,0 +1,307 @@ +//! Holzleitner Mail-Versende-Client (Windows). +//! +//! Läuft entweder als **Windows-Dienst** (vom SCM gestartet) oder im +//! **Konsolenmodus** (interaktiv / zum Testen). In beiden Fällen pollt er alle +//! `poll_interval_secs` (Default 300 = 5 Min): +//! 1. GET {backend}/admin/delivered-belegnummern (offene, noch nicht +//! versendete Belege; Header X-Admin-Api-Key) +//! 2. Wenn nicht leer: EIN Aufruf von ERPFRAME.EXE mit allen Belegnummern +//! (Format 'V-1','V-2') — ERPframe verschickt die Mails. +//! 3. Nur bei ExitCode 0: POST {backend}/admin/mark-mail-sent → markiert die +//! Belege server-seitig als versendet (Dedup). +//! +//! Erst ERPframe, dann markieren ⇒ schlägt ERPframe fehl, bleiben die Belege +//! offen und werden beim nächsten Tick erneut versucht. + +mod config; +mod logging; +#[cfg(windows)] +mod service; + +use std::path::{Path, PathBuf}; +use std::time::Duration; + +use anyhow::{anyhow, Context, Result}; +use serde::Deserialize; + +use crate::config::Config; +use crate::logging::Logger; + +#[derive(Debug, Deserialize)] +struct DeliveredResponse { + #[serde(default)] + belegnummern: Vec, +} + +#[derive(Debug, Deserialize)] +struct MarkResponse { + #[serde(default)] + marked: u64, +} + +/// Verzeichnis der laufenden EXE. Wichtig, weil ein Dienst mit Arbeits- +/// verzeichnis `C:\Windows\System32` startet — config.json/logs werden daher +/// IMMER relativ zur EXE aufgelöst, nicht relativ zum cwd. +fn exe_dir() -> PathBuf { + std::env::current_exe() + .ok() + .and_then(|p| p.parent().map(|d| d.to_path_buf())) + .unwrap_or_else(|| PathBuf::from(".")) +} + +fn main() { + // Unter Windows: standardmäßig versuchen, als Dienst zu starten. Wird die + // EXE interaktiv gestartet (Doppelklick / Konsole), scheitert der + // SCM-Dispatcher (nicht vom SCM gestartet) → Konsolenmodus. `--console` + // erzwingt den Konsolenmodus direkt. + #[cfg(windows)] + { + let console = std::env::args().any(|a| a == "--console" || a == "-c"); + if !console { + match service::run() { + Ok(()) => return, // lief als Dienst, sauber beendet + Err(_e) => { /* interaktiv gestartet → Konsole */ } + } + } + } + + run_console(); +} + +/// Konsolenmodus: eigene tokio-Runtime, Stop via Ctrl-C. +fn run_console() { + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .expect("tokio runtime"); + rt.block_on(run_app(async { + let _ = tokio::signal::ctrl_c().await; + })); +} + +/// Lädt Konfiguration + Logger und fährt die Poll-Schleife, bis `shutdown` +/// feuert (Ctrl-C im Konsolenmodus, Stop/Shutdown vom SCM im Dienstmodus). +/// Gibt selbst keine Fehler zurück — alles wird geloggt (im Dienst gibt es +/// keine Konsole; vor dem Logger greift `fallback_log`). +pub(crate) async fn run_app(shutdown: impl std::future::Future) { + let base_dir = exe_dir(); + + let cfg = match Config::load(&base_dir.join("config.json")) { + Ok(c) => c, + Err(e) => { + fallback_log(&base_dir, &format!("Konfiguration konnte nicht geladen werden: {e:#}")); + return; + } + }; + + let log_dir = if Path::new(&cfg.log_dir).is_absolute() { + PathBuf::from(&cfg.log_dir) + } else { + base_dir.join(&cfg.log_dir) + }; + let logger = match Logger::new(&log_dir) { + Ok(l) => l, + Err(e) => { + fallback_log(&base_dir, &format!("Log-Verzeichnis konnte nicht angelegt werden: {e:#}")); + return; + } + }; + + logger.info(&format!( + "Mailclient gestartet. backend={} intervall={}s erpframe={}", + cfg.backend_base_url, cfg.poll_interval_secs, cfg.erpframe.exe_path + )); + + let client = match reqwest::Client::builder() + .timeout(Duration::from_secs(cfg.request_timeout_secs)) + .build() + { + Ok(c) => c, + Err(e) => { + logger.error(&format!("HTTP-Client konnte nicht gebaut werden: {e:#}")); + return; + } + }; + + let mut ticker = tokio::time::interval(Duration::from_secs(cfg.poll_interval_secs)); + tokio::pin!(shutdown); + + loop { + tokio::select! { + _ = ticker.tick() => { + if let Err(e) = run_tick(&cfg, &client, &logger).await { + logger.error(&format!("Durchlauf fehlgeschlagen: {e:#}")); + } + } + _ = &mut shutdown => { + logger.info("Beende (Stop-Signal)."); + break; + } + } + } +} + +/// Notfall-Logging, wenn der reguläre Logger (noch) nicht steht — schreibt nach +/// `\mailclient-fatal.log`. Im Dienstmodus die einzige Spur, falls +/// schon das Laden von config.json scheitert. +fn fallback_log(dir: &Path, msg: &str) { + eprintln!("{msg}"); + let file = dir.join("mailclient-fatal.log"); + if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(&file) { + use std::io::Write; + let _ = writeln!( + f, + "[{}] {}", + chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), + msg + ); + } +} + +/// Ein Poll-Durchlauf. Fehler werden zurückgegeben (vom Aufrufer geloggt) und +/// brechen die Schleife NICHT ab. +async fn run_tick(cfg: &Config, client: &reqwest::Client, logger: &Logger) -> Result<()> { + let base = cfg.backend_base_url.trim_end_matches('/'); + + // 1) Offene Belege holen (ohne day ⇒ alle offenen). + let url = format!("{base}/admin/delivered-belegnummern"); + let resp = client + .get(&url) + .header("X-Admin-Api-Key", &cfg.admin_api_key) + .send() + .await + .context("GET delivered-belegnummern")?; + let status = resp.status(); + if !status.is_success() { + let body = resp.text().await.unwrap_or_default(); + return Err(anyhow!("delivered-belegnummern HTTP {status}: {body}")); + } + let data: DeliveredResponse = resp.json().await.context("Antwort parsen")?; + let belege = data.belegnummern; + + if belege.is_empty() { + logger.info("Keine offenen Belege."); + return Ok(()); + } + let liste = belege.join(", "); + logger.info(&format!("{} offene Beleg(e) gefunden: {}", belege.len(), liste)); + + // 2) ERPframe aufrufen (ein Aufruf, alle Belege). Fehler ⇒ NICHT markieren, + // Belege bleiben offen und werden beim nächsten Tick erneut versucht. + if let Err(e) = run_erpframe(cfg, logger, &belege).await { + logger.error(&format!( + "ERPframe-Versand fehlgeschlagen — {} Beleg(e) bleiben offen [{}]: {e:#}", + belege.len(), + liste + )); + return Ok(()); + } + + // 3) Server-seitig als versendet markieren. + let mark_url = format!("{base}/admin/mark-mail-sent"); + let result = client + .post(&mark_url) + .header("X-Admin-Api-Key", &cfg.admin_api_key) + .json(&serde_json::json!({ "belegnummern": belege })) + .send() + .await; + + match result { + Ok(resp) if resp.status().is_success() => { + let marked = resp + .json::() + .await + .map(|m| m.marked) + .unwrap_or(0); + // DAS ist die zentrale „erfolgreich versendet"-Zeile: + logger.info(&format!( + "VERSENDET: {} Beleg(e) verschickt + als versendet markiert: [{}] (frisch markiert: {})", + belege.len(), + liste, + marked + )); + } + Ok(resp) => { + let status = resp.status(); + let body = resp.text().await.unwrap_or_default(); + // Mails sind raus, aber Markierung schlug fehl → Doppelversand-Risiko. + logger.error(&format!( + "ACHTUNG: ERPframe-Versand OK, aber mark-mail-sent HTTP {status} — Belege [{}] \ + bleiben offen und werden beim nächsten Tick ERNEUT versendet! Antwort: {body}", + liste + )); + } + Err(e) => { + logger.error(&format!( + "ACHTUNG: ERPframe-Versand OK, aber mark-mail-sent nicht erreichbar — Belege [{}] \ + bleiben offen und werden beim nächsten Tick ERNEUT versendet! Fehler: {e:#}", + liste + )); + } + } + Ok(()) +} + +/// Startet ERPFRAME.EXE synchron (ein Aufruf, alle Belege als ein Argument) +/// und wartet auf das Ende. Argumente wie im PowerShell-Skript: +/// ERPFRAME.EXE +/// Belege im Format 'V-1','V-2' (jede Nr. single-quoted, kommagetrennt). +async fn run_erpframe(cfg: &Config, logger: &Logger, belege: &[String]) -> Result<()> { + let e = &cfg.erpframe; + // Jede Belegnummer in einfache Anführungszeichen, mit dem konfigurierten + // Trenner verbunden → 'V-30690288','V-30690290' (Format, das das + // ERPframe-Makro erwartet, direkt für SQL `IN (...)` verwendbar). + let belege_csv = belege + .iter() + .map(|b| format!("'{b}'")) + .collect::>() + .join(&cfg.belegnummer_separator); + + let mut cmd = tokio::process::Command::new(&e.exe_path); + cmd.arg(&e.mode) + .arg(&e.app_name) + .arg(&e.user) + .arg(&e.password) + .arg(&e.command) + .arg(&belege_csv); + + // Arbeitsverzeichnis: konfiguriert, sonst EXE-Verzeichnis. + if let Some(wd) = &e.working_dir { + cmd.current_dir(wd); + } else if let Some(parent) = Path::new(&e.exe_path).parent() { + if !parent.as_os_str().is_empty() { + cmd.current_dir(parent); + } + } + + logger.info(&format!( + "Starte ERPframe: {} {} \"{}\" \"{}\" \"***\" \"{}\" \"{}\"", + e.exe_path, e.mode, e.app_name, e.user, e.command, belege_csv + )); + + let mut child = cmd + .spawn() + .with_context(|| format!("ERPframe starten: {}", e.exe_path))?; + + let status = if cfg.erp_timeout_secs > 0 { + match tokio::time::timeout(Duration::from_secs(cfg.erp_timeout_secs), child.wait()).await { + Ok(s) => s.context("ERPframe abwarten")?, + Err(_) => { + let _ = child.kill().await; + return Err(anyhow!( + "ERPframe Timeout nach {}s — Prozess beendet", + cfg.erp_timeout_secs + )); + } + } + } else { + child.wait().await.context("ERPframe abwarten")? + }; + + if status.success() { + logger.info("ERPframe erfolgreich (ExitCode 0)."); + Ok(()) + } else { + Err(anyhow!("ERPframe ExitCode {:?}", status.code())) + } +} diff --git a/src/service.rs b/src/service.rs new file mode 100644 index 0000000..08b054f --- /dev/null +++ b/src/service.rs @@ -0,0 +1,97 @@ +//! Windows-Dienst-Integration (Service Control Manager). Nur unter Windows +//! kompiliert (`#[cfg(windows)]` am `mod service` in `main.rs`). +//! +//! Ablauf: `run()` übergibt den Prozess an den SCM. Der SCM ruft `service_main` +//! in einem eigenen Thread; dort registrieren wir einen Control-Handler +//! (für Stop/Shutdown), melden „Running", starten eine eigene tokio-Runtime und +//! lassen `crate::run_app` laufen, bis der Handler ein Stop-Signal schickt. + +use std::ffi::OsString; +use std::sync::mpsc; +use std::time::Duration; + +use windows_service::service::{ + ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState, ServiceStatus, ServiceType, +}; +use windows_service::service_control_handler::{self, ServiceControlHandlerResult}; +use windows_service::{define_windows_service, service_dispatcher}; + +/// Interner Dienst-Name (Schlüssel). **Muss** mit dem `-Name` im +/// Install-Skript (`install-service.ps1`) übereinstimmen. Der Anzeigename +/// „Holzleitner App Mails" wird separat beim Registrieren gesetzt. +const SERVICE_NAME: &str = "HolzleitnerAppMails"; +const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS; + +define_windows_service!(ffi_service_main, service_main); + +/// Übergibt den Prozess an den SCM. Gibt `Err` zurück, wenn der Prozess NICHT +/// vom SCM gestartet wurde (interaktiver Start) — der Aufrufer fällt dann in +/// den Konsolenmodus zurück. +pub fn run() -> windows_service::Result<()> { + service_dispatcher::start(SERVICE_NAME, ffi_service_main) +} + +fn service_main(_args: Vec) { + // Fehler hier landen (falls vor dem Logger) im fallback_log von run_app bzw. + // werden vom SCM als nicht-laufend erkannt. + let _ = run_service(); +} + +fn run_service() -> windows_service::Result<()> { + // Kanal: Control-Handler -> Loop. Stop/Shutdown sendet ein () hinein. + let (shutdown_tx, shutdown_rx) = mpsc::channel::<()>(); + + let handler = move |control| -> ServiceControlHandlerResult { + match control { + ServiceControl::Stop | ServiceControl::Shutdown => { + let _ = shutdown_tx.send(()); + ServiceControlHandlerResult::NoError + } + ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, + _ => ServiceControlHandlerResult::NotImplemented, + } + }; + + let status_handle = service_control_handler::register(SERVICE_NAME, handler)?; + + // „Running" melden (akzeptiert Stop + System-Shutdown). + status_handle.set_service_status(ServiceStatus { + service_type: SERVICE_TYPE, + current_state: ServiceState::Running, + controls_accepted: ServiceControlAccept::STOP | ServiceControlAccept::SHUTDOWN, + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + })?; + + // Eigene tokio-Runtime im Dienst-Thread (kein #[tokio::main]). + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .expect("tokio runtime"); + rt.block_on(async move { + // mpsc::recv ist blockierend → in spawn_blocking auslagern und als + // Future an run_app übergeben. + let shutdown = async move { + let _ = tokio::task::spawn_blocking(move || { + let _ = shutdown_rx.recv(); + }) + .await; + }; + crate::run_app(shutdown).await; + }); + + // „Stopped" melden. + status_handle.set_service_status(ServiceStatus { + service_type: SERVICE_TYPE, + current_state: ServiceState::Stopped, + controls_accepted: ServiceControlAccept::empty(), + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + })?; + + Ok(()) +} diff --git a/uninstall-service.ps1 b/uninstall-service.ps1 new file mode 100644 index 0000000..cee8611 --- /dev/null +++ b/uninstall-service.ps1 @@ -0,0 +1,20 @@ +#Requires -RunAsAdministrator +<# +.SYNOPSIS + Stoppt und entfernt den Dienst "Holzleitner App Mails". +#> +[CmdletBinding()] +param( + [string]$ServiceName = "HolzleitnerAppMails" +) +$ErrorActionPreference = 'Stop' + +$svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue +if (-not $svc) { Write-Host "Dienst '$ServiceName' existiert nicht."; return } + +if ($svc.Status -ne 'Stopped') { + Write-Host "Stoppe Dienst..." + Stop-Service -Name $ServiceName -Force +} +& sc.exe delete $ServiceName | Out-Null +Write-Host "Dienst '$ServiceName' entfernt."