diff --git a/flake.lock b/flake.lock index 1f363dc..a989bb3 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,20 @@ { "nodes": { + "crane": { + "locked": { + "lastModified": 1739936662, + "narHash": "sha256-x4syUjNUuRblR07nDPeLDP7DpphaBVbUaSoeZkFbGSk=", + "owner": "ipetkov", + "repo": "crane", + "rev": "19de14aaeb869287647d9461cbd389187d8ecdb7", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, "flake-compat": { "flake": false, "locked": { @@ -97,6 +112,7 @@ }, "root": { "inputs": { + "crane": "crane", "flake-parts": "flake-parts", "git-hooks-nix": "git-hooks-nix", "nixpkgs": "nixpkgs", diff --git a/flake.nix b/flake.nix index 476a7d4..21280e1 100644 --- a/flake.nix +++ b/flake.nix @@ -7,6 +7,7 @@ flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs"; treefmt-nix.url = "github:numtide/treefmt-nix"; treefmt-nix.inputs.nixpkgs.follows = "nixpkgs"; + crane.url = "github:ipetkov/crane"; git-hooks-nix.url = "github:cachix/git-hooks.nix"; git-hooks-nix.inputs.nixpkgs.follows = "nixpkgs"; }; @@ -23,6 +24,7 @@ "aarch64-linux" ]; imports = [ + flake-parts.flakeModules.easyOverlay inputs.treefmt-nix.flakeModule inputs.git-hooks-nix.flakeModule ]; @@ -34,7 +36,10 @@ pkgs, system, ... - }: { + }: let + craneLib = inputs.crane.mkLib pkgs; + pkgs = nixpkgs.legacyPackages.${system}.extend self.overlays.default; + in { checks = let tests = ["internal" "basic" "aliases" "rspamd"]; genTest = testName: { @@ -44,9 +49,20 @@ in pkgs.lib.listToAttrs (map genTest tests); - devShells.default = pkgs.mkShell { + packages = rec { + default = mailnix; + mailnix = craneLib.buildPackage { + src = craneLib.cleanCargoSource ./pkgs/mailnix; + }; + }; + overlayAttrs = { + mailnix = config.packages.mailnix; + }; + + devShells.default = craneLib.devShell { packages = with pkgs; [ self'.formatter.outPath # Add all formatters to environment + mailnix ]; shellHook = '' ${config.pre-commit.installationScript} @@ -61,6 +77,7 @@ programs = { actionlint.enable = true; alejandra.enable = true; + rustfmt.enable = true; }; settings.global.excludes = [ ".envrc" diff --git a/pkgs/mailnix/Cargo.lock b/pkgs/mailnix/Cargo.lock new file mode 100644 index 0000000..9bd1eef --- /dev/null +++ b/pkgs/mailnix/Cargo.lock @@ -0,0 +1,749 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[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 = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "cc" +version = "1.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets", +] + +[[package]] +name = "clap" +version = "4.5.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "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 = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "libyml" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3302702afa434ffa30847a83305f0a69d6abd74293b6554c18ec85c7ef30c980" +dependencies = [ + "anyhow", + "version_check", +] + +[[package]] +name = "log" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" + +[[package]] +name = "mailnix" +version = "0.1.0" +dependencies = [ + "clap", + "regex", + "serde", + "serde_with", + "serde_yml", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[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.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + +[[package]] +name = "ryu" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" + +[[package]] +name = "serde" +version = "1.0.218" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.218" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.7.1", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_yml" +version = "0.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59e2dd588bf1597a252c3b920e0143eb99b0f76e4e082f4c92ce34fbc9e71ddd" +dependencies = [ + "indexmap 2.7.1", + "itoa", + "libyml", + "memchr", + "ryu", + "serde", + "version_check", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "time" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "unicode-ident" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[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_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[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_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[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_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/pkgs/mailnix/Cargo.toml b/pkgs/mailnix/Cargo.toml new file mode 100644 index 0000000..accee63 --- /dev/null +++ b/pkgs/mailnix/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "mailnix" +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { version = "4.5.28", features = ["derive"] } +regex = "1.11.1" +serde = { version = "1.0.217", features = ["derive", "rc"] } +serde_with = "3.12.0" +serde_yml = "0.0.12" diff --git a/pkgs/mailnix/src/cli.rs b/pkgs/mailnix/src/cli.rs new file mode 100644 index 0000000..ab995fa --- /dev/null +++ b/pkgs/mailnix/src/cli.rs @@ -0,0 +1,22 @@ +use clap::{Parser, Subcommand}; + +#[derive(Subcommand, Debug)] +pub enum Commands { + Check, + GenerateUserdb, + GenerateStaticPassdb, + UpdateDynamicPassdb { path: String }, + GenerateAliases, + GenerateDeniedRecipients, + GenerateDomains, +} + +#[derive(Parser, Debug)] +#[command(author, version, about)] +pub struct Cli { + pub config_path: String, + pub additional_config_path: Option, + + #[command(subcommand)] + pub command: Commands, +} diff --git a/pkgs/mailnix/src/config.rs b/pkgs/mailnix/src/config.rs new file mode 100644 index 0000000..0fc016d --- /dev/null +++ b/pkgs/mailnix/src/config.rs @@ -0,0 +1,199 @@ +use serde::Deserialize; +use serde_with::formats::PreferMany; +use serde_with::{serde_as, MapPreventDuplicates, OneOrMany}; + +use regex::Regex; +use std::collections::{HashMap, HashSet}; +use std::error::Error; +use std::fs::{self, File}; +use std::io::BufReader; +use std::path::Path; + +#[serde_as] +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Config { + #[serde(default)] + pub domains: Vec, + + #[serde(default)] + #[serde_as(deserialize_as = "MapPreventDuplicates<_, _>")] + pub accounts: HashMap, + + #[serde(default)] + #[serde_as(deserialize_as = "MapPreventDuplicates<_, OneOrMany<_, PreferMany>>")] + pub aliases: HashMap>, +} + +fn default_reject_message() -> String { + "This account cannot receive emails.".to_string() +} + +#[derive(Clone, Debug, Deserialize)] +pub struct AccountConfig { + #[serde(rename(deserialize = "hashedPassword"))] + #[serde(skip)] + pub hashed_password: String, + + #[serde(rename(deserialize = "hashedPassword"))] + hashed_password_: Option, + + #[serde(rename(deserialize = "hashedPasswordFile"))] + hashed_password_file_: Option, + + #[serde(default, rename(deserialize = "isSystemUser"))] + pub is_system_user: bool, + + #[serde( + default = "default_reject_message", + rename(deserialize = "rejectMessage") + )] + pub reject_message: String, +} + +fn sanitize_vec(vec: &mut [String]) -> Result<(), Box> { + let mut seen = HashSet::new(); + for item in vec.iter_mut() { + *item = item.to_lowercase(); + if !seen.insert(item.clone()) { + return Err(format!("Duplicate entry ({item}) detected, aborting...").into()); + } + } + Ok(()) +} + +fn sanitize_map_keys(map: &mut HashMap) -> Result<(), Box> { + let mut new_map = HashMap::::new(); + let mut seen = HashSet::new(); + for (k, v) in map.iter() { + let new_k = k.to_lowercase(); + new_map.insert(new_k.clone(), v.clone()); + if !seen.insert(new_k.clone()) { + return Err(format!("Duplicate entry ({new_k}) detected, aborting...").into()); + } + } + *map = new_map; + Ok(()) +} + +impl Config { + pub fn load>(path: P) -> Result> { + let file = File::open(path)?; + let reader = BufReader::new(file); + + let mut cfg: Config = serde_yml::from_reader(reader)?; + cfg.sanitize()?; + for (name, acc) in cfg.accounts.iter_mut() { + match ( + acc.hashed_password_.clone(), + acc.hashed_password_file_.clone(), + ) { + (Some(_hash), Some(_file)) => { + return Err("Account {name} has both HashedPassword and HashedPasswordFile set, aborting...".into()); + } + (Some(hash), None) => { + acc.hashed_password = hash; + } + (None, Some(file)) => { + let hash = fs::read_to_string(file.clone()).unwrap_or_else(|err| { + panic!("Account ({name}): Reading {file} failed: {err:?}") + }); + acc.hashed_password = hash.trim().to_string(); + } + (None, None) => { + return Err("Account {name} is missing HashedPassword or HashedPasswordFile, aborting...".into()); + } + } + } + Ok(cfg) + } + + fn sanitize(&mut self) -> Result<(), Box> { + // standardize capitalization, ... + sanitize_vec(&mut self.domains).unwrap_or_else(|err| panic!("domain: {err:?}")); + sanitize_map_keys(&mut self.accounts).unwrap_or_else(|err| panic!("accounts: {err:?}")); + sanitize_map_keys(&mut self.aliases).unwrap_or_else(|err| panic!("aliases: {err:?}")); + for (from, dests) in self.aliases.iter_mut() { + sanitize_vec(&mut *dests).unwrap_or_else(|err| panic!("aliases ({from}): {err:?}")); + } + Ok(()) + } + + pub fn check(&self) -> Result<(), Box> { + // check whether all account domains exist + let re = Regex::new(r"^(?P.*)@(?P.*)$").unwrap(); + for name in self.accounts.keys() { + let caps = re + .captures(name) + .ok_or("Mail address regex does not match") + .unwrap(); + if !self.domains.contains(&caps["domain"].to_string()) { + panic!("Domain of account \"{name}\" does not exist"); + } + } + + // check whether aliases have corresponding accounts/domains + for (from, dests) in self.aliases.iter() { + let is_domain = from.contains("@"); + if is_domain && !self.accounts.contains_key(from) { + panic!("Aliased from-account \"{from}\" does not exist"); + } else if !is_domain && !self.domains.contains(from) { + panic!("Aliased from-domain \"{from}\" does not exist"); + } + for dest in dests.iter() { + let is_domain = dest.contains("@"); + if is_domain && !self.accounts.contains_key(dest) { + panic!("Aliased dest-account \"{dest}\" does not exist"); + } else if !is_domain && !self.domains.contains(dest) { + panic!("Aliased dest-domain \"{dest}\" does not exist"); + } + } + } + Ok(()) + } + + pub fn merge>(&mut self, path: P) -> Result<(), Box> { + let mut other = Config::load(path).unwrap(); + + if !self + .domains + .iter() + .all(|domain| other.domains.contains(domain)) + { + return Err( + "domains: Duplicate entry during merge detected, aborting..." + .to_string() + .into(), + ); + } + self.domains.append(&mut other.domains); + + if !self + .accounts + .keys() + .all(|name| other.accounts.contains_key(name)) + { + return Err( + "accounts: Duplicate entry during merge detected, aborting..." + .to_string() + .into(), + ); + } + self.accounts.extend(other.accounts); + + if !self + .aliases + .keys() + .all(|alias| other.aliases.contains_key(alias)) + { + return Err( + "aliases: Duplicate entry during merge detected, aborting..." + .to_string() + .into(), + ); + } + self.aliases.extend(other.aliases); + + Ok(()) + } +} diff --git a/pkgs/mailnix/src/dovecot.rs b/pkgs/mailnix/src/dovecot.rs new file mode 100644 index 0000000..65b3daf --- /dev/null +++ b/pkgs/mailnix/src/dovecot.rs @@ -0,0 +1,55 @@ +use regex::Regex; +use std::collections::HashMap; +use std::error::Error; +use std::fs; +use std::path::Path; + +use crate::config::{AccountConfig, Config}; + +pub fn generate_userdb(cfg: Config) { + for name in cfg.accounts.into_keys() { + println!("{}:::::::", name); + } +} + +pub fn generate_static_passdb(cfg: Config) { + let system_accounts = cfg + .accounts + .into_iter() + .filter(|(_, acc)| acc.is_system_user); + for (name, _) in system_accounts { + println!("{}:::::::", name); + } +} + +pub fn update_dynamic_passdb>(cfg: Config, path: P) -> Result<(), Box> { + // create hashmap of all accounts with their initial passdb-lines + let mut accounts: HashMap = cfg + .accounts + .into_iter() + .filter(|(_, acc)| !acc.is_system_user) + .collect(); + eprintln!("settings: {:#?}", accounts); + + // load current passdb and update account password hashes + if path.as_ref().exists() { + let re = Regex::new(r"^(?P.*):(?P.*)::::::$").unwrap(); + + let curr_passdb = fs::read_to_string(path.as_ref()).unwrap(); + eprintln!("current passdb: {curr_passdb}"); + for line in curr_passdb.lines() { + //let caps = re.captures(line).ok_or_else(panic!("Regex does not match").unwrap(); + let caps = re + .captures(line) + .unwrap_or_else(|| panic!("Regex does not match line: {line}")); + accounts.entry(caps["name"].to_string()).and_modify(|e| { + e.hashed_password = caps["hashed_password"].to_string(); + }); + } + } + + for (name, acc) in accounts.into_iter() { + println!("{}:{}::::::", name, acc.hashed_password); + } + Ok(()) +} diff --git a/pkgs/mailnix/src/main.rs b/pkgs/mailnix/src/main.rs new file mode 100644 index 0000000..86847ab --- /dev/null +++ b/pkgs/mailnix/src/main.rs @@ -0,0 +1,33 @@ +mod cli; +mod config; +mod dovecot; +mod postfix; + +use crate::{ + cli::{Cli, Commands}, + config::Config, +}; + +use clap::Parser; + +fn main() { + let args = Cli::parse(); + let mut cfg = Config::load(args.config_path).unwrap(); + if let Some(additional_cfg_path) = args.additional_config_path { + cfg.merge(additional_cfg_path).unwrap(); + } + let cfg = cfg; + cfg.check().unwrap(); + + match &args.command { + Commands::Check => println!("Check: {:#?}", cfg), + Commands::GenerateUserdb => dovecot::generate_userdb(cfg), + Commands::GenerateStaticPassdb => dovecot::generate_static_passdb(cfg), + Commands::UpdateDynamicPassdb { path } => { + dovecot::update_dynamic_passdb(cfg, path).unwrap() + } + Commands::GenerateAliases => postfix::generate_aliases(cfg), + Commands::GenerateDeniedRecipients => postfix::generate_denied_recipients(cfg), + Commands::GenerateDomains => postfix::generate_domains(cfg), + } +} diff --git a/pkgs/mailnix/src/postfix.rs b/pkgs/mailnix/src/postfix.rs new file mode 100644 index 0000000..d7b456b --- /dev/null +++ b/pkgs/mailnix/src/postfix.rs @@ -0,0 +1,32 @@ +use crate::config::Config; + +fn prefix_domain(value: String) -> String { + if !value.contains("@") { + return format!("@{value}"); + } + value +} + +pub fn generate_aliases(cfg: Config) { + for name in cfg.accounts.into_keys() { + println!("{} {}", name, name); + } + for (src, dests) in cfg.aliases.into_iter() { + let dests: Vec<_> = dests.into_iter().map(prefix_domain).collect(); + println!("{} {}", prefix_domain(src), dests.join(", ")); + } +} + +pub fn generate_denied_recipients(cfg: Config) { + let system_accounts = cfg + .accounts + .into_iter() + .filter(|(_, acc)| acc.is_system_user); + for (name, acc) in system_accounts { + println!("{} REJECT {}", name, acc.reject_message); + } +} + +pub fn generate_domains(cfg: Config) { + println!("{}", cfg.domains.into_iter().collect::>().join("\n")); +}