├── src ├── report │ ├── mod.rs │ ├── models.rs │ ├── helpers.rs │ └── generator.rs ├── api │ ├── mod.rs │ ├── models.rs │ └── client.rs ├── config │ ├── mod.rs │ ├── validation.rs │ └── models.rs ├── main.rs └── error │ └── mod.rs ├── .gitignore ├── .github └── workflows │ ├── build-release.yml │ └── run-tests.yml ├── LICENSE ├── Cargo.toml ├── flake.nix ├── config.sample.toml ├── flake.lock ├── deny.toml ├── README.md ├── templates ├── countries.json └── email.html └── Cargo.lock /src/report/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod generator; 2 | pub mod helpers; 3 | pub mod models; 4 | -------------------------------------------------------------------------------- /src/api/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod client; 2 | pub mod models; 3 | 4 | pub use client::UmamiClient; 5 | -------------------------------------------------------------------------------- /src/report/models.rs: -------------------------------------------------------------------------------- 1 | use crate::api::models::{Metric, MetricValue, Stats}; 2 | use serde::Serialize; 3 | 4 | #[derive(Debug, Serialize)] 5 | pub struct ReportData { 6 | pub website_name: String, 7 | pub date: String, 8 | pub report_type: String, 9 | pub stats: Stats, 10 | pub bounce_rate: MetricValue, 11 | pub time_spent: String, 12 | pub pages: Vec, 13 | pub countries: Vec, 14 | pub browsers: Vec, 15 | pub devices: Vec, 16 | pub referrers: Vec, 17 | } 18 | -------------------------------------------------------------------------------- /src/api/models.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Clone, Deserialize, Serialize)] 4 | pub struct Stats { 5 | pub pageviews: MetricValue, 6 | pub visitors: MetricValue, 7 | pub visits: MetricValue, 8 | pub bounces: MetricValue, 9 | #[serde(rename = "totaltime")] 10 | pub total_time: MetricValue, 11 | } 12 | 13 | #[derive(Debug, Clone, Deserialize, Serialize)] 14 | pub struct MetricValue { 15 | pub value: f64, 16 | pub prev: f64, 17 | } 18 | 19 | #[derive(Debug, Clone, Deserialize, Serialize)] 20 | pub struct Metric { 21 | pub x: String, 22 | pub y: f64, 23 | } 24 | 25 | #[derive(Debug, Deserialize)] 26 | pub(crate) struct AuthResponse { 27 | pub token: String, 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # MSVC Windows builds of rustc generate these, which store debugging information 10 | *.pdb 11 | 12 | # RustRover 13 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 14 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 15 | # and can be added to the global gitignore or merged into this file. For a more nuclear 16 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 17 | #.idea 18 | 19 | # Ignore nix build results 20 | result 21 | result-* 22 | 23 | # Ignore config.toml 24 | config.toml 25 | -------------------------------------------------------------------------------- /.github/workflows/build-release.yml: -------------------------------------------------------------------------------- 1 | name: Build Release Binaries 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | release: 10 | name: Release ${{ matrix.target }} 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | include: 16 | - target: x86_64-unknown-linux-musl 17 | archive: tar.gz tar.xz 18 | - target: x86_64-apple-darwin 19 | archive: zip 20 | steps: 21 | - uses: actions/checkout@master 22 | - name: Build and release 23 | uses: rust-build/rust-build.action@v1.4.5 24 | with: 25 | TOOLCHAIN_VERSION: stable 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | RUSTTARGET: ${{ matrix.target }} 29 | EXTRA_FILES: "config.sample.toml" 30 | ARCHIVE_TYPES: ${{ matrix.archive }} 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Chinmay Pai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::OnceLock; 3 | use tracing::debug; 4 | 5 | pub mod models; 6 | pub mod validation; 7 | 8 | pub use models::{Config, WebsiteConfig}; 9 | 10 | static COUNTRY_MAP: OnceLock> = OnceLock::new(); 11 | static COUNTRY_JSON: &str = include_str!(concat!( 12 | env!("CARGO_MANIFEST_DIR"), 13 | "/templates/countries.json" 14 | )); 15 | 16 | pub async fn load_country_map() -> Result<(), crate::error::AppError> { 17 | let map: HashMap = serde_json::from_str(COUNTRY_JSON)?; 18 | 19 | // Initialize the static map 20 | COUNTRY_MAP.set(map).map_err(|_| { 21 | crate::error::AppError::Config("Failed to initialize country map".to_string()) 22 | })?; 23 | 24 | Ok(()) 25 | } 26 | 27 | pub fn get_country_name(code: &str) -> String { 28 | if code.trim() == "(Unknown)" || code.is_empty() { 29 | return "Unknown".to_string(); 30 | } 31 | 32 | COUNTRY_MAP 33 | .get() 34 | .and_then(|map| map.get(code)) 35 | .map(|name| name.to_string()) 36 | .unwrap_or_else(|| { 37 | debug!("Unknown country code: '{}'", code); 38 | code.to_string() 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "umami-alerts" 3 | version = "0.1.1" 4 | authors = ["Chinmay D. Pai "] 5 | edition = "2021" 6 | license = "MIT" 7 | description = "Daily reports generator for Umami Analytics" 8 | keywords = ["reports", "analytics", "email"] 9 | repository = "https://github.com/Thunderbottom/umami-alerts" 10 | homepage = "https://github.com/Thunderbottom/umami-alerts" 11 | readme = "README.md" 12 | include = ["src/**/*", "LICENSE*", "README.md"] 13 | publish = false 14 | 15 | [dependencies] 16 | tokio = { version = "1.43", features = ["full"] } 17 | reqwest = { version = "0.12.12", features = ["json", "rustls-tls"], default-features = false } 18 | serde = { version = "1.0.217", features = ["derive"] } 19 | serde_json = "1.0.138" 20 | toml = "0.8.20" 21 | thiserror = "2.0.11" 22 | chrono = { version = "0.4.39", features = ["serde"] } 23 | chrono-tz = "0.10.1" 24 | handlebars = { version = "6.3.0", features = ["dir_source"] } 25 | lettre = { version = "0.11.12", features = ["smtp-transport", "tokio1-rustls-tls", "builder"], default-features = false } 26 | tracing = "0.1.41" 27 | tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } 28 | futures = "0.3.31" 29 | url = "2.5.4" 30 | mockito = "1.6.1" 31 | clap = { version = "4.5.28", features = ["derive"] } 32 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Generate and send emails from Umami Analytics"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; 6 | rust-overlay = { 7 | url = "github:oxalica/rust-overlay"; 8 | inputs.nixpkgs.follows = "nixpkgs"; 9 | }; 10 | flake-utils.url = "github:numtide/flake-utils"; 11 | }; 12 | 13 | outputs = 14 | { 15 | nixpkgs, 16 | rust-overlay, 17 | flake-utils, 18 | ... 19 | }: 20 | flake-utils.lib.eachDefaultSystem ( 21 | system: 22 | let 23 | overlays = [ (import rust-overlay) ]; 24 | pkgs = import nixpkgs { 25 | inherit system overlays; 26 | }; 27 | nativeBuildInputs = with pkgs; [ 28 | pkg-config 29 | ]; 30 | buildInputs = with pkgs; [ 31 | openssl 32 | ]; 33 | in 34 | { 35 | devShells.default = pkgs.mkShell { 36 | inherit buildInputs nativeBuildInputs; 37 | 38 | packages = with pkgs; [ 39 | (pkgs.rust-bin.beta.latest.default.override { 40 | extensions = [ "rust-src" ]; 41 | }) 42 | rust-analyzer 43 | gcc 44 | ]; 45 | 46 | RUST_SRC_PATH = "${ 47 | pkgs.rust-bin.beta.latest.default.override { 48 | extensions = [ "rust-src" ]; 49 | } 50 | }/lib/rustlib/src/rust/library"; 51 | }; 52 | 53 | packages.default = 54 | let 55 | manifest = (pkgs.lib.importTOML ./Cargo.toml).package; 56 | in 57 | pkgs.rustPlatform.buildRustPackage { 58 | pname = manifest.name; 59 | version = manifest.version; 60 | src = pkgs.lib.cleanSource ./.; 61 | cargoLock.lockFile = ./Cargo.lock; 62 | 63 | inherit buildInputs nativeBuildInputs; 64 | }; 65 | } 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /config.sample.toml: -------------------------------------------------------------------------------- 1 | [app] 2 | # Enable debug logging. 3 | debug = false 4 | # Enable dry run option for testing. 5 | # Enabling this will generate the report but will not send an email. 6 | dry_run = false 7 | # The number of websites to process at once. 8 | max_concurrent_jobs = 4 9 | # The duration for which the report is generated. Can be daily or weekly. 10 | report_type = "weekly" 11 | 12 | [smtp] 13 | # SMTP Host and Port 14 | host = "smtp.example.com" 15 | port = 587 16 | # The username and password to access the SMTP 17 | username = "your-username" 18 | password = "your-password" 19 | # Sets the From field in the email report. You may add a 20 | # name here: Umami Reports 21 | from = "reports@example.com" 22 | # Enable this to skip checks for self-signed certificates 23 | skip_tls_verify = false 24 | # STARTTLS for SMTP. 25 | tls = true 26 | 27 | [websites.example] 28 | # Disable report generation for the website. 29 | # Remove this or set to false to enable processing this section. 30 | disabled = true 31 | base_url = "https://analytics.example.com" 32 | # The UUID for the website to generate the report for. 33 | # Can be found under the website settings. 34 | id = "e97f683e-12e8-4fb5-970b-f5171804fe21" 35 | # Set this to a sitename, or a URL. Whatever flaots your boat. 36 | name = "Example Website" 37 | # Umami does not have the concept of "API Keys". Instead, these keys 38 | # are generated on each login. So a username and password with access 39 | # is required here. 40 | username = "your-username" 41 | password = "your-password" 42 | # Email reports are generated and sent per website. 43 | recipients = ["user@example.com"] 44 | # The timezone to be used for the website. This impacts the report data 45 | # collected by the app. 46 | timezone = "UTC" 47 | 48 | # You may add more websites as such. 49 | [websites.example-io] 50 | base_url = "https://umami.example.com" 51 | id = "e4de62a3-d40a-40da-b900-3ea016893f38" 52 | name = "example.io" 53 | username = "umami-user" 54 | password = "hunter2" 55 | recipients = [ 56 | "user2@example.com", 57 | "user3@example.com", 58 | ] 59 | timezone = "Asia/Kolkata" 60 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1747744144, 24 | "narHash": "sha256-W7lqHp0qZiENCDwUZ5EX/lNhxjMdNapFnbErcbnP11Q=", 25 | "owner": "nixos", 26 | "repo": "nixpkgs", 27 | "rev": "2795c506fe8fb7b03c36ccb51f75b6df0ab2553f", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "nixos", 32 | "ref": "nixos-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs", 41 | "rust-overlay": "rust-overlay" 42 | } 43 | }, 44 | "rust-overlay": { 45 | "inputs": { 46 | "nixpkgs": [ 47 | "nixpkgs" 48 | ] 49 | }, 50 | "locked": { 51 | "lastModified": 1747795013, 52 | "narHash": "sha256-c7i0xJ+xFhgjO9SWHYu5dF/7lq63RPDvwKAdjc6VCE4=", 53 | "owner": "oxalica", 54 | "repo": "rust-overlay", 55 | "rev": "6b1cf12374361859242a562e1933a7930649131a", 56 | "type": "github" 57 | }, 58 | "original": { 59 | "owner": "oxalica", 60 | "repo": "rust-overlay", 61 | "type": "github" 62 | } 63 | }, 64 | "systems": { 65 | "locked": { 66 | "lastModified": 1681028828, 67 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 68 | "owner": "nix-systems", 69 | "repo": "default", 70 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 71 | "type": "github" 72 | }, 73 | "original": { 74 | "owner": "nix-systems", 75 | "repo": "default", 76 | "type": "github" 77 | } 78 | } 79 | }, 80 | "root": "root", 81 | "version": 7 82 | } 83 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | [graph] 2 | # cargo-deny is really only ever intended to run on the "normal" tier-1 targets 3 | targets = [ 4 | "x86_64-unknown-linux-gnu", 5 | "aarch64-unknown-linux-gnu", 6 | "x86_64-unknown-linux-musl", 7 | "aarch64-apple-darwin", 8 | "x86_64-apple-darwin", 9 | "x86_64-pc-windows-msvc", 10 | ] 11 | all-features = true 12 | 13 | [advisories] 14 | version = 2 15 | ignore = [] 16 | 17 | [bans] 18 | multiple-versions = "warn" 19 | wildcards = 'deny' 20 | deny = [ 21 | { crate = "git2", use-instead = "gix" }, 22 | { crate = "openssl", use-instead = "rustls" }, 23 | { crate = "openssl-sys", use-instead = "rustls" }, 24 | "libssh2-sys", 25 | { crate = "cmake", use-instead = "cc" }, 26 | { crate = "windows", reason = "bloated and unnecessary", use-instead = "ideally inline bindings, practically, windows-sys" }, 27 | ] 28 | skip = [ 29 | { crate = "hashbrown@0.14.5", reason = "gix uses this old version" }, 30 | { crate = "core-foundation@0.9.4", reason = "reqwest -> system-configuration uses this old version" }, 31 | ] 32 | skip-tree = [ 33 | { crate = "windows-sys@0.52.0", reason = "a foundational crate for many that bumps far too frequently to ever have a shared version" }, 34 | { crate = "thiserror@1.0.69", reason = "gix depends on both the 1.0 and 2.0 versions" }, 35 | ] 36 | 37 | [sources] 38 | unknown-registry = "deny" 39 | unknown-git = "deny" 40 | 41 | [licenses] 42 | # We want really high confidence when inferring licenses from text 43 | confidence-threshold = 0.93 44 | allow = [ 45 | "Apache-2.0", 46 | "Apache-2.0 WITH LLVM-exception", 47 | "CDLA-Permissive-2.0", 48 | "MIT", 49 | "MPL-2.0", 50 | "BSD-3-Clause", 51 | "ISC", 52 | "Unicode-3.0", 53 | "0BSD" 54 | ] 55 | exceptions = [ 56 | # Use exceptions for these as they only have a single user 57 | { allow = ["Zlib"], crate = "tinyvec" }, 58 | { allow = ["OpenSSL"], crate = "ring" }, 59 | ] 60 | 61 | # Sigh 62 | [[licenses.clarify]] 63 | crate = "ring" 64 | # SPDX considers OpenSSL to encompass both the OpenSSL and SSLeay licenses 65 | # https://spdx.org/licenses/OpenSSL.html 66 | # ISC - Both BoringSSL and ring use this for their new files 67 | # MIT - "Files in third_party/ have their own licenses, as described therein. The MIT 68 | # license, for third_party/fiat, which, unlike other third_party directories, is 69 | # compiled into non-test libraries, is included below." 70 | # OpenSSL - Obviously 71 | expression = "ISC AND MIT AND OpenSSL" 72 | license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }] 73 | 74 | [[licenses.clarify]] 75 | crate = "webpki" 76 | expression = "ISC" 77 | license-files = [{ path = "LICENSE", hash = 0x001c7e6c }] 78 | 79 | # Actually "ISC-style" 80 | [[licenses.clarify]] 81 | crate = "rustls-webpki" 82 | expression = "ISC" 83 | license-files = [{ path = "LICENSE", hash = 0x001c7e6c }] 84 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | # ref: https://github.com/msfjarvis/shared-workflows/blob/main/.github/workflows/test-rust-project.yml 2 | 3 | name: Run Tests 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | env: 14 | RUSTFLAGS: '-Dwarnings' 15 | CARGO_INCREMENTAL: 0 16 | CARGO_NET_RETRY: 10 17 | RUSTUP_MAX_RETRIES: 10 18 | RUST_BACKTRACE: short 19 | 20 | jobs: 21 | check-msrv: 22 | name: Check MSRV 23 | runs-on: ${{ matrix.os }} 24 | strategy: 25 | fail-fast: true 26 | matrix: 27 | os: 28 | - macos-13 29 | - ubuntu-22.04 30 | - windows-2022 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v4 34 | with: 35 | persist-credentials: false 36 | 37 | - name: Install Rust 38 | uses: dtolnay/rust-toolchain@stable 39 | with: 40 | toolchain: "1.80.0" 41 | 42 | - uses: Swatinem/rust-cache@v2 43 | with: 44 | key: cargo-cache-${{ hashFiles('Cargo.lock', 'Cargo.toml') }}-${{ matrix.os }} 45 | 46 | - name: Install cargo-hack 47 | uses: baptiste0928/cargo-install@v3 48 | with: 49 | crate: cargo-hack 50 | 51 | cargo-deny: 52 | name: Run cargo-deny 53 | runs-on: ubuntu-latest 54 | strategy: 55 | matrix: 56 | checks: 57 | - advisories 58 | - bans licenses sources 59 | continue-on-error: ${{ matrix.checks == 'advisories' }} 60 | steps: 61 | - uses: actions/checkout@v4 62 | with: 63 | persist-credentials: false 64 | 65 | - uses: EmbarkStudios/cargo-deny-action@v2 66 | with: 67 | command: check ${{ matrix.checks }} 68 | 69 | test: 70 | name: Run Checks 71 | runs-on: ${{ matrix.os }} 72 | strategy: 73 | fail-fast: false 74 | matrix: 75 | os: 76 | - macos-13 77 | - ubuntu-22.04 78 | - windows-2022 79 | rust: 80 | - beta 81 | - nightly 82 | - stable 83 | steps: 84 | - name: Checkout repository 85 | uses: actions/checkout@v4 86 | with: 87 | persist-credentials: false 88 | 89 | - uses: rui314/setup-mold@v1 90 | if: runner.os == 'Linux' 91 | with: 92 | make-default: true 93 | 94 | - name: Install Rust 95 | uses: dtolnay/rust-toolchain@stable 96 | with: 97 | toolchain: ${{ matrix.rust }} 98 | components: clippy, rustfmt 99 | 100 | - name: Install cargo-nextest 101 | uses: baptiste0928/cargo-install@v3 102 | with: 103 | crate: cargo-nextest 104 | 105 | - name: Install cargo-hack 106 | uses: baptiste0928/cargo-install@v3 107 | with: 108 | crate: cargo-hack 109 | 110 | - uses: Swatinem/rust-cache@v2 111 | with: 112 | key: cargo-cache-${{ hashFiles('Cargo.lock', 'Cargo.toml') }}-${{ matrix.os }}-${{ matrix.rust }} 113 | 114 | - name: Check formatting 115 | shell: bash 116 | run: cargo fmt -- --check 117 | 118 | - name: Setup Embark Studios lint rules 119 | shell: bash 120 | run: | 121 | mkdir .cargo 122 | curl -sL https://raw.githubusercontent.com/EmbarkStudios/rust-ecosystem/main/lints.toml > .cargo/config.toml 123 | 124 | - name: Clippy 125 | shell: bash 126 | run: cargo clippy 127 | 128 | - name: Test compilation 129 | run: cargo hack check --each-feature 130 | 131 | - name: Run tests 132 | shell: bash 133 | run: cargo nextest run --all-features 134 | -------------------------------------------------------------------------------- /src/config/validation.rs: -------------------------------------------------------------------------------- 1 | use super::models::Config; 2 | use crate::error::{AppError, Result}; 3 | use tracing::warn; 4 | 5 | #[cfg(test)] 6 | use { 7 | crate::config::models::{AppConfig, ReportType, SmtpConfig}, 8 | crate::WebsiteConfig, 9 | }; 10 | 11 | pub fn validate_config(config: &Config) -> Result<()> { 12 | // Validate SMTP configuration 13 | config.smtp.validate()?; 14 | 15 | // Validate website configurations 16 | if config.websites.is_empty() { 17 | return Err(AppError::Config("No websites configured".to_string())); 18 | } 19 | 20 | let mut has_enabled_websites = false; 21 | for (name, website) in &config.websites { 22 | if website.disabled { 23 | warn!("Website {} is disabled", name); 24 | continue; 25 | } 26 | has_enabled_websites = true; 27 | website.validate()?; 28 | } 29 | 30 | if !has_enabled_websites { 31 | return Err(AppError::Config("No enabled websites found".to_string())); 32 | } 33 | 34 | // Validate max concurrent jobs 35 | if config.app.max_concurrent_jobs == 0 { 36 | return Err(AppError::Config( 37 | "max_concurrent_jobs must be greater than 0".to_string(), 38 | )); 39 | } 40 | 41 | Ok(()) 42 | } 43 | 44 | #[cfg(test)] 45 | mod tests { 46 | use super::*; 47 | use std::collections::HashMap; 48 | 49 | fn create_test_config() -> Config { 50 | Config { 51 | smtp: SmtpConfig { 52 | host: "smtp.example.com".to_string(), 53 | port: 587, 54 | username: "test".to_string(), 55 | password: "password".to_string(), 56 | from: "test@example.com".to_string(), 57 | tls: true, 58 | timeout_seconds: 30, 59 | skip_verify: false, 60 | }, 61 | websites: { 62 | let mut map = HashMap::new(); 63 | map.insert( 64 | "test".to_string(), 65 | WebsiteConfig { 66 | id: "test-id".to_string(), 67 | name: "Test Site".to_string(), 68 | base_url: "https://analytics.example.com".to_string(), 69 | username: "test".to_string(), 70 | password: "password".to_string(), 71 | recipients: vec!["admin@example.com".to_string()], 72 | timezone: "UTC".to_string(), 73 | disabled: false, 74 | }, 75 | ); 76 | map 77 | }, 78 | app: AppConfig { 79 | debug: false, 80 | dry_run: false, 81 | max_concurrent_jobs: 4, 82 | report_type: ReportType::Daily, 83 | }, 84 | } 85 | } 86 | 87 | #[test] 88 | fn test_valid_config() { 89 | let config = create_test_config(); 90 | assert!(validate_config(&config).is_ok()); 91 | } 92 | 93 | #[test] 94 | fn test_smtp_validation() { 95 | let mut config = create_test_config(); 96 | config.smtp.host = "".to_string(); 97 | assert!(validate_config(&config).is_err()); 98 | } 99 | 100 | #[test] 101 | fn test_website_validation() { 102 | let mut config = create_test_config(); 103 | config.websites.get_mut("test").unwrap().base_url = "invalid-url".to_string(); 104 | assert!(validate_config(&config).is_err()); 105 | } 106 | 107 | #[test] 108 | fn test_timezone_validation() { 109 | let mut config = create_test_config(); 110 | config.websites.get_mut("test").unwrap().timezone = "Invalid/Timezone".to_string(); 111 | assert!(validate_config(&config).is_err()); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

umami-alerts

2 | 3 | A fast, efficient daily analytics report generator for [Umami Analytics](https://umami.is/). This tool fetches your analytics data and sends simple, detailed email reports including: 4 | 5 | - Pageviews and visitor statistics 6 | - Engagement metrics (bounce rates, time spent) 7 | - Top referrers and traffic sources 8 | - Geographic distribution of visitors 9 | - Browser and device breakdowns 10 | 11 | ## Installation 12 | 13 | ### Using Cargo 14 | 15 | ```bash 16 | # Install directly from git 17 | $ cargo install --git https://github.com/Thunderbottom/umami-alerts 18 | 19 | # Or clone and build 20 | $ git clone https://github.com/Thunderbottom/umami-alerts 21 | $ cd umami-alerts 22 | $ cargo build --release 23 | ``` 24 | 25 | ### Using Nix 26 | 27 | ```bash 28 | $ nix build github:Thunderbottom/umami-alerts 29 | $ ./results/bin/umami-alerts -c config.toml 30 | ``` 31 | 32 | ## Configuration 33 | 34 | Create a `config.toml` file: 35 | 36 | ```toml 37 | [app] 38 | debug = false 39 | dry_run = false 40 | max_concurrent_jobs = 4 41 | report_type = "weekly" 42 | 43 | [smtp] 44 | host = "smtp.example.com" 45 | port = 587 46 | username = "your-username" 47 | password = "your-password" 48 | from = "reports@example.com" 49 | skip_tls_verify = false 50 | tls = true 51 | 52 | [websites.example] 53 | disabled = true 54 | base_url = "https://analytics.example.com" 55 | id = "e97f683e-12e8-4fb5-970b-f5171804fe21" 56 | name = "Example Website" 57 | username = "your-username" 58 | password = "your-password" 59 | recipients = ["user@example.com"] 60 | timezone = "UTC" 61 | 62 | [websites.example-io] 63 | base_url = "https://umami.example.com" 64 | id = "e4de62a3-d40a-40da-b900-3ea016893f38" 65 | name = "example.io" 66 | username = "umami-user" 67 | password = "hunter2" 68 | recipients = [ 69 | "user2@example.com", 70 | "user3@example.com", 71 | ] 72 | timezone = "Asia/Kolkata" 73 | 74 | ``` 75 | 76 | You may add multiple such websites under `[websites]` as `[websites.new-example]` with the site's configuration. 77 | 78 | ## Usage 79 | 80 | ```bash 81 | # Run with default config path 82 | $ umami-alerts 83 | 84 | # Specify config path 85 | $ umami-alerts --config /path/to/config.toml 86 | ``` 87 | ### Crontab Configuration 88 | 89 | `umami-alerts` is meant to be run as an everyday-cron to send daily reports. 90 | 91 | ```bash 92 | # Add an entry to crontab to run at 8am daily 93 | 0 8 * * * /path/to/umami-alerts --config /path/to/config.toml 94 | ``` 95 | 96 | ## Development 97 | 98 | ### Prerequisites 99 | 100 | - Rust 1.70 or higher 101 | - OpenSSL development libraries 102 | - GCC or compatible C compiler 103 | - pkg-config 104 | 105 | ### Building from Source 106 | 107 | ```bash 108 | # Using cargo 109 | $ cargo build --release 110 | 111 | # Using nix develop 112 | $ nix develop 113 | (nix shell) $ cargo build --release 114 | ``` 115 | 116 | ### Running Tests 117 | 118 | ```bash 119 | cargo test 120 | ``` 121 | 122 | ## Contributing 123 | 124 | 1. Fork the repository 125 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 126 | 3. Commit your changes (`git commit -m 'Add some amazing feature'`) 127 | 4. Push to the branch (`git push origin feature/amazing-feature`) 128 | 5. Open a Pull Request 129 | 130 | ## License 131 | 132 | MIT License 133 | 134 | ``` 135 | Copyright (c) 2025 Chinmay D. Pai 136 | 137 | Permission is hereby granted, free of charge, to any person obtaining a copy 138 | of this software and associated documentation files (the "Software"), to deal 139 | in the Software without restriction, including without limitation the rights 140 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 141 | copies of the Software, and to permit persons to whom the Software is 142 | furnished to do so, subject to the following conditions: 143 | 144 | The above copyright notice and this permission notice shall be included in all 145 | copies or substantial portions of the Software. 146 | 147 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 148 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 149 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 150 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 151 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 152 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 153 | SOFTWARE. 154 | ``` 155 | -------------------------------------------------------------------------------- /src/report/helpers.rs: -------------------------------------------------------------------------------- 1 | use handlebars::{ 2 | Context, Handlebars, Helper, HelperResult, Output, RenderContext, RenderError, 3 | RenderErrorReason, 4 | }; 5 | 6 | /// Format a number with thousands separators 7 | pub fn format_number( 8 | h: &Helper, 9 | _: &Handlebars, 10 | _: &Context, 11 | _: &mut RenderContext, 12 | out: &mut dyn Output, 13 | ) -> HelperResult { 14 | let param = h.param(0).ok_or_else(|| { 15 | RenderError::from(RenderErrorReason::ParamNotFoundForName( 16 | "format_number", 17 | "first parameter".to_string(), 18 | )) 19 | })?; 20 | 21 | let number = param.value().as_f64().ok_or_else(|| { 22 | RenderError::from(RenderErrorReason::Other( 23 | "Parameter must be a number".to_string(), 24 | )) 25 | })?; 26 | 27 | out.write(&format!("{}", number as i64))?; 28 | Ok(()) 29 | } 30 | 31 | /// Calculate and format time spent per visit 32 | pub fn format_time_spent(total_time: f64, visits: f64) -> String { 33 | if visits <= 0.0 { 34 | return "0m 0s".to_string(); 35 | } 36 | 37 | let seconds = total_time / visits; 38 | let minutes = (seconds / 60.0) as i64; 39 | let remaining_seconds = (seconds % 60.0) as i64; 40 | 41 | if minutes > 0 { 42 | format!("{minutes}m {remaining_seconds}s") 43 | } else { 44 | format!("{remaining_seconds}s") 45 | } 46 | } 47 | 48 | /// Calculate percentage and ensure it's between 0-100 49 | pub fn percentage( 50 | h: &Helper, 51 | _: &Handlebars, 52 | _: &Context, 53 | _: &mut RenderContext, 54 | out: &mut dyn Output, 55 | ) -> HelperResult { 56 | let value = h.param(0).and_then(|v| v.value().as_f64()).unwrap_or(0.0); 57 | let total = h.param(1).and_then(|v| v.value().as_f64()).unwrap_or(1.0); 58 | 59 | let percentage = if total > 0.0 { 60 | (value / total * 100.0).clamp(0.0, 100.0) 61 | } else { 62 | 0.0 63 | }; 64 | 65 | out.write(&format!("{percentage:.1}"))?; 66 | Ok(()) 67 | } 68 | 69 | /// Format a float with specified decimal places 70 | pub fn format_float( 71 | h: &Helper, 72 | _: &Handlebars, 73 | _: &Context, 74 | _: &mut RenderContext, 75 | out: &mut dyn Output, 76 | ) -> HelperResult { 77 | let param = h.param(0).ok_or_else(|| { 78 | RenderError::from(RenderErrorReason::ParamNotFoundForName( 79 | "format_float", 80 | "first parameter".to_string(), 81 | )) 82 | })?; 83 | 84 | let number = param.value().as_f64().ok_or_else(|| { 85 | RenderError::from(RenderErrorReason::Other( 86 | "Parameter must be a number".to_string(), 87 | )) 88 | })?; 89 | 90 | let decimals = h.param(1).and_then(|v| v.value().as_u64()).unwrap_or(2) as usize; 91 | 92 | out.write(&format!("{number:.decimals$}"))?; 93 | Ok(()) 94 | } 95 | 96 | #[cfg(test)] 97 | mod tests { 98 | use super::*; 99 | 100 | #[test] 101 | fn test_format_time_spent() { 102 | assert_eq!(format_time_spent(0.0, 0.0), "0m 0s"); 103 | assert_eq!(format_time_spent(30.0, 1.0), "30s"); 104 | assert_eq!(format_time_spent(90.0, 1.0), "1m 30s"); 105 | assert_eq!(format_time_spent(3600.0, 1.0), "60m 0s"); 106 | } 107 | 108 | #[test] 109 | fn test_handlebars_helpers() { 110 | let mut handlebars = Handlebars::new(); 111 | handlebars.register_helper("formatNumber", Box::new(format_number)); 112 | handlebars.register_helper("percentage", Box::new(percentage)); 113 | handlebars.register_helper("formatFloat", Box::new(format_float)); 114 | 115 | // Test formatNumber 116 | let template = "{{formatNumber number}}"; 117 | let mut data = serde_json::json!({"number": 1234.56}); 118 | assert_eq!(handlebars.render_template(template, &data).unwrap(), "1234"); 119 | 120 | // Test percentage 121 | let template = "{{percentage value total}}"; 122 | data = serde_json::json!({ 123 | "value": 25.0, 124 | "total": 100.0 125 | }); 126 | assert_eq!(handlebars.render_template(template, &data).unwrap(), "25.0"); 127 | 128 | // Test formatFloat 129 | let template = "{{formatFloat number 3}}"; 130 | data = serde_json::json!({"number": 1234.5678}); 131 | assert_eq!( 132 | handlebars.render_template(template, &data).unwrap(), 133 | "1234.568" 134 | ); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use futures::stream::{self, StreamExt}; 3 | use std::path::PathBuf; 4 | use std::sync::Arc; 5 | use tokio::sync::Semaphore; 6 | use tracing::{debug, error, info, Level}; 7 | 8 | mod api; 9 | mod config; 10 | mod error; 11 | mod report; 12 | 13 | use crate::{ 14 | api::UmamiClient, 15 | config::{validation::validate_config, Config, WebsiteConfig}, 16 | error::{AppError, Result}, 17 | report::generator::ReportGenerator, 18 | }; 19 | 20 | #[derive(Clone)] 21 | struct AppState { 22 | config: Arc, 23 | report_generator: Arc, 24 | semaphore: Arc, 25 | } 26 | 27 | #[derive(Parser, Debug)] 28 | #[command(author, version, about, long_about = None)] 29 | struct Args { 30 | /// Path to the configuration file 31 | #[arg(short, long, default_value = "config.toml")] 32 | config: PathBuf, 33 | } 34 | 35 | #[tokio::main] 36 | async fn main() -> Result<()> { 37 | let args = Args::parse(); 38 | 39 | // Check if config file exists 40 | if !args.config.exists() { 41 | return Err(format!("Config file not found: {}", args.config.display()).into()); 42 | } 43 | // Load configuration 44 | let config = Config::load(&args.config).await?; 45 | validate_config(&config) 46 | .map_err(|e| AppError::api(format!("Config validation failed: {e}")))?; 47 | let max_concurrent_jobs = config.app.max_concurrent_jobs; 48 | 49 | let log_level = if config.app.debug { 50 | Level::DEBUG 51 | } else { 52 | Level::INFO 53 | }; 54 | 55 | // Initialize logging 56 | tracing_subscriber::fmt() 57 | .with_max_level(log_level) 58 | .with_file(true) 59 | .with_line_number(true) 60 | .with_thread_ids(true) 61 | .with_target(false) 62 | .init(); 63 | 64 | info!("Starting umami-alerts"); 65 | debug!("Debug mode enabled"); 66 | debug!("Report type: {:?}", config.app.report_type); 67 | 68 | config::load_country_map().await?; 69 | info!("Loaded country mappings"); 70 | 71 | // Initialize template engine 72 | let mut handlebars = handlebars::Handlebars::new(); 73 | handlebars.register_template_string( 74 | "email", 75 | include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/email.html")), 76 | )?; 77 | handlebars.register_helper("formatNumber", Box::new(report::helpers::format_number)); 78 | handlebars.register_helper("percentage", Box::new(report::helpers::percentage)); 79 | handlebars.register_helper("formatFloat", Box::new(report::helpers::format_float)); 80 | handlebars.set_strict_mode(false); 81 | handlebars::handlebars_helper!(sub: |x:f64, y:f64| x - y); 82 | handlebars.register_helper("sub", Box::new(sub)); 83 | let handlebars = Arc::new(handlebars); 84 | 85 | // Create application state 86 | let state = AppState { 87 | config: Arc::new(config), 88 | report_generator: Arc::new(ReportGenerator::new(handlebars)), 89 | semaphore: Arc::new(Semaphore::new(max_concurrent_jobs)), 90 | }; 91 | 92 | // Process all enabled websites concurrently 93 | let results = stream::iter(state.config.enabled_websites()) 94 | .map(|(name, website)| { 95 | let state = state.clone(); 96 | async move { 97 | let _permit = state.semaphore.acquire().await.unwrap(); 98 | match process_website(&state, name, website).await { 99 | Ok(_) => Ok(name.to_string()), 100 | Err(e) => Err((name.to_string(), e)), 101 | } 102 | } 103 | }) 104 | .buffer_unordered(max_concurrent_jobs) 105 | .collect::>() 106 | .await; 107 | 108 | // Report results 109 | let (successes, failures): (Vec<_>, Vec<_>) = results.into_iter().partition(|r| r.is_ok()); 110 | 111 | info!( 112 | "Processing complete. {} succeeded, {} failed", 113 | successes.len(), 114 | failures.len() 115 | ); 116 | 117 | if !failures.is_empty() { 118 | let failed_sites: Vec<_> = failures 119 | .iter() 120 | .map(|r| r.as_ref().unwrap_err().0.clone()) 121 | .collect(); 122 | error!("Failed websites: {}", failed_sites.join(", ")); 123 | return Err(AppError::task(format!( 124 | "Failed to process {} websites", 125 | failures.len() 126 | ))); 127 | } 128 | 129 | Ok(()) 130 | } 131 | 132 | async fn process_website(state: &AppState, site_name: &str, website: &WebsiteConfig) -> Result<()> { 133 | info!("Processing website: {}", site_name); 134 | 135 | // Create API client 136 | let client = UmamiClient::new(website.base_url.clone())?; 137 | 138 | // Authenticate 139 | let token = client 140 | .authenticate(&website.username, &website.password) 141 | .await?; 142 | 143 | // Generate and send report 144 | state 145 | .report_generator 146 | .generate_and_send( 147 | &client, 148 | &state.config.app.dry_run, 149 | website, 150 | &state.config.app.report_type, 151 | &state.config.smtp, 152 | &token, 153 | ) 154 | .await?; 155 | 156 | Ok(()) 157 | } 158 | -------------------------------------------------------------------------------- /src/config/models.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::collections::HashMap; 3 | use std::fmt; 4 | use std::path::Path; 5 | use tokio::fs; 6 | use url::Url; 7 | 8 | use crate::error::{AppError, Result}; 9 | 10 | #[derive(Debug, Clone, Deserialize, Serialize)] 11 | pub struct Config { 12 | pub smtp: SmtpConfig, 13 | pub websites: HashMap, 14 | #[serde(default)] 15 | pub app: AppConfig, 16 | } 17 | 18 | #[derive(Debug, Clone, Deserialize, Serialize)] 19 | pub struct AppConfig { 20 | #[serde(default)] 21 | pub debug: bool, 22 | #[serde(default)] 23 | pub dry_run: bool, 24 | #[serde(default = "default_max_concurrent_jobs")] 25 | pub max_concurrent_jobs: usize, 26 | #[serde(default = "default_report_type")] 27 | pub report_type: ReportType, 28 | } 29 | 30 | #[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq)] 31 | #[serde(rename_all = "lowercase")] 32 | pub enum ReportType { 33 | Daily, 34 | Weekly, 35 | } 36 | 37 | impl fmt::Display for ReportType { 38 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 39 | match self { 40 | ReportType::Daily => write!(f, "Daily"), 41 | ReportType::Weekly => write!(f, "Weekly"), 42 | } 43 | } 44 | } 45 | 46 | fn default_max_concurrent_jobs() -> usize { 47 | 4 48 | } 49 | 50 | fn default_report_type() -> ReportType { 51 | ReportType::Daily 52 | } 53 | 54 | impl Default for AppConfig { 55 | fn default() -> Self { 56 | Self { 57 | debug: false, 58 | dry_run: false, 59 | max_concurrent_jobs: default_max_concurrent_jobs(), 60 | report_type: default_report_type(), 61 | } 62 | } 63 | } 64 | 65 | #[derive(Debug, Clone, Deserialize, Serialize)] 66 | pub struct SmtpConfig { 67 | pub host: String, 68 | pub port: u16, 69 | pub username: String, 70 | pub password: String, 71 | pub from: String, 72 | #[serde(default = "default_tls")] 73 | pub tls: bool, 74 | #[serde(default = "default_timeout")] 75 | pub timeout_seconds: u64, 76 | #[serde(default)] 77 | pub skip_verify: bool, 78 | } 79 | 80 | fn default_timeout() -> u64 { 81 | 30 82 | } 83 | 84 | fn default_tls() -> bool { 85 | true 86 | } 87 | 88 | #[derive(Debug, Clone, Deserialize, Serialize)] 89 | pub struct WebsiteConfig { 90 | pub id: String, 91 | pub name: String, 92 | pub base_url: String, 93 | pub username: String, 94 | pub password: String, 95 | pub recipients: Vec, 96 | #[serde(default = "default_timezone")] 97 | pub timezone: String, 98 | #[serde(default)] 99 | pub disabled: bool, 100 | } 101 | 102 | fn default_timezone() -> String { 103 | "UTC".to_string() 104 | } 105 | 106 | impl Config { 107 | /// Load configuration from a TOML file 108 | pub async fn load(path: &Path) -> Result { 109 | let content = fs::read_to_string(path).await?; 110 | let config = toml::from_str(&content)?; 111 | tracing::info!("Configuration loaded successfully"); 112 | Ok(config) 113 | } 114 | 115 | /// Get all enabled websites 116 | pub fn enabled_websites(&self) -> impl Iterator { 117 | self.websites.iter().filter(|(_, config)| !config.disabled) 118 | } 119 | } 120 | 121 | impl SmtpConfig { 122 | /// Validate SMTP configuration 123 | pub fn validate(&self) -> Result<()> { 124 | if self.host.is_empty() { 125 | return Err(AppError::Config("SMTP host cannot be empty".to_string())); 126 | } 127 | 128 | if self.port == 0 { 129 | return Err(AppError::Config("Invalid SMTP port".to_string())); 130 | } 131 | 132 | if self.username.is_empty() { 133 | return Err(AppError::Config( 134 | "SMTP username cannot be empty".to_string(), 135 | )); 136 | } 137 | 138 | if self.password.is_empty() { 139 | return Err(AppError::Config( 140 | "SMTP password cannot be empty".to_string(), 141 | )); 142 | } 143 | 144 | if !self.from.contains('@') { 145 | return Err(AppError::Config("Invalid SMTP from address".to_string())); 146 | } 147 | 148 | Ok(()) 149 | } 150 | } 151 | 152 | impl WebsiteConfig { 153 | /// Validate website configuration 154 | pub fn validate(&self) -> Result<()> { 155 | if self.id.is_empty() { 156 | return Err(AppError::Config("Website ID cannot be empty".to_string())); 157 | } 158 | 159 | if self.name.is_empty() { 160 | return Err(AppError::Config("Website name cannot be empty".to_string())); 161 | } 162 | 163 | // Validate base URL 164 | Url::parse(&self.base_url) 165 | .map_err(|e| AppError::Config(format!("Invalid base URL {}: {}", self.base_url, e)))?; 166 | 167 | if self.username.is_empty() { 168 | return Err(AppError::Config("Username cannot be empty".to_string())); 169 | } 170 | 171 | if self.password.is_empty() { 172 | return Err(AppError::Config("Password cannot be empty".to_string())); 173 | } 174 | 175 | if self.recipients.is_empty() { 176 | return Err(AppError::Config( 177 | "At least one recipient is required".to_string(), 178 | )); 179 | } 180 | 181 | // Validate email addresses 182 | for recipient in &self.recipients { 183 | if !recipient.contains('@') { 184 | return Err(AppError::Config(format!( 185 | "Invalid email address: {recipient}" 186 | ))); 187 | } 188 | } 189 | 190 | // Validate timezone 191 | self.timezone 192 | .parse::() 193 | .map_err(|e| AppError::Config(format!("Invalid timezone {}: {}", self.timezone, e)))?; 194 | 195 | Ok(()) 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /templates/countries.json: -------------------------------------------------------------------------------- 1 | { 2 | "AF": "Afghanistan", 3 | "AX": "Åland Islands", 4 | "AL": "Albania", 5 | "DZ": "Algeria", 6 | "AS": "American Samoa", 7 | "AD": "Andorra", 8 | "AO": "Angola", 9 | "AI": "Anguilla", 10 | "AQ": "Antarctica", 11 | "AG": "Antigua & Barbuda", 12 | "AR": "Argentina", 13 | "AM": "Armenia", 14 | "AW": "Aruba", 15 | "AU": "Australia", 16 | "AT": "Austria", 17 | "AZ": "Azerbaijan", 18 | "BS": "Bahamas", 19 | "BH": "Bahrain", 20 | "BD": "Bangladesh", 21 | "BB": "Barbados", 22 | "BY": "Belarus", 23 | "BE": "Belgium", 24 | "BZ": "Belize", 25 | "BJ": "Benin", 26 | "BM": "Bermuda", 27 | "BT": "Bhutan", 28 | "BO": "Bolivia", 29 | "BA": "Bosnia & Herzegovina", 30 | "BW": "Botswana", 31 | "BV": "Bouvet Island", 32 | "BR": "Brazil", 33 | "IO": "British Indian Ocean Territory", 34 | "VG": "British Virgin Islands", 35 | "BN": "Brunei", 36 | "BG": "Bulgaria", 37 | "BF": "Burkina Faso", 38 | "BI": "Burundi", 39 | "KH": "Cambodia", 40 | "CM": "Cameroon", 41 | "CA": "Canada", 42 | "CV": "Cape Verde", 43 | "BQ": "Caribbean Netherlands", 44 | "KY": "Cayman Islands", 45 | "CF": "Central African Republic", 46 | "TD": "Chad", 47 | "CL": "Chile", 48 | "CN": "China", 49 | "CX": "Christmas Island", 50 | "CC": "Cocos (Keeling) Islands", 51 | "CO": "Colombia", 52 | "KM": "Comoros", 53 | "CG": "Congo - Brazzaville", 54 | "CD": "Congo - Kinshasa", 55 | "CK": "Cook Islands", 56 | "CR": "Costa Rica", 57 | "CI": "Côte d’Ivoire", 58 | "HR": "Croatia", 59 | "CU": "Cuba", 60 | "CW": "Curaçao", 61 | "CY": "Cyprus", 62 | "CZ": "Czechia", 63 | "DK": "Denmark", 64 | "DJ": "Djibouti", 65 | "DM": "Dominica", 66 | "DO": "Dominican Republic", 67 | "EC": "Ecuador", 68 | "EG": "Egypt", 69 | "SV": "El Salvador", 70 | "GQ": "Equatorial Guinea", 71 | "ER": "Eritrea", 72 | "EE": "Estonia", 73 | "SZ": "Eswatini", 74 | "ET": "Ethiopia", 75 | "FK": "Falkland Islands", 76 | "FO": "Faroe Islands", 77 | "FJ": "Fiji", 78 | "FI": "Finland", 79 | "FR": "France", 80 | "GF": "French Guiana", 81 | "PF": "French Polynesia", 82 | "TF": "French Southern Territories", 83 | "GA": "Gabon", 84 | "GM": "Gambia", 85 | "GE": "Georgia", 86 | "DE": "Germany", 87 | "GH": "Ghana", 88 | "GI": "Gibraltar", 89 | "GR": "Greece", 90 | "GL": "Greenland", 91 | "GD": "Grenada", 92 | "GP": "Guadeloupe", 93 | "GU": "Guam", 94 | "GT": "Guatemala", 95 | "GG": "Guernsey", 96 | "GN": "Guinea", 97 | "GW": "Guinea-Bissau", 98 | "GY": "Guyana", 99 | "HT": "Haiti", 100 | "HM": "Heard & McDonald Islands", 101 | "HN": "Honduras", 102 | "HK": "Hong Kong SAR China", 103 | "HU": "Hungary", 104 | "IS": "Iceland", 105 | "IN": "India", 106 | "ID": "Indonesia", 107 | "IR": "Iran", 108 | "IQ": "Iraq", 109 | "IE": "Ireland", 110 | "IM": "Isle of Man", 111 | "IL": "Israel", 112 | "IT": "Italy", 113 | "JM": "Jamaica", 114 | "JP": "Japan", 115 | "JE": "Jersey", 116 | "JO": "Jordan", 117 | "KZ": "Kazakhstan", 118 | "KE": "Kenya", 119 | "KI": "Kiribati", 120 | "KW": "Kuwait", 121 | "KG": "Kyrgyzstan", 122 | "LA": "Laos", 123 | "LV": "Latvia", 124 | "LB": "Lebanon", 125 | "LS": "Lesotho", 126 | "LR": "Liberia", 127 | "LY": "Libya", 128 | "LI": "Liechtenstein", 129 | "LT": "Lithuania", 130 | "LU": "Luxembourg", 131 | "MO": "Macao SAR China", 132 | "MG": "Madagascar", 133 | "MW": "Malawi", 134 | "MY": "Malaysia", 135 | "MV": "Maldives", 136 | "ML": "Mali", 137 | "MT": "Malta", 138 | "MH": "Marshall Islands", 139 | "MQ": "Martinique", 140 | "MR": "Mauritania", 141 | "MU": "Mauritius", 142 | "YT": "Mayotte", 143 | "MX": "Mexico", 144 | "FM": "Micronesia", 145 | "MD": "Moldova", 146 | "MC": "Monaco", 147 | "MN": "Mongolia", 148 | "ME": "Montenegro", 149 | "MS": "Montserrat", 150 | "MA": "Morocco", 151 | "MZ": "Mozambique", 152 | "MM": "Myanmar (Burma)", 153 | "NA": "Namibia", 154 | "NR": "Nauru", 155 | "NP": "Nepal", 156 | "NL": "Netherlands", 157 | "NC": "New Caledonia", 158 | "NZ": "New Zealand", 159 | "NI": "Nicaragua", 160 | "NE": "Niger", 161 | "NG": "Nigeria", 162 | "NU": "Niue", 163 | "NF": "Norfolk Island", 164 | "KP": "North Korea", 165 | "MK": "North Macedonia", 166 | "MP": "Northern Mariana Islands", 167 | "NO": "Norway", 168 | "OM": "Oman", 169 | "PK": "Pakistan", 170 | "PW": "Palau", 171 | "PS": "Palestinian Territories", 172 | "PA": "Panama", 173 | "PG": "Papua New Guinea", 174 | "PY": "Paraguay", 175 | "PE": "Peru", 176 | "PH": "Philippines", 177 | "PN": "Pitcairn Islands", 178 | "PL": "Poland", 179 | "PT": "Portugal", 180 | "PR": "Puerto Rico", 181 | "QA": "Qatar", 182 | "RE": "Réunion", 183 | "RO": "Romania", 184 | "RU": "Russia", 185 | "RW": "Rwanda", 186 | "WS": "Samoa", 187 | "SM": "San Marino", 188 | "ST": "São Tomé & Príncipe", 189 | "SA": "Saudi Arabia", 190 | "SN": "Senegal", 191 | "RS": "Serbia", 192 | "SC": "Seychelles", 193 | "SL": "Sierra Leone", 194 | "SG": "Singapore", 195 | "SX": "Sint Maarten", 196 | "SK": "Slovakia", 197 | "SI": "Slovenia", 198 | "SB": "Solomon Islands", 199 | "SO": "Somalia", 200 | "ZA": "South Africa", 201 | "GS": "South Georgia & South Sandwich Islands", 202 | "KR": "South Korea", 203 | "SS": "South Sudan", 204 | "ES": "Spain", 205 | "LK": "Sri Lanka", 206 | "BL": "St. Barthélemy", 207 | "SH": "St. Helena", 208 | "KN": "St. Kitts & Nevis", 209 | "LC": "St. Lucia", 210 | "MF": "St. Martin", 211 | "PM": "St. Pierre & Miquelon", 212 | "VC": "St. Vincent & Grenadines", 213 | "SD": "Sudan", 214 | "SR": "Suriname", 215 | "SJ": "Svalbard & Jan Mayen", 216 | "SE": "Sweden", 217 | "CH": "Switzerland", 218 | "SY": "Syria", 219 | "TW": "Taiwan", 220 | "TJ": "Tajikistan", 221 | "TZ": "Tanzania", 222 | "TH": "Thailand", 223 | "TL": "Timor-Leste", 224 | "TG": "Togo", 225 | "TK": "Tokelau", 226 | "TO": "Tonga", 227 | "TT": "Trinidad & Tobago", 228 | "TN": "Tunisia", 229 | "TR": "Turkey", 230 | "TM": "Turkmenistan", 231 | "TC": "Turks & Caicos Islands", 232 | "TV": "Tuvalu", 233 | "UM": "U.S. Outlying Islands", 234 | "VI": "U.S. Virgin Islands", 235 | "UG": "Uganda", 236 | "UA": "Ukraine", 237 | "AE": "United Arab Emirates", 238 | "GB": "United Kingdom", 239 | "US": "United States", 240 | "UY": "Uruguay", 241 | "UZ": "Uzbekistan", 242 | "VU": "Vanuatu", 243 | "VA": "Vatican City", 244 | "VE": "Venezuela", 245 | "VN": "Vietnam", 246 | "WF": "Wallis & Futuna", 247 | "EH": "Western Sahara", 248 | "YE": "Yemen", 249 | "ZM": "Zambia", 250 | "ZW": "Zimbabwe" 251 | } 252 | -------------------------------------------------------------------------------- /src/error/mod.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error as StdError; 2 | use std::fmt; 3 | 4 | pub type Result = std::result::Result; 5 | 6 | #[derive(Debug)] 7 | pub enum AppError { 8 | Io(std::io::Error), 9 | Config(String), 10 | Api(String), 11 | Template(String), 12 | Smtp(String), 13 | Json(serde_json::Error), 14 | Toml(toml::de::Error), 15 | Request(reqwest::Error), 16 | Email(String), 17 | Handlebars(handlebars::RenderError), 18 | Url(url::ParseError), 19 | Task(String), 20 | } 21 | 22 | impl StdError for AppError { 23 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 24 | match self { 25 | Self::Io(e) => Some(e), 26 | // Self::TimeZone(e) => Some(e), // chrono_tz::ParseError implements StdError 27 | Self::Json(e) => Some(e), 28 | Self::Toml(e) => Some(e), 29 | Self::Request(e) => Some(e), 30 | Self::Handlebars(e) => Some(e), 31 | Self::Url(e) => Some(e), 32 | // String-based variants don't have a source 33 | Self::Config(_) 34 | | Self::Api(_) 35 | | Self::Template(_) 36 | | Self::Smtp(_) 37 | | Self::Email(_) 38 | | Self::Task(_) => None, 39 | } 40 | } 41 | } 42 | 43 | impl fmt::Display for AppError { 44 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 45 | match self { 46 | Self::Io(e) => write!(f, "IO error: {e}"), 47 | Self::Config(msg) => write!(f, "Configuration error: {msg}"), 48 | Self::Api(msg) => write!(f, "API error: {msg}"), 49 | Self::Template(msg) => write!(f, "Template error: {msg}"), 50 | Self::Smtp(msg) => write!(f, "SMTP error: {msg}"), 51 | Self::Json(e) => write!(f, "JSON error: {e}"), 52 | Self::Toml(e) => write!(f, "TOML error: {e}"), 53 | Self::Request(e) => write!(f, "Request error: {e}"), 54 | Self::Email(msg) => write!(f, "Email error: {msg}"), 55 | Self::Handlebars(e) => write!(f, "Handlebars error: {e}"), 56 | Self::Url(e) => write!(f, "URL parsing error: {e}"), 57 | Self::Task(msg) => write!(f, "Task error: {msg}"), 58 | } 59 | } 60 | } 61 | 62 | // Implement conversions from other error types 63 | impl From for AppError { 64 | fn from(err: std::io::Error) -> Self { 65 | Self::Io(err) 66 | } 67 | } 68 | 69 | impl From for AppError { 70 | fn from(err: reqwest::Error) -> Self { 71 | Self::Request(err) 72 | } 73 | } 74 | 75 | impl From for AppError { 76 | fn from(err: serde_json::Error) -> Self { 77 | Self::Json(err) 78 | } 79 | } 80 | 81 | impl From for AppError { 82 | fn from(err: toml::de::Error) -> Self { 83 | Self::Toml(err) 84 | } 85 | } 86 | 87 | impl From for AppError { 88 | fn from(err: url::ParseError) -> Self { 89 | Self::Url(err) 90 | } 91 | } 92 | 93 | impl From for AppError { 94 | fn from(err: handlebars::RenderError) -> Self { 95 | Self::Handlebars(err) 96 | } 97 | } 98 | 99 | impl From for AppError { 100 | fn from(err: handlebars::TemplateError) -> Self { 101 | Self::Template(err.to_string()) 102 | } 103 | } 104 | 105 | impl From for AppError { 106 | fn from(err: lettre::error::Error) -> Self { 107 | Self::Smtp(err.to_string()) 108 | } 109 | } 110 | 111 | impl From for AppError { 112 | fn from(err: lettre::transport::smtp::Error) -> Self { 113 | Self::Smtp(err.to_string()) 114 | } 115 | } 116 | 117 | impl From for AppError { 118 | fn from(err: lettre::address::AddressError) -> Self { 119 | Self::Email(err.to_string()) 120 | } 121 | } 122 | 123 | impl From<&str> for AppError { 124 | fn from(msg: &str) -> Self { 125 | Self::Task(msg.to_string()) 126 | } 127 | } 128 | 129 | impl From for AppError { 130 | fn from(msg: String) -> Self { 131 | Self::Task(msg) 132 | } 133 | } 134 | 135 | // Helper methods for error classification 136 | impl AppError { 137 | #[cfg_attr(not(test), allow(dead_code))] 138 | pub fn is_network_error(&self) -> bool { 139 | match self { 140 | Self::Request(e) => e.is_connect() || e.is_timeout(), 141 | Self::Api(msg) => msg.contains("network"), 142 | Self::Smtp(msg) => msg.contains("connection"), 143 | _ => false, 144 | } 145 | } 146 | 147 | #[cfg_attr(not(test), allow(dead_code))] 148 | pub fn is_auth_error(&self) -> bool { 149 | match self { 150 | Self::Api(msg) => msg.contains("unauthorized") || msg.contains("forbidden"), 151 | Self::Smtp(msg) => msg.contains("authentication"), 152 | _ => false, 153 | } 154 | } 155 | 156 | #[cfg_attr(not(test), allow(dead_code))] 157 | pub fn is_retryable(&self) -> bool { 158 | self.is_network_error() 159 | || match self { 160 | Self::Api(msg) => msg.contains("rate limit") || msg.contains("timeout"), 161 | Self::Smtp(msg) => msg.contains("try again"), 162 | _ => false, 163 | } 164 | } 165 | 166 | // Helper constructors 167 | #[cfg_attr(not(test), allow(dead_code))] 168 | pub fn config(msg: T) -> Self { 169 | Self::Config(msg.to_string()) 170 | } 171 | 172 | pub fn api(msg: T) -> Self { 173 | Self::Api(msg.to_string()) 174 | } 175 | 176 | pub fn task(msg: T) -> Self { 177 | Self::Task(msg.to_string()) 178 | } 179 | } 180 | 181 | #[cfg(test)] 182 | mod tests { 183 | use super::*; 184 | 185 | #[test] 186 | fn test_error_conversions() { 187 | let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found"); 188 | let app_err: AppError = io_err.into(); 189 | assert!(matches!(app_err, AppError::Io(_))); 190 | 191 | let str_err: AppError = "test error".into(); 192 | assert!(matches!(str_err, AppError::Task(_))); 193 | 194 | let string_err: AppError = String::from("test error").into(); 195 | assert!(matches!(string_err, AppError::Task(_))); 196 | } 197 | 198 | #[test] 199 | fn test_error_classification() { 200 | let network_err = AppError::Api("network timeout".to_string()); 201 | assert!(network_err.is_network_error()); 202 | assert!(!network_err.is_auth_error()); 203 | 204 | let auth_err = AppError::Api("unauthorized access".to_string()); 205 | assert!(!auth_err.is_network_error()); 206 | assert!(auth_err.is_auth_error()); 207 | 208 | let retry_err = AppError::Api("rate limit exceeded".to_string()); 209 | assert!(retry_err.is_retryable()); 210 | } 211 | 212 | #[test] 213 | fn test_error_display() { 214 | let err = AppError::config("invalid config"); 215 | assert_eq!(err.to_string(), "Configuration error: invalid config"); 216 | 217 | let err = AppError::api("API timeout"); 218 | assert_eq!(err.to_string(), "API error: API timeout"); 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/api/client.rs: -------------------------------------------------------------------------------- 1 | use reqwest::{Client, StatusCode}; 2 | use std::time::Duration; 3 | use tracing::{debug, error, instrument}; 4 | 5 | use super::models::{AuthResponse, Metric, Stats}; 6 | use crate::error::{AppError, Result}; 7 | 8 | const API_TIMEOUT: Duration = Duration::from_secs(30); 9 | 10 | #[derive(Debug, Clone)] 11 | pub struct UmamiClient { 12 | client: Client, 13 | base_url: String, 14 | } 15 | 16 | impl UmamiClient { 17 | pub fn new(base_url: String) -> Result { 18 | let client = Client::builder() 19 | .timeout(API_TIMEOUT) 20 | .build() 21 | .map_err(|e| AppError::api(format!("Failed to create HTTP client: {e}")))?; 22 | 23 | // Ensure base_url doesn't end with a slash 24 | let base_url = base_url.trim_end_matches('/').to_string(); 25 | 26 | Ok(Self { client, base_url }) 27 | } 28 | 29 | #[instrument(skip(self, password))] 30 | pub async fn authenticate(&self, username: &str, password: &str) -> Result { 31 | debug!("Authenticating with Umami API"); 32 | 33 | let response = self 34 | .client 35 | .post(format!("{}/api/auth/login", self.base_url)) 36 | .json(&serde_json::json!({ 37 | "username": username, 38 | "password": password, 39 | })) 40 | .send() 41 | .await 42 | .map_err(|e| AppError::api(format!("Authentication request failed: {e}")))?; 43 | 44 | match response.status() { 45 | StatusCode::OK => { 46 | let auth = response.json::().await.map_err(|e| { 47 | AppError::api(format!("Failed to parse authentication response: {e}")) 48 | })?; 49 | Ok(auth.token) 50 | } 51 | status => { 52 | let error = response 53 | .text() 54 | .await 55 | .unwrap_or_else(|_| "Unknown error".to_string()); 56 | error!("Authentication failed with status {status}: {error}"); 57 | Err(AppError::api(format!( 58 | "Authentication failed ({status}): {error}" 59 | ))) 60 | } 61 | } 62 | } 63 | 64 | #[instrument(skip(self, token))] 65 | pub async fn get_stats( 66 | &self, 67 | token: &str, 68 | website_id: &str, 69 | start_at: i64, 70 | end_at: i64, 71 | ) -> Result { 72 | debug!("Fetching stats for website {}", website_id); 73 | 74 | let response = self 75 | .client 76 | .get(format!( 77 | "{}/api/websites/{}/stats", 78 | self.base_url, website_id 79 | )) 80 | .query(&[ 81 | ("startAt", start_at.to_string()), 82 | ("endAt", end_at.to_string()), 83 | ]) 84 | .bearer_auth(token) 85 | .send() 86 | .await 87 | .map_err(|e| AppError::api(format!("Failed to fetch stats: {e}")))?; 88 | 89 | self.handle_response(response).await 90 | } 91 | 92 | #[instrument(skip(self, token))] 93 | pub async fn get_metrics( 94 | &self, 95 | token: &str, 96 | website_id: &str, 97 | metric_type: &str, 98 | start_at: i64, 99 | end_at: i64, 100 | limit: u32, 101 | ) -> Result> { 102 | debug!( 103 | "Fetching {} metrics for website {} (limit: {})", 104 | metric_type, website_id, limit 105 | ); 106 | 107 | let response = self 108 | .client 109 | .get(format!( 110 | "{}/api/websites/{}/metrics", 111 | self.base_url, website_id 112 | )) 113 | .query(&[ 114 | ("type", metric_type.to_string()), 115 | ("startAt", start_at.to_string()), 116 | ("endAt", end_at.to_string()), 117 | ("limit", limit.to_string()), 118 | ]) 119 | .bearer_auth(token) 120 | .send() 121 | .await 122 | .map_err(|e| AppError::api(format!("Failed to fetch metrics: {e}")))?; 123 | 124 | let mut metrics: Vec = self.handle_response(response).await?; 125 | 126 | if metric_type == "country" { 127 | for metric in &mut metrics { 128 | debug!("Processing country code: '{}'", metric.x); 129 | metric.x = crate::config::get_country_name(&metric.x); 130 | } 131 | } 132 | 133 | Ok(metrics) 134 | } 135 | 136 | async fn handle_response(&self, response: reqwest::Response) -> Result 137 | where 138 | T: for<'de> serde::Deserialize<'de>, 139 | { 140 | match response.status() { 141 | StatusCode::OK => response 142 | .json::() 143 | .await 144 | .map_err(|e| AppError::api(format!("Failed to parse API response: {e}"))), 145 | StatusCode::UNAUTHORIZED => { 146 | error!("API authentication failed"); 147 | Err(AppError::api("Authentication token expired or invalid")) 148 | } 149 | StatusCode::NOT_FOUND => { 150 | error!("API endpoint or resource not found"); 151 | Err(AppError::api("Resource not found")) 152 | } 153 | StatusCode::TOO_MANY_REQUESTS => { 154 | error!("API rate limit exceeded"); 155 | Err(AppError::api("Rate limit exceeded")) 156 | } 157 | status => { 158 | let error = response 159 | .text() 160 | .await 161 | .unwrap_or_else(|_| "Unknown error".to_string()); 162 | error!("API request failed with status {}: {}", status, error); 163 | Err(AppError::api(format!( 164 | "API request failed ({status}): {error}" 165 | ))) 166 | } 167 | } 168 | } 169 | } 170 | 171 | #[cfg(test)] 172 | mod tests { 173 | use super::*; 174 | use mockito::Server; 175 | use serde_json::json; 176 | 177 | #[tokio::test] 178 | async fn test_authentication_success() { 179 | let mut server = Server::new_async().await; 180 | let client = UmamiClient::new(server.url()).unwrap(); 181 | 182 | let _mock = server 183 | .mock("POST", "/api/auth/login") 184 | .with_status(200) 185 | .with_header("content-type", "application/json") 186 | .with_body(r#"{"token": "test-token"}"#) 187 | .create_async() 188 | .await; 189 | 190 | let result = client.authenticate("test", "password").await; 191 | assert!(result.is_ok()); 192 | assert_eq!(result.unwrap(), "test-token"); 193 | } 194 | 195 | #[tokio::test] 196 | async fn test_authentication_failure() { 197 | let mut server = Server::new_async().await; 198 | let client = UmamiClient::new(server.url()).unwrap(); 199 | 200 | let _mock = server 201 | .mock("POST", "/api/auth/login") 202 | .with_status(401) 203 | .with_header("content-type", "application/json") 204 | .with_body(r#"{"error": "Invalid credentials"}"#) 205 | .create_async() 206 | .await; 207 | 208 | let result = client.authenticate("test", "wrong").await; 209 | assert!(result.is_err()); 210 | assert!(matches!(result.unwrap_err(), AppError::Api(_))); 211 | } 212 | 213 | #[tokio::test] 214 | async fn test_get_stats_success() { 215 | let mut server = Server::new_async().await; 216 | let client = UmamiClient::new(server.url()).unwrap(); 217 | 218 | let stats = json!({ 219 | "pageviews": { "value": 100, "prev": 90 }, 220 | "visitors": { "value": 50, "prev": 45 }, 221 | "visits": { "value": 75, "prev": 70 }, 222 | "bounces": { "value": 20, "prev": 25 }, 223 | "totaltime": { "value": 3600, "prev": 3300 } 224 | }); 225 | 226 | let _mock = server 227 | .mock("GET", "/api/websites/test-id/stats") 228 | .match_query(mockito::Matcher::Any) 229 | .with_status(200) 230 | .with_header("content-type", "application/json") 231 | .with_body(stats.to_string()) 232 | .create_async() 233 | .await; 234 | 235 | let result = client.get_stats("token", "test-id", 0, 1000).await; 236 | assert!(result.is_ok()); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/report/generator.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Datelike, TimeZone, Utc}; 2 | use handlebars::Handlebars; 3 | use lettre::{ 4 | message::{header, Message, MultiPart}, 5 | transport::smtp::{ 6 | authentication::Credentials, 7 | client::{Tls, TlsParameters}, 8 | }, 9 | AsyncSmtpTransport, AsyncTransport, Tokio1Executor, 10 | }; 11 | use std::sync::Arc; 12 | use tracing::{debug, error, info}; 13 | 14 | use super::{helpers, models::ReportData}; 15 | use crate::{ 16 | api::client::UmamiClient, 17 | api::models::MetricValue, 18 | config::models::{ReportType, SmtpConfig, WebsiteConfig}, 19 | error::{AppError, Result}, 20 | }; 21 | 22 | #[derive(Debug)] 23 | struct TimeRange { 24 | start: DateTime, 25 | end: DateTime, 26 | } 27 | 28 | #[derive(Clone)] 29 | pub struct ReportGenerator { 30 | template: Arc>, 31 | } 32 | 33 | impl ReportGenerator { 34 | pub fn new(template: Arc>) -> Self { 35 | Self { template } 36 | } 37 | 38 | pub async fn generate_and_send( 39 | &self, 40 | client: &UmamiClient, 41 | dry_run: &bool, 42 | website: &WebsiteConfig, 43 | report_type: &ReportType, 44 | smtp_config: &SmtpConfig, 45 | token: &str, 46 | ) -> Result<()> { 47 | info!("Generating report for website: {}", website.name); 48 | 49 | let time_range = self.calculate_time_range(&website.timezone, report_type)?; 50 | let report_data = self 51 | .fetch_report_data(client, website, token, time_range, report_type) 52 | .await?; 53 | let html = self.render_report(&report_data)?; 54 | 55 | if *dry_run { 56 | info!("Dry run enabled, will not send an email"); 57 | } else { 58 | self.send_email( 59 | smtp_config, 60 | &website.recipients, 61 | &format!( 62 | "{} Analytics Report - {} - {}", 63 | report_type, website.name, report_data.date 64 | ), 65 | &html, 66 | ) 67 | .await?; 68 | } 69 | 70 | info!("Successfully sent report for website: {}", website.name); 71 | Ok(()) 72 | } 73 | 74 | fn calculate_time_range(&self, timezone: &str, report_type: &ReportType) -> Result { 75 | let tz: chrono_tz::Tz = timezone.parse().map_err(|e| { 76 | error!("Invalid timezone {}: {}", timezone, e); 77 | AppError::Config(format!("Invalid timezone: {e}")) 78 | })?; 79 | 80 | let now = Utc::now().with_timezone(&tz); 81 | let (start, end); 82 | 83 | if report_type == &ReportType::Daily { 84 | let yesterday = now - chrono::Duration::days(1); 85 | debug!( 86 | "Calculating daily report for {}", 87 | yesterday.format("%Y-%m-%d") 88 | ); 89 | 90 | start = tz 91 | .with_ymd_and_hms( 92 | yesterday.year(), 93 | yesterday.month(), 94 | yesterday.day(), 95 | 0, 96 | 0, 97 | 0, 98 | ) 99 | .unwrap() 100 | .with_timezone(&Utc); 101 | 102 | end = start + chrono::Duration::days(1) - chrono::Duration::seconds(1); 103 | } else { 104 | // Generate report for time ending yesterday 105 | let yesterday = now - chrono::Duration::days(1); 106 | debug!( 107 | "Calculating weekly report ending: {}", 108 | yesterday.format("%Y-%m-%d") 109 | ); 110 | 111 | // Set end time to yesterday 23:59:59 112 | end = tz 113 | .with_ymd_and_hms( 114 | yesterday.year(), 115 | yesterday.month(), 116 | yesterday.day(), 117 | 23, 118 | 59, 119 | 59, 120 | ) 121 | .unwrap() 122 | .with_timezone(&Utc); 123 | 124 | // Start time is 7 days before end time (previous Sunday 00:00:00) 125 | start = end - chrono::Duration::days(7) + chrono::Duration::seconds(1); 126 | } 127 | 128 | debug!("Time range: {} to {}", start, end); 129 | Ok(TimeRange { start, end }) 130 | } 131 | 132 | async fn fetch_report_data( 133 | &self, 134 | client: &UmamiClient, 135 | website: &WebsiteConfig, 136 | token: &str, 137 | time_range: TimeRange, 138 | report_type: &ReportType, 139 | ) -> Result { 140 | debug!( 141 | "Fetching metrics for time range: {} to {}", 142 | time_range.start, time_range.end 143 | ); 144 | 145 | let start_at = time_range.start.timestamp_millis(); 146 | let end_at = time_range.end.timestamp_millis(); 147 | 148 | let stats = client 149 | .get_stats(token, &website.id, start_at, end_at) 150 | .await?; 151 | 152 | let bounce_rate = MetricValue { 153 | value: if stats.visits.value > 0.0 { 154 | (stats.bounces.value / stats.visits.value * 100.0).min(100.0) 155 | } else { 156 | 0.0 157 | }, 158 | prev: if stats.visits.prev > 0.0 { 159 | (stats.bounces.prev / stats.visits.prev * 100.0).min(100.0) 160 | } else { 161 | 0.0 162 | }, 163 | }; 164 | 165 | let time_spent = helpers::format_time_spent(stats.total_time.value, stats.visits.value); 166 | 167 | let pages = client 168 | .get_metrics(token, &website.id, "url", start_at, end_at, 10) 169 | .await?; 170 | 171 | let countries = client 172 | .get_metrics(token, &website.id, "country", start_at, end_at, 10) 173 | .await?; 174 | 175 | let browsers = client 176 | .get_metrics(token, &website.id, "browser", start_at, end_at, 5) 177 | .await?; 178 | 179 | let devices = client 180 | .get_metrics(token, &website.id, "device", start_at, end_at, 5) 181 | .await?; 182 | 183 | let referrers = client 184 | .get_metrics(token, &website.id, "referrer", start_at, end_at, 5) 185 | .await?; 186 | 187 | Ok(ReportData { 188 | website_name: website.name.clone(), 189 | date: time_range.end.format("%B %d, %Y").to_string(), 190 | report_type: report_type.to_string(), 191 | stats, 192 | bounce_rate, 193 | time_spent, 194 | pages, 195 | countries, 196 | browsers, 197 | devices, 198 | referrers, 199 | }) 200 | } 201 | 202 | fn render_report(&self, data: &ReportData) -> Result { 203 | debug!("Rendering report template"); 204 | 205 | self.template.render("email", &data).map_err(|e| { 206 | error!("Failed to render template: {}", e); 207 | AppError::Template(format!("Failed to render report: {e}")) 208 | }) 209 | } 210 | 211 | async fn send_email( 212 | &self, 213 | config: &SmtpConfig, 214 | recipients: &[String], 215 | subject: &str, 216 | html_content: &str, 217 | ) -> Result<()> { 218 | debug!("Sending email to {} recipients", recipients.len()); 219 | 220 | let creds = Credentials::new(config.username.clone(), config.password.clone()); 221 | 222 | let tls_parameters = if config.tls { 223 | Tls::Required(TlsParameters::new(config.host.clone())?) 224 | } else { 225 | Tls::None 226 | }; 227 | 228 | let mailer = AsyncSmtpTransport::::relay(&config.host)? 229 | .credentials(creds) 230 | .port(config.port) 231 | .tls(tls_parameters) 232 | .build(); 233 | 234 | for recipient in recipients { 235 | let email = Message::builder() 236 | .from(config.from.parse()?) 237 | .to(recipient.parse()?) 238 | .subject(subject) 239 | .multipart( 240 | MultiPart::alternative().singlepart( 241 | lettre::message::SinglePart::builder() 242 | .header(header::ContentType::TEXT_HTML) 243 | .body(html_content.to_string()), 244 | ), 245 | )?; 246 | 247 | match mailer.send(email).await { 248 | Ok(_) => debug!("Email sent successfully to {}", recipient), 249 | Err(e) => { 250 | error!("Failed to send email to {}: {}", recipient, e); 251 | return Err(AppError::Smtp(format!( 252 | "Failed to send email to {recipient}: {e}" 253 | ))); 254 | } 255 | } 256 | } 257 | 258 | Ok(()) 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /templates/email.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 157 | 158 | 159 |
160 |
161 |

{{website_name}} - {{report_type}} Analytics Report

162 |

{{date}}

163 |
164 | 165 | 166 | 167 | 178 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 199 | 210 | 211 |
168 |
Pageviews
169 |
{{formatNumber stats.pageviews.value}}
170 |
171 | {{#if (gt stats.pageviews.value stats.pageviews.prev)}} 172 |
↑ {{formatNumber (sub stats.pageviews.value stats.pageviews.prev)}} views
173 | {{else}} 174 |
↓ {{formatNumber (sub stats.pageviews.prev stats.pageviews.value)}} views
175 | {{/if}} 176 |
177 |
179 |
Unique Visitors
180 |
{{formatNumber stats.visitors.value}}
181 |
182 | {{#if (gt stats.visitors.value stats.visitors.prev)}} 183 |
↑ {{formatNumber (sub stats.visitors.value stats.visitors.prev)}} visitors
184 | {{else}} 185 |
↓ {{formatNumber (sub stats.visitors.prev stats.visitors.value)}} visitors
186 | {{/if}} 187 |
188 |
196 |
Avg Time per Visit
197 |
{{time_spent}}
198 |
200 |
Bounce Rate
201 |
{{formatFloat bounce_rate.value 1}}%
202 |
203 | {{#if (gt bounce_rate.value bounce_rate.prev)}} 204 |
↑ {{formatFloat (sub bounce_rate.value bounce_rate.prev) 1}}%
205 | {{else}} 206 |
↓ {{formatFloat (sub bounce_rate.prev bounce_rate.value) 1}}%
207 | {{/if}} 208 |
209 |
212 | 213 |
214 |
215 |

Top Pages

216 | 217 | 218 | 219 | 220 | 221 | {{#each pages}} 222 | 223 | 224 | 230 | 231 | {{/each}} 232 |
PageViews
{{x}} 225 | {{formatNumber y}} 226 |
227 |
228 |
229 |
233 |
234 | 235 |
236 |

Top Referrers

237 | 238 | 239 | 240 | 241 | 242 | {{#each referrers}} 243 | 244 | 245 | 251 | 252 | {{/each}} 253 |
SourceVisitors
{{#if (eq x "")}}No Referrer{{else}}{{x}}{{/if}} 246 | {{formatNumber y}} 247 |
248 |
249 |
250 |
254 |
255 |
256 | 257 |
258 |
259 |

Browsers

260 | 261 | 262 | 263 | 264 | 265 | {{#each browsers}} 266 | 267 | 268 | 274 | 275 | {{/each}} 276 |
BrowserUsers
{{x}} 269 | {{formatNumber y}} 270 |
271 |
272 |
273 |
277 |
278 | 279 |
280 |

Devices

281 | 282 | 283 | 284 | 285 | 286 | {{#each devices}} 287 | 288 | 289 | 295 | 296 | {{/each}} 297 |
DeviceUsers
{{x}} 290 | {{formatNumber y}} 291 |
292 |
293 |
294 |
298 |
299 |
300 | 301 |
302 |

Top Countries

303 | 304 | 305 | 306 | 307 | 308 | {{#each countries}} 309 | 310 | 311 | 317 | 318 | {{/each}} 319 |
CountryVisitors
{{x}} 312 | {{formatNumber y}} 313 |
314 |
315 |
316 |
320 |
321 | 324 |
325 | 326 | 327 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "ahash" 22 | version = "0.8.12" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" 25 | dependencies = [ 26 | "cfg-if", 27 | "once_cell", 28 | "version_check", 29 | "zerocopy", 30 | ] 31 | 32 | [[package]] 33 | name = "aho-corasick" 34 | version = "1.1.3" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 37 | dependencies = [ 38 | "memchr", 39 | ] 40 | 41 | [[package]] 42 | name = "allocator-api2" 43 | version = "0.2.21" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" 46 | 47 | [[package]] 48 | name = "android-tzdata" 49 | version = "0.1.1" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 52 | 53 | [[package]] 54 | name = "android_system_properties" 55 | version = "0.1.5" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 58 | dependencies = [ 59 | "libc", 60 | ] 61 | 62 | [[package]] 63 | name = "anstream" 64 | version = "0.6.18" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 67 | dependencies = [ 68 | "anstyle", 69 | "anstyle-parse", 70 | "anstyle-query", 71 | "anstyle-wincon", 72 | "colorchoice", 73 | "is_terminal_polyfill", 74 | "utf8parse", 75 | ] 76 | 77 | [[package]] 78 | name = "anstyle" 79 | version = "1.0.10" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 82 | 83 | [[package]] 84 | name = "anstyle-parse" 85 | version = "0.2.6" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 88 | dependencies = [ 89 | "utf8parse", 90 | ] 91 | 92 | [[package]] 93 | name = "anstyle-query" 94 | version = "1.1.2" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 97 | dependencies = [ 98 | "windows-sys 0.59.0", 99 | ] 100 | 101 | [[package]] 102 | name = "anstyle-wincon" 103 | version = "3.0.7" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 106 | dependencies = [ 107 | "anstyle", 108 | "once_cell", 109 | "windows-sys 0.59.0", 110 | ] 111 | 112 | [[package]] 113 | name = "assert-json-diff" 114 | version = "2.0.2" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" 117 | dependencies = [ 118 | "serde", 119 | "serde_json", 120 | ] 121 | 122 | [[package]] 123 | name = "async-trait" 124 | version = "0.1.88" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" 127 | dependencies = [ 128 | "proc-macro2", 129 | "quote", 130 | "syn", 131 | ] 132 | 133 | [[package]] 134 | name = "atomic-waker" 135 | version = "1.1.2" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 138 | 139 | [[package]] 140 | name = "autocfg" 141 | version = "1.4.0" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 144 | 145 | [[package]] 146 | name = "backtrace" 147 | version = "0.3.75" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" 150 | dependencies = [ 151 | "addr2line", 152 | "cfg-if", 153 | "libc", 154 | "miniz_oxide", 155 | "object", 156 | "rustc-demangle", 157 | "windows-targets 0.52.6", 158 | ] 159 | 160 | [[package]] 161 | name = "base64" 162 | version = "0.22.1" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 165 | 166 | [[package]] 167 | name = "bitflags" 168 | version = "2.9.1" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 171 | 172 | [[package]] 173 | name = "block-buffer" 174 | version = "0.10.4" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 177 | dependencies = [ 178 | "generic-array", 179 | ] 180 | 181 | [[package]] 182 | name = "bumpalo" 183 | version = "3.17.0" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 186 | 187 | [[package]] 188 | name = "bytes" 189 | version = "1.10.1" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 192 | 193 | [[package]] 194 | name = "cc" 195 | version = "1.2.23" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766" 198 | dependencies = [ 199 | "shlex", 200 | ] 201 | 202 | [[package]] 203 | name = "cfg-if" 204 | version = "1.0.0" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 207 | 208 | [[package]] 209 | name = "cfg_aliases" 210 | version = "0.2.1" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 213 | 214 | [[package]] 215 | name = "chrono" 216 | version = "0.4.41" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" 219 | dependencies = [ 220 | "android-tzdata", 221 | "iana-time-zone", 222 | "js-sys", 223 | "num-traits", 224 | "serde", 225 | "wasm-bindgen", 226 | "windows-link", 227 | ] 228 | 229 | [[package]] 230 | name = "chrono-tz" 231 | version = "0.10.3" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "efdce149c370f133a071ca8ef6ea340b7b88748ab0810097a9e2976eaa34b4f3" 234 | dependencies = [ 235 | "chrono", 236 | "chrono-tz-build", 237 | "phf", 238 | ] 239 | 240 | [[package]] 241 | name = "chrono-tz-build" 242 | version = "0.4.1" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "8f10f8c9340e31fc120ff885fcdb54a0b48e474bbd77cab557f0c30a3e569402" 245 | dependencies = [ 246 | "parse-zoneinfo", 247 | "phf_codegen", 248 | ] 249 | 250 | [[package]] 251 | name = "chumsky" 252 | version = "0.9.3" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" 255 | dependencies = [ 256 | "hashbrown 0.14.5", 257 | "stacker", 258 | ] 259 | 260 | [[package]] 261 | name = "clap" 262 | version = "4.5.38" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" 265 | dependencies = [ 266 | "clap_builder", 267 | "clap_derive", 268 | ] 269 | 270 | [[package]] 271 | name = "clap_builder" 272 | version = "4.5.38" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120" 275 | dependencies = [ 276 | "anstream", 277 | "anstyle", 278 | "clap_lex", 279 | "strsim", 280 | ] 281 | 282 | [[package]] 283 | name = "clap_derive" 284 | version = "4.5.32" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 287 | dependencies = [ 288 | "heck", 289 | "proc-macro2", 290 | "quote", 291 | "syn", 292 | ] 293 | 294 | [[package]] 295 | name = "clap_lex" 296 | version = "0.7.4" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 299 | 300 | [[package]] 301 | name = "colorchoice" 302 | version = "1.0.3" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 305 | 306 | [[package]] 307 | name = "colored" 308 | version = "3.0.0" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" 311 | dependencies = [ 312 | "windows-sys 0.59.0", 313 | ] 314 | 315 | [[package]] 316 | name = "core-foundation-sys" 317 | version = "0.8.7" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 320 | 321 | [[package]] 322 | name = "cpufeatures" 323 | version = "0.2.17" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 326 | dependencies = [ 327 | "libc", 328 | ] 329 | 330 | [[package]] 331 | name = "crypto-common" 332 | version = "0.1.6" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 335 | dependencies = [ 336 | "generic-array", 337 | "typenum", 338 | ] 339 | 340 | [[package]] 341 | name = "darling" 342 | version = "0.20.11" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" 345 | dependencies = [ 346 | "darling_core", 347 | "darling_macro", 348 | ] 349 | 350 | [[package]] 351 | name = "darling_core" 352 | version = "0.20.11" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" 355 | dependencies = [ 356 | "fnv", 357 | "ident_case", 358 | "proc-macro2", 359 | "quote", 360 | "strsim", 361 | "syn", 362 | ] 363 | 364 | [[package]] 365 | name = "darling_macro" 366 | version = "0.20.11" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" 369 | dependencies = [ 370 | "darling_core", 371 | "quote", 372 | "syn", 373 | ] 374 | 375 | [[package]] 376 | name = "derive_builder" 377 | version = "0.20.2" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" 380 | dependencies = [ 381 | "derive_builder_macro", 382 | ] 383 | 384 | [[package]] 385 | name = "derive_builder_core" 386 | version = "0.20.2" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" 389 | dependencies = [ 390 | "darling", 391 | "proc-macro2", 392 | "quote", 393 | "syn", 394 | ] 395 | 396 | [[package]] 397 | name = "derive_builder_macro" 398 | version = "0.20.2" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" 401 | dependencies = [ 402 | "derive_builder_core", 403 | "syn", 404 | ] 405 | 406 | [[package]] 407 | name = "digest" 408 | version = "0.10.7" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 411 | dependencies = [ 412 | "block-buffer", 413 | "crypto-common", 414 | ] 415 | 416 | [[package]] 417 | name = "displaydoc" 418 | version = "0.2.5" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 421 | dependencies = [ 422 | "proc-macro2", 423 | "quote", 424 | "syn", 425 | ] 426 | 427 | [[package]] 428 | name = "email-encoding" 429 | version = "0.4.1" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "9298e6504d9b9e780ed3f7dfd43a61be8cd0e09eb07f7706a945b0072b6670b6" 432 | dependencies = [ 433 | "base64", 434 | "memchr", 435 | ] 436 | 437 | [[package]] 438 | name = "email_address" 439 | version = "0.2.9" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" 442 | 443 | [[package]] 444 | name = "equivalent" 445 | version = "1.0.2" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 448 | 449 | [[package]] 450 | name = "fastrand" 451 | version = "2.3.0" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 454 | 455 | [[package]] 456 | name = "fnv" 457 | version = "1.0.7" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 460 | 461 | [[package]] 462 | name = "form_urlencoded" 463 | version = "1.2.1" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 466 | dependencies = [ 467 | "percent-encoding", 468 | ] 469 | 470 | [[package]] 471 | name = "futures" 472 | version = "0.3.31" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 475 | dependencies = [ 476 | "futures-channel", 477 | "futures-core", 478 | "futures-executor", 479 | "futures-io", 480 | "futures-sink", 481 | "futures-task", 482 | "futures-util", 483 | ] 484 | 485 | [[package]] 486 | name = "futures-channel" 487 | version = "0.3.31" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 490 | dependencies = [ 491 | "futures-core", 492 | "futures-sink", 493 | ] 494 | 495 | [[package]] 496 | name = "futures-core" 497 | version = "0.3.31" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 500 | 501 | [[package]] 502 | name = "futures-executor" 503 | version = "0.3.31" 504 | source = "registry+https://github.com/rust-lang/crates.io-index" 505 | checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 506 | dependencies = [ 507 | "futures-core", 508 | "futures-task", 509 | "futures-util", 510 | ] 511 | 512 | [[package]] 513 | name = "futures-io" 514 | version = "0.3.31" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 517 | 518 | [[package]] 519 | name = "futures-macro" 520 | version = "0.3.31" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 523 | dependencies = [ 524 | "proc-macro2", 525 | "quote", 526 | "syn", 527 | ] 528 | 529 | [[package]] 530 | name = "futures-sink" 531 | version = "0.3.31" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 534 | 535 | [[package]] 536 | name = "futures-task" 537 | version = "0.3.31" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 540 | 541 | [[package]] 542 | name = "futures-util" 543 | version = "0.3.31" 544 | source = "registry+https://github.com/rust-lang/crates.io-index" 545 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 546 | dependencies = [ 547 | "futures-channel", 548 | "futures-core", 549 | "futures-io", 550 | "futures-macro", 551 | "futures-sink", 552 | "futures-task", 553 | "memchr", 554 | "pin-project-lite", 555 | "pin-utils", 556 | "slab", 557 | ] 558 | 559 | [[package]] 560 | name = "generic-array" 561 | version = "0.14.7" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 564 | dependencies = [ 565 | "typenum", 566 | "version_check", 567 | ] 568 | 569 | [[package]] 570 | name = "getrandom" 571 | version = "0.2.16" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 574 | dependencies = [ 575 | "cfg-if", 576 | "js-sys", 577 | "libc", 578 | "wasi 0.11.0+wasi-snapshot-preview1", 579 | "wasm-bindgen", 580 | ] 581 | 582 | [[package]] 583 | name = "getrandom" 584 | version = "0.3.3" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 587 | dependencies = [ 588 | "cfg-if", 589 | "js-sys", 590 | "libc", 591 | "r-efi", 592 | "wasi 0.14.2+wasi-0.2.4", 593 | "wasm-bindgen", 594 | ] 595 | 596 | [[package]] 597 | name = "gimli" 598 | version = "0.31.1" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 601 | 602 | [[package]] 603 | name = "h2" 604 | version = "0.4.10" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" 607 | dependencies = [ 608 | "atomic-waker", 609 | "bytes", 610 | "fnv", 611 | "futures-core", 612 | "futures-sink", 613 | "http", 614 | "indexmap", 615 | "slab", 616 | "tokio", 617 | "tokio-util", 618 | "tracing", 619 | ] 620 | 621 | [[package]] 622 | name = "handlebars" 623 | version = "6.3.2" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "759e2d5aea3287cb1190c8ec394f42866cb5bf74fcbf213f354e3c856ea26098" 626 | dependencies = [ 627 | "derive_builder", 628 | "log", 629 | "num-order", 630 | "pest", 631 | "pest_derive", 632 | "serde", 633 | "serde_json", 634 | "thiserror", 635 | "walkdir", 636 | ] 637 | 638 | [[package]] 639 | name = "hashbrown" 640 | version = "0.14.5" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 643 | dependencies = [ 644 | "ahash", 645 | "allocator-api2", 646 | ] 647 | 648 | [[package]] 649 | name = "hashbrown" 650 | version = "0.15.3" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" 653 | 654 | [[package]] 655 | name = "heck" 656 | version = "0.5.0" 657 | source = "registry+https://github.com/rust-lang/crates.io-index" 658 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 659 | 660 | [[package]] 661 | name = "http" 662 | version = "1.3.1" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 665 | dependencies = [ 666 | "bytes", 667 | "fnv", 668 | "itoa", 669 | ] 670 | 671 | [[package]] 672 | name = "http-body" 673 | version = "1.0.1" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 676 | dependencies = [ 677 | "bytes", 678 | "http", 679 | ] 680 | 681 | [[package]] 682 | name = "http-body-util" 683 | version = "0.1.3" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 686 | dependencies = [ 687 | "bytes", 688 | "futures-core", 689 | "http", 690 | "http-body", 691 | "pin-project-lite", 692 | ] 693 | 694 | [[package]] 695 | name = "httparse" 696 | version = "1.10.1" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 699 | 700 | [[package]] 701 | name = "httpdate" 702 | version = "1.0.3" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 705 | 706 | [[package]] 707 | name = "hyper" 708 | version = "1.6.0" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" 711 | dependencies = [ 712 | "bytes", 713 | "futures-channel", 714 | "futures-util", 715 | "h2", 716 | "http", 717 | "http-body", 718 | "httparse", 719 | "httpdate", 720 | "itoa", 721 | "pin-project-lite", 722 | "smallvec", 723 | "tokio", 724 | "want", 725 | ] 726 | 727 | [[package]] 728 | name = "hyper-rustls" 729 | version = "0.27.5" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" 732 | dependencies = [ 733 | "futures-util", 734 | "http", 735 | "hyper", 736 | "hyper-util", 737 | "rustls", 738 | "rustls-pki-types", 739 | "tokio", 740 | "tokio-rustls", 741 | "tower-service", 742 | "webpki-roots 0.26.11", 743 | ] 744 | 745 | [[package]] 746 | name = "hyper-util" 747 | version = "0.1.12" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "cf9f1e950e0d9d1d3c47184416723cf29c0d1f93bd8cccf37e4beb6b44f31710" 750 | dependencies = [ 751 | "bytes", 752 | "futures-channel", 753 | "futures-util", 754 | "http", 755 | "http-body", 756 | "hyper", 757 | "libc", 758 | "pin-project-lite", 759 | "socket2", 760 | "tokio", 761 | "tower-service", 762 | "tracing", 763 | ] 764 | 765 | [[package]] 766 | name = "iana-time-zone" 767 | version = "0.1.63" 768 | source = "registry+https://github.com/rust-lang/crates.io-index" 769 | checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" 770 | dependencies = [ 771 | "android_system_properties", 772 | "core-foundation-sys", 773 | "iana-time-zone-haiku", 774 | "js-sys", 775 | "log", 776 | "wasm-bindgen", 777 | "windows-core", 778 | ] 779 | 780 | [[package]] 781 | name = "iana-time-zone-haiku" 782 | version = "0.1.2" 783 | source = "registry+https://github.com/rust-lang/crates.io-index" 784 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 785 | dependencies = [ 786 | "cc", 787 | ] 788 | 789 | [[package]] 790 | name = "icu_collections" 791 | version = "2.0.0" 792 | source = "registry+https://github.com/rust-lang/crates.io-index" 793 | checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" 794 | dependencies = [ 795 | "displaydoc", 796 | "potential_utf", 797 | "yoke", 798 | "zerofrom", 799 | "zerovec", 800 | ] 801 | 802 | [[package]] 803 | name = "icu_locale_core" 804 | version = "2.0.0" 805 | source = "registry+https://github.com/rust-lang/crates.io-index" 806 | checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" 807 | dependencies = [ 808 | "displaydoc", 809 | "litemap", 810 | "tinystr", 811 | "writeable", 812 | "zerovec", 813 | ] 814 | 815 | [[package]] 816 | name = "icu_normalizer" 817 | version = "2.0.0" 818 | source = "registry+https://github.com/rust-lang/crates.io-index" 819 | checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" 820 | dependencies = [ 821 | "displaydoc", 822 | "icu_collections", 823 | "icu_normalizer_data", 824 | "icu_properties", 825 | "icu_provider", 826 | "smallvec", 827 | "zerovec", 828 | ] 829 | 830 | [[package]] 831 | name = "icu_normalizer_data" 832 | version = "2.0.0" 833 | source = "registry+https://github.com/rust-lang/crates.io-index" 834 | checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" 835 | 836 | [[package]] 837 | name = "icu_properties" 838 | version = "2.0.1" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" 841 | dependencies = [ 842 | "displaydoc", 843 | "icu_collections", 844 | "icu_locale_core", 845 | "icu_properties_data", 846 | "icu_provider", 847 | "potential_utf", 848 | "zerotrie", 849 | "zerovec", 850 | ] 851 | 852 | [[package]] 853 | name = "icu_properties_data" 854 | version = "2.0.1" 855 | source = "registry+https://github.com/rust-lang/crates.io-index" 856 | checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" 857 | 858 | [[package]] 859 | name = "icu_provider" 860 | version = "2.0.0" 861 | source = "registry+https://github.com/rust-lang/crates.io-index" 862 | checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" 863 | dependencies = [ 864 | "displaydoc", 865 | "icu_locale_core", 866 | "stable_deref_trait", 867 | "tinystr", 868 | "writeable", 869 | "yoke", 870 | "zerofrom", 871 | "zerotrie", 872 | "zerovec", 873 | ] 874 | 875 | [[package]] 876 | name = "ident_case" 877 | version = "1.0.1" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 880 | 881 | [[package]] 882 | name = "idna" 883 | version = "1.0.3" 884 | source = "registry+https://github.com/rust-lang/crates.io-index" 885 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 886 | dependencies = [ 887 | "idna_adapter", 888 | "smallvec", 889 | "utf8_iter", 890 | ] 891 | 892 | [[package]] 893 | name = "idna_adapter" 894 | version = "1.2.1" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 897 | dependencies = [ 898 | "icu_normalizer", 899 | "icu_properties", 900 | ] 901 | 902 | [[package]] 903 | name = "indexmap" 904 | version = "2.9.0" 905 | source = "registry+https://github.com/rust-lang/crates.io-index" 906 | checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" 907 | dependencies = [ 908 | "equivalent", 909 | "hashbrown 0.15.3", 910 | ] 911 | 912 | [[package]] 913 | name = "ipnet" 914 | version = "2.11.0" 915 | source = "registry+https://github.com/rust-lang/crates.io-index" 916 | checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 917 | 918 | [[package]] 919 | name = "is_terminal_polyfill" 920 | version = "1.70.1" 921 | source = "registry+https://github.com/rust-lang/crates.io-index" 922 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 923 | 924 | [[package]] 925 | name = "itoa" 926 | version = "1.0.15" 927 | source = "registry+https://github.com/rust-lang/crates.io-index" 928 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 929 | 930 | [[package]] 931 | name = "js-sys" 932 | version = "0.3.77" 933 | source = "registry+https://github.com/rust-lang/crates.io-index" 934 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 935 | dependencies = [ 936 | "once_cell", 937 | "wasm-bindgen", 938 | ] 939 | 940 | [[package]] 941 | name = "lazy_static" 942 | version = "1.5.0" 943 | source = "registry+https://github.com/rust-lang/crates.io-index" 944 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 945 | 946 | [[package]] 947 | name = "lettre" 948 | version = "0.11.16" 949 | source = "registry+https://github.com/rust-lang/crates.io-index" 950 | checksum = "87ffd14fa289730e3ad68edefdc31f603d56fe716ec38f2076bb7410e09147c2" 951 | dependencies = [ 952 | "async-trait", 953 | "base64", 954 | "chumsky", 955 | "email-encoding", 956 | "email_address", 957 | "fastrand", 958 | "futures-io", 959 | "futures-util", 960 | "httpdate", 961 | "idna", 962 | "mime", 963 | "nom", 964 | "percent-encoding", 965 | "quoted_printable", 966 | "rustls", 967 | "socket2", 968 | "tokio", 969 | "tokio-rustls", 970 | "url", 971 | "webpki-roots 1.0.0", 972 | ] 973 | 974 | [[package]] 975 | name = "libc" 976 | version = "0.2.172" 977 | source = "registry+https://github.com/rust-lang/crates.io-index" 978 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 979 | 980 | [[package]] 981 | name = "litemap" 982 | version = "0.8.0" 983 | source = "registry+https://github.com/rust-lang/crates.io-index" 984 | checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" 985 | 986 | [[package]] 987 | name = "lock_api" 988 | version = "0.4.12" 989 | source = "registry+https://github.com/rust-lang/crates.io-index" 990 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 991 | dependencies = [ 992 | "autocfg", 993 | "scopeguard", 994 | ] 995 | 996 | [[package]] 997 | name = "log" 998 | version = "0.4.27" 999 | source = "registry+https://github.com/rust-lang/crates.io-index" 1000 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 1001 | 1002 | [[package]] 1003 | name = "lru-slab" 1004 | version = "0.1.2" 1005 | source = "registry+https://github.com/rust-lang/crates.io-index" 1006 | checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" 1007 | 1008 | [[package]] 1009 | name = "matchers" 1010 | version = "0.1.0" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 1013 | dependencies = [ 1014 | "regex-automata 0.1.10", 1015 | ] 1016 | 1017 | [[package]] 1018 | name = "memchr" 1019 | version = "2.7.4" 1020 | source = "registry+https://github.com/rust-lang/crates.io-index" 1021 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 1022 | 1023 | [[package]] 1024 | name = "mime" 1025 | version = "0.3.17" 1026 | source = "registry+https://github.com/rust-lang/crates.io-index" 1027 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 1028 | 1029 | [[package]] 1030 | name = "miniz_oxide" 1031 | version = "0.8.8" 1032 | source = "registry+https://github.com/rust-lang/crates.io-index" 1033 | checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" 1034 | dependencies = [ 1035 | "adler2", 1036 | ] 1037 | 1038 | [[package]] 1039 | name = "mio" 1040 | version = "1.0.3" 1041 | source = "registry+https://github.com/rust-lang/crates.io-index" 1042 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 1043 | dependencies = [ 1044 | "libc", 1045 | "wasi 0.11.0+wasi-snapshot-preview1", 1046 | "windows-sys 0.52.0", 1047 | ] 1048 | 1049 | [[package]] 1050 | name = "mockito" 1051 | version = "1.7.0" 1052 | source = "registry+https://github.com/rust-lang/crates.io-index" 1053 | checksum = "7760e0e418d9b7e5777c0374009ca4c93861b9066f18cb334a20ce50ab63aa48" 1054 | dependencies = [ 1055 | "assert-json-diff", 1056 | "bytes", 1057 | "colored", 1058 | "futures-util", 1059 | "http", 1060 | "http-body", 1061 | "http-body-util", 1062 | "hyper", 1063 | "hyper-util", 1064 | "log", 1065 | "rand 0.9.1", 1066 | "regex", 1067 | "serde_json", 1068 | "serde_urlencoded", 1069 | "similar", 1070 | "tokio", 1071 | ] 1072 | 1073 | [[package]] 1074 | name = "nom" 1075 | version = "8.0.0" 1076 | source = "registry+https://github.com/rust-lang/crates.io-index" 1077 | checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" 1078 | dependencies = [ 1079 | "memchr", 1080 | ] 1081 | 1082 | [[package]] 1083 | name = "nu-ansi-term" 1084 | version = "0.46.0" 1085 | source = "registry+https://github.com/rust-lang/crates.io-index" 1086 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 1087 | dependencies = [ 1088 | "overload", 1089 | "winapi", 1090 | ] 1091 | 1092 | [[package]] 1093 | name = "num-modular" 1094 | version = "0.6.1" 1095 | source = "registry+https://github.com/rust-lang/crates.io-index" 1096 | checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" 1097 | 1098 | [[package]] 1099 | name = "num-order" 1100 | version = "1.2.0" 1101 | source = "registry+https://github.com/rust-lang/crates.io-index" 1102 | checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6" 1103 | dependencies = [ 1104 | "num-modular", 1105 | ] 1106 | 1107 | [[package]] 1108 | name = "num-traits" 1109 | version = "0.2.19" 1110 | source = "registry+https://github.com/rust-lang/crates.io-index" 1111 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 1112 | dependencies = [ 1113 | "autocfg", 1114 | ] 1115 | 1116 | [[package]] 1117 | name = "object" 1118 | version = "0.36.7" 1119 | source = "registry+https://github.com/rust-lang/crates.io-index" 1120 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 1121 | dependencies = [ 1122 | "memchr", 1123 | ] 1124 | 1125 | [[package]] 1126 | name = "once_cell" 1127 | version = "1.21.3" 1128 | source = "registry+https://github.com/rust-lang/crates.io-index" 1129 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 1130 | 1131 | [[package]] 1132 | name = "overload" 1133 | version = "0.1.1" 1134 | source = "registry+https://github.com/rust-lang/crates.io-index" 1135 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 1136 | 1137 | [[package]] 1138 | name = "parking_lot" 1139 | version = "0.12.3" 1140 | source = "registry+https://github.com/rust-lang/crates.io-index" 1141 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 1142 | dependencies = [ 1143 | "lock_api", 1144 | "parking_lot_core", 1145 | ] 1146 | 1147 | [[package]] 1148 | name = "parking_lot_core" 1149 | version = "0.9.10" 1150 | source = "registry+https://github.com/rust-lang/crates.io-index" 1151 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 1152 | dependencies = [ 1153 | "cfg-if", 1154 | "libc", 1155 | "redox_syscall", 1156 | "smallvec", 1157 | "windows-targets 0.52.6", 1158 | ] 1159 | 1160 | [[package]] 1161 | name = "parse-zoneinfo" 1162 | version = "0.3.1" 1163 | source = "registry+https://github.com/rust-lang/crates.io-index" 1164 | checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" 1165 | dependencies = [ 1166 | "regex", 1167 | ] 1168 | 1169 | [[package]] 1170 | name = "percent-encoding" 1171 | version = "2.3.1" 1172 | source = "registry+https://github.com/rust-lang/crates.io-index" 1173 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 1174 | 1175 | [[package]] 1176 | name = "pest" 1177 | version = "2.8.0" 1178 | source = "registry+https://github.com/rust-lang/crates.io-index" 1179 | checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" 1180 | dependencies = [ 1181 | "memchr", 1182 | "thiserror", 1183 | "ucd-trie", 1184 | ] 1185 | 1186 | [[package]] 1187 | name = "pest_derive" 1188 | version = "2.8.0" 1189 | source = "registry+https://github.com/rust-lang/crates.io-index" 1190 | checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" 1191 | dependencies = [ 1192 | "pest", 1193 | "pest_generator", 1194 | ] 1195 | 1196 | [[package]] 1197 | name = "pest_generator" 1198 | version = "2.8.0" 1199 | source = "registry+https://github.com/rust-lang/crates.io-index" 1200 | checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" 1201 | dependencies = [ 1202 | "pest", 1203 | "pest_meta", 1204 | "proc-macro2", 1205 | "quote", 1206 | "syn", 1207 | ] 1208 | 1209 | [[package]] 1210 | name = "pest_meta" 1211 | version = "2.8.0" 1212 | source = "registry+https://github.com/rust-lang/crates.io-index" 1213 | checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" 1214 | dependencies = [ 1215 | "once_cell", 1216 | "pest", 1217 | "sha2", 1218 | ] 1219 | 1220 | [[package]] 1221 | name = "phf" 1222 | version = "0.11.3" 1223 | source = "registry+https://github.com/rust-lang/crates.io-index" 1224 | checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" 1225 | dependencies = [ 1226 | "phf_shared", 1227 | ] 1228 | 1229 | [[package]] 1230 | name = "phf_codegen" 1231 | version = "0.11.3" 1232 | source = "registry+https://github.com/rust-lang/crates.io-index" 1233 | checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" 1234 | dependencies = [ 1235 | "phf_generator", 1236 | "phf_shared", 1237 | ] 1238 | 1239 | [[package]] 1240 | name = "phf_generator" 1241 | version = "0.11.3" 1242 | source = "registry+https://github.com/rust-lang/crates.io-index" 1243 | checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" 1244 | dependencies = [ 1245 | "phf_shared", 1246 | "rand 0.8.5", 1247 | ] 1248 | 1249 | [[package]] 1250 | name = "phf_shared" 1251 | version = "0.11.3" 1252 | source = "registry+https://github.com/rust-lang/crates.io-index" 1253 | checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" 1254 | dependencies = [ 1255 | "siphasher", 1256 | ] 1257 | 1258 | [[package]] 1259 | name = "pin-project-lite" 1260 | version = "0.2.16" 1261 | source = "registry+https://github.com/rust-lang/crates.io-index" 1262 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 1263 | 1264 | [[package]] 1265 | name = "pin-utils" 1266 | version = "0.1.0" 1267 | source = "registry+https://github.com/rust-lang/crates.io-index" 1268 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1269 | 1270 | [[package]] 1271 | name = "potential_utf" 1272 | version = "0.1.2" 1273 | source = "registry+https://github.com/rust-lang/crates.io-index" 1274 | checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" 1275 | dependencies = [ 1276 | "zerovec", 1277 | ] 1278 | 1279 | [[package]] 1280 | name = "ppv-lite86" 1281 | version = "0.2.21" 1282 | source = "registry+https://github.com/rust-lang/crates.io-index" 1283 | checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 1284 | dependencies = [ 1285 | "zerocopy", 1286 | ] 1287 | 1288 | [[package]] 1289 | name = "proc-macro2" 1290 | version = "1.0.95" 1291 | source = "registry+https://github.com/rust-lang/crates.io-index" 1292 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 1293 | dependencies = [ 1294 | "unicode-ident", 1295 | ] 1296 | 1297 | [[package]] 1298 | name = "psm" 1299 | version = "0.1.26" 1300 | source = "registry+https://github.com/rust-lang/crates.io-index" 1301 | checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f" 1302 | dependencies = [ 1303 | "cc", 1304 | ] 1305 | 1306 | [[package]] 1307 | name = "quinn" 1308 | version = "0.11.8" 1309 | source = "registry+https://github.com/rust-lang/crates.io-index" 1310 | checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" 1311 | dependencies = [ 1312 | "bytes", 1313 | "cfg_aliases", 1314 | "pin-project-lite", 1315 | "quinn-proto", 1316 | "quinn-udp", 1317 | "rustc-hash", 1318 | "rustls", 1319 | "socket2", 1320 | "thiserror", 1321 | "tokio", 1322 | "tracing", 1323 | "web-time", 1324 | ] 1325 | 1326 | [[package]] 1327 | name = "quinn-proto" 1328 | version = "0.11.12" 1329 | source = "registry+https://github.com/rust-lang/crates.io-index" 1330 | checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" 1331 | dependencies = [ 1332 | "bytes", 1333 | "getrandom 0.3.3", 1334 | "lru-slab", 1335 | "rand 0.9.1", 1336 | "ring", 1337 | "rustc-hash", 1338 | "rustls", 1339 | "rustls-pki-types", 1340 | "slab", 1341 | "thiserror", 1342 | "tinyvec", 1343 | "tracing", 1344 | "web-time", 1345 | ] 1346 | 1347 | [[package]] 1348 | name = "quinn-udp" 1349 | version = "0.5.12" 1350 | source = "registry+https://github.com/rust-lang/crates.io-index" 1351 | checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" 1352 | dependencies = [ 1353 | "cfg_aliases", 1354 | "libc", 1355 | "once_cell", 1356 | "socket2", 1357 | "tracing", 1358 | "windows-sys 0.59.0", 1359 | ] 1360 | 1361 | [[package]] 1362 | name = "quote" 1363 | version = "1.0.40" 1364 | source = "registry+https://github.com/rust-lang/crates.io-index" 1365 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 1366 | dependencies = [ 1367 | "proc-macro2", 1368 | ] 1369 | 1370 | [[package]] 1371 | name = "quoted_printable" 1372 | version = "0.5.1" 1373 | source = "registry+https://github.com/rust-lang/crates.io-index" 1374 | checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" 1375 | 1376 | [[package]] 1377 | name = "r-efi" 1378 | version = "5.2.0" 1379 | source = "registry+https://github.com/rust-lang/crates.io-index" 1380 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 1381 | 1382 | [[package]] 1383 | name = "rand" 1384 | version = "0.8.5" 1385 | source = "registry+https://github.com/rust-lang/crates.io-index" 1386 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1387 | dependencies = [ 1388 | "rand_core 0.6.4", 1389 | ] 1390 | 1391 | [[package]] 1392 | name = "rand" 1393 | version = "0.9.1" 1394 | source = "registry+https://github.com/rust-lang/crates.io-index" 1395 | checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" 1396 | dependencies = [ 1397 | "rand_chacha", 1398 | "rand_core 0.9.3", 1399 | ] 1400 | 1401 | [[package]] 1402 | name = "rand_chacha" 1403 | version = "0.9.0" 1404 | source = "registry+https://github.com/rust-lang/crates.io-index" 1405 | checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 1406 | dependencies = [ 1407 | "ppv-lite86", 1408 | "rand_core 0.9.3", 1409 | ] 1410 | 1411 | [[package]] 1412 | name = "rand_core" 1413 | version = "0.6.4" 1414 | source = "registry+https://github.com/rust-lang/crates.io-index" 1415 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1416 | 1417 | [[package]] 1418 | name = "rand_core" 1419 | version = "0.9.3" 1420 | source = "registry+https://github.com/rust-lang/crates.io-index" 1421 | checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 1422 | dependencies = [ 1423 | "getrandom 0.3.3", 1424 | ] 1425 | 1426 | [[package]] 1427 | name = "redox_syscall" 1428 | version = "0.5.12" 1429 | source = "registry+https://github.com/rust-lang/crates.io-index" 1430 | checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" 1431 | dependencies = [ 1432 | "bitflags", 1433 | ] 1434 | 1435 | [[package]] 1436 | name = "regex" 1437 | version = "1.11.1" 1438 | source = "registry+https://github.com/rust-lang/crates.io-index" 1439 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 1440 | dependencies = [ 1441 | "aho-corasick", 1442 | "memchr", 1443 | "regex-automata 0.4.9", 1444 | "regex-syntax 0.8.5", 1445 | ] 1446 | 1447 | [[package]] 1448 | name = "regex-automata" 1449 | version = "0.1.10" 1450 | source = "registry+https://github.com/rust-lang/crates.io-index" 1451 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 1452 | dependencies = [ 1453 | "regex-syntax 0.6.29", 1454 | ] 1455 | 1456 | [[package]] 1457 | name = "regex-automata" 1458 | version = "0.4.9" 1459 | source = "registry+https://github.com/rust-lang/crates.io-index" 1460 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 1461 | dependencies = [ 1462 | "aho-corasick", 1463 | "memchr", 1464 | "regex-syntax 0.8.5", 1465 | ] 1466 | 1467 | [[package]] 1468 | name = "regex-syntax" 1469 | version = "0.6.29" 1470 | source = "registry+https://github.com/rust-lang/crates.io-index" 1471 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 1472 | 1473 | [[package]] 1474 | name = "regex-syntax" 1475 | version = "0.8.5" 1476 | source = "registry+https://github.com/rust-lang/crates.io-index" 1477 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 1478 | 1479 | [[package]] 1480 | name = "reqwest" 1481 | version = "0.12.15" 1482 | source = "registry+https://github.com/rust-lang/crates.io-index" 1483 | checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" 1484 | dependencies = [ 1485 | "base64", 1486 | "bytes", 1487 | "futures-core", 1488 | "futures-util", 1489 | "http", 1490 | "http-body", 1491 | "http-body-util", 1492 | "hyper", 1493 | "hyper-rustls", 1494 | "hyper-util", 1495 | "ipnet", 1496 | "js-sys", 1497 | "log", 1498 | "mime", 1499 | "once_cell", 1500 | "percent-encoding", 1501 | "pin-project-lite", 1502 | "quinn", 1503 | "rustls", 1504 | "rustls-pemfile", 1505 | "rustls-pki-types", 1506 | "serde", 1507 | "serde_json", 1508 | "serde_urlencoded", 1509 | "sync_wrapper", 1510 | "tokio", 1511 | "tokio-rustls", 1512 | "tower", 1513 | "tower-service", 1514 | "url", 1515 | "wasm-bindgen", 1516 | "wasm-bindgen-futures", 1517 | "web-sys", 1518 | "webpki-roots 0.26.11", 1519 | "windows-registry", 1520 | ] 1521 | 1522 | [[package]] 1523 | name = "ring" 1524 | version = "0.17.14" 1525 | source = "registry+https://github.com/rust-lang/crates.io-index" 1526 | checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 1527 | dependencies = [ 1528 | "cc", 1529 | "cfg-if", 1530 | "getrandom 0.2.16", 1531 | "libc", 1532 | "untrusted", 1533 | "windows-sys 0.52.0", 1534 | ] 1535 | 1536 | [[package]] 1537 | name = "rustc-demangle" 1538 | version = "0.1.24" 1539 | source = "registry+https://github.com/rust-lang/crates.io-index" 1540 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 1541 | 1542 | [[package]] 1543 | name = "rustc-hash" 1544 | version = "2.1.1" 1545 | source = "registry+https://github.com/rust-lang/crates.io-index" 1546 | checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 1547 | 1548 | [[package]] 1549 | name = "rustls" 1550 | version = "0.23.27" 1551 | source = "registry+https://github.com/rust-lang/crates.io-index" 1552 | checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" 1553 | dependencies = [ 1554 | "log", 1555 | "once_cell", 1556 | "ring", 1557 | "rustls-pki-types", 1558 | "rustls-webpki", 1559 | "subtle", 1560 | "zeroize", 1561 | ] 1562 | 1563 | [[package]] 1564 | name = "rustls-pemfile" 1565 | version = "2.2.0" 1566 | source = "registry+https://github.com/rust-lang/crates.io-index" 1567 | checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" 1568 | dependencies = [ 1569 | "rustls-pki-types", 1570 | ] 1571 | 1572 | [[package]] 1573 | name = "rustls-pki-types" 1574 | version = "1.12.0" 1575 | source = "registry+https://github.com/rust-lang/crates.io-index" 1576 | checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" 1577 | dependencies = [ 1578 | "web-time", 1579 | "zeroize", 1580 | ] 1581 | 1582 | [[package]] 1583 | name = "rustls-webpki" 1584 | version = "0.103.3" 1585 | source = "registry+https://github.com/rust-lang/crates.io-index" 1586 | checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" 1587 | dependencies = [ 1588 | "ring", 1589 | "rustls-pki-types", 1590 | "untrusted", 1591 | ] 1592 | 1593 | [[package]] 1594 | name = "rustversion" 1595 | version = "1.0.20" 1596 | source = "registry+https://github.com/rust-lang/crates.io-index" 1597 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 1598 | 1599 | [[package]] 1600 | name = "ryu" 1601 | version = "1.0.20" 1602 | source = "registry+https://github.com/rust-lang/crates.io-index" 1603 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 1604 | 1605 | [[package]] 1606 | name = "same-file" 1607 | version = "1.0.6" 1608 | source = "registry+https://github.com/rust-lang/crates.io-index" 1609 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 1610 | dependencies = [ 1611 | "winapi-util", 1612 | ] 1613 | 1614 | [[package]] 1615 | name = "scopeguard" 1616 | version = "1.2.0" 1617 | source = "registry+https://github.com/rust-lang/crates.io-index" 1618 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1619 | 1620 | [[package]] 1621 | name = "serde" 1622 | version = "1.0.219" 1623 | source = "registry+https://github.com/rust-lang/crates.io-index" 1624 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 1625 | dependencies = [ 1626 | "serde_derive", 1627 | ] 1628 | 1629 | [[package]] 1630 | name = "serde_derive" 1631 | version = "1.0.219" 1632 | source = "registry+https://github.com/rust-lang/crates.io-index" 1633 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 1634 | dependencies = [ 1635 | "proc-macro2", 1636 | "quote", 1637 | "syn", 1638 | ] 1639 | 1640 | [[package]] 1641 | name = "serde_json" 1642 | version = "1.0.140" 1643 | source = "registry+https://github.com/rust-lang/crates.io-index" 1644 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 1645 | dependencies = [ 1646 | "itoa", 1647 | "memchr", 1648 | "ryu", 1649 | "serde", 1650 | ] 1651 | 1652 | [[package]] 1653 | name = "serde_spanned" 1654 | version = "0.6.8" 1655 | source = "registry+https://github.com/rust-lang/crates.io-index" 1656 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" 1657 | dependencies = [ 1658 | "serde", 1659 | ] 1660 | 1661 | [[package]] 1662 | name = "serde_urlencoded" 1663 | version = "0.7.1" 1664 | source = "registry+https://github.com/rust-lang/crates.io-index" 1665 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1666 | dependencies = [ 1667 | "form_urlencoded", 1668 | "itoa", 1669 | "ryu", 1670 | "serde", 1671 | ] 1672 | 1673 | [[package]] 1674 | name = "sha2" 1675 | version = "0.10.9" 1676 | source = "registry+https://github.com/rust-lang/crates.io-index" 1677 | checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" 1678 | dependencies = [ 1679 | "cfg-if", 1680 | "cpufeatures", 1681 | "digest", 1682 | ] 1683 | 1684 | [[package]] 1685 | name = "sharded-slab" 1686 | version = "0.1.7" 1687 | source = "registry+https://github.com/rust-lang/crates.io-index" 1688 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 1689 | dependencies = [ 1690 | "lazy_static", 1691 | ] 1692 | 1693 | [[package]] 1694 | name = "shlex" 1695 | version = "1.3.0" 1696 | source = "registry+https://github.com/rust-lang/crates.io-index" 1697 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1698 | 1699 | [[package]] 1700 | name = "signal-hook-registry" 1701 | version = "1.4.5" 1702 | source = "registry+https://github.com/rust-lang/crates.io-index" 1703 | checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" 1704 | dependencies = [ 1705 | "libc", 1706 | ] 1707 | 1708 | [[package]] 1709 | name = "similar" 1710 | version = "2.7.0" 1711 | source = "registry+https://github.com/rust-lang/crates.io-index" 1712 | checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" 1713 | 1714 | [[package]] 1715 | name = "siphasher" 1716 | version = "1.0.1" 1717 | source = "registry+https://github.com/rust-lang/crates.io-index" 1718 | checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" 1719 | 1720 | [[package]] 1721 | name = "slab" 1722 | version = "0.4.9" 1723 | source = "registry+https://github.com/rust-lang/crates.io-index" 1724 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1725 | dependencies = [ 1726 | "autocfg", 1727 | ] 1728 | 1729 | [[package]] 1730 | name = "smallvec" 1731 | version = "1.15.0" 1732 | source = "registry+https://github.com/rust-lang/crates.io-index" 1733 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 1734 | 1735 | [[package]] 1736 | name = "socket2" 1737 | version = "0.5.9" 1738 | source = "registry+https://github.com/rust-lang/crates.io-index" 1739 | checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" 1740 | dependencies = [ 1741 | "libc", 1742 | "windows-sys 0.52.0", 1743 | ] 1744 | 1745 | [[package]] 1746 | name = "stable_deref_trait" 1747 | version = "1.2.0" 1748 | source = "registry+https://github.com/rust-lang/crates.io-index" 1749 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1750 | 1751 | [[package]] 1752 | name = "stacker" 1753 | version = "0.1.21" 1754 | source = "registry+https://github.com/rust-lang/crates.io-index" 1755 | checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b" 1756 | dependencies = [ 1757 | "cc", 1758 | "cfg-if", 1759 | "libc", 1760 | "psm", 1761 | "windows-sys 0.59.0", 1762 | ] 1763 | 1764 | [[package]] 1765 | name = "strsim" 1766 | version = "0.11.1" 1767 | source = "registry+https://github.com/rust-lang/crates.io-index" 1768 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1769 | 1770 | [[package]] 1771 | name = "subtle" 1772 | version = "2.6.1" 1773 | source = "registry+https://github.com/rust-lang/crates.io-index" 1774 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 1775 | 1776 | [[package]] 1777 | name = "syn" 1778 | version = "2.0.101" 1779 | source = "registry+https://github.com/rust-lang/crates.io-index" 1780 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 1781 | dependencies = [ 1782 | "proc-macro2", 1783 | "quote", 1784 | "unicode-ident", 1785 | ] 1786 | 1787 | [[package]] 1788 | name = "sync_wrapper" 1789 | version = "1.0.2" 1790 | source = "registry+https://github.com/rust-lang/crates.io-index" 1791 | checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 1792 | dependencies = [ 1793 | "futures-core", 1794 | ] 1795 | 1796 | [[package]] 1797 | name = "synstructure" 1798 | version = "0.13.2" 1799 | source = "registry+https://github.com/rust-lang/crates.io-index" 1800 | checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 1801 | dependencies = [ 1802 | "proc-macro2", 1803 | "quote", 1804 | "syn", 1805 | ] 1806 | 1807 | [[package]] 1808 | name = "thiserror" 1809 | version = "2.0.12" 1810 | source = "registry+https://github.com/rust-lang/crates.io-index" 1811 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 1812 | dependencies = [ 1813 | "thiserror-impl", 1814 | ] 1815 | 1816 | [[package]] 1817 | name = "thiserror-impl" 1818 | version = "2.0.12" 1819 | source = "registry+https://github.com/rust-lang/crates.io-index" 1820 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 1821 | dependencies = [ 1822 | "proc-macro2", 1823 | "quote", 1824 | "syn", 1825 | ] 1826 | 1827 | [[package]] 1828 | name = "thread_local" 1829 | version = "1.1.8" 1830 | source = "registry+https://github.com/rust-lang/crates.io-index" 1831 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 1832 | dependencies = [ 1833 | "cfg-if", 1834 | "once_cell", 1835 | ] 1836 | 1837 | [[package]] 1838 | name = "tinystr" 1839 | version = "0.8.1" 1840 | source = "registry+https://github.com/rust-lang/crates.io-index" 1841 | checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" 1842 | dependencies = [ 1843 | "displaydoc", 1844 | "zerovec", 1845 | ] 1846 | 1847 | [[package]] 1848 | name = "tinyvec" 1849 | version = "1.9.0" 1850 | source = "registry+https://github.com/rust-lang/crates.io-index" 1851 | checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" 1852 | dependencies = [ 1853 | "tinyvec_macros", 1854 | ] 1855 | 1856 | [[package]] 1857 | name = "tinyvec_macros" 1858 | version = "0.1.1" 1859 | source = "registry+https://github.com/rust-lang/crates.io-index" 1860 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1861 | 1862 | [[package]] 1863 | name = "tokio" 1864 | version = "1.45.0" 1865 | source = "registry+https://github.com/rust-lang/crates.io-index" 1866 | checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" 1867 | dependencies = [ 1868 | "backtrace", 1869 | "bytes", 1870 | "libc", 1871 | "mio", 1872 | "parking_lot", 1873 | "pin-project-lite", 1874 | "signal-hook-registry", 1875 | "socket2", 1876 | "tokio-macros", 1877 | "windows-sys 0.52.0", 1878 | ] 1879 | 1880 | [[package]] 1881 | name = "tokio-macros" 1882 | version = "2.5.0" 1883 | source = "registry+https://github.com/rust-lang/crates.io-index" 1884 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 1885 | dependencies = [ 1886 | "proc-macro2", 1887 | "quote", 1888 | "syn", 1889 | ] 1890 | 1891 | [[package]] 1892 | name = "tokio-rustls" 1893 | version = "0.26.2" 1894 | source = "registry+https://github.com/rust-lang/crates.io-index" 1895 | checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" 1896 | dependencies = [ 1897 | "rustls", 1898 | "tokio", 1899 | ] 1900 | 1901 | [[package]] 1902 | name = "tokio-util" 1903 | version = "0.7.15" 1904 | source = "registry+https://github.com/rust-lang/crates.io-index" 1905 | checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" 1906 | dependencies = [ 1907 | "bytes", 1908 | "futures-core", 1909 | "futures-sink", 1910 | "pin-project-lite", 1911 | "tokio", 1912 | ] 1913 | 1914 | [[package]] 1915 | name = "toml" 1916 | version = "0.8.22" 1917 | source = "registry+https://github.com/rust-lang/crates.io-index" 1918 | checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" 1919 | dependencies = [ 1920 | "serde", 1921 | "serde_spanned", 1922 | "toml_datetime", 1923 | "toml_edit", 1924 | ] 1925 | 1926 | [[package]] 1927 | name = "toml_datetime" 1928 | version = "0.6.9" 1929 | source = "registry+https://github.com/rust-lang/crates.io-index" 1930 | checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" 1931 | dependencies = [ 1932 | "serde", 1933 | ] 1934 | 1935 | [[package]] 1936 | name = "toml_edit" 1937 | version = "0.22.26" 1938 | source = "registry+https://github.com/rust-lang/crates.io-index" 1939 | checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" 1940 | dependencies = [ 1941 | "indexmap", 1942 | "serde", 1943 | "serde_spanned", 1944 | "toml_datetime", 1945 | "toml_write", 1946 | "winnow", 1947 | ] 1948 | 1949 | [[package]] 1950 | name = "toml_write" 1951 | version = "0.1.1" 1952 | source = "registry+https://github.com/rust-lang/crates.io-index" 1953 | checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" 1954 | 1955 | [[package]] 1956 | name = "tower" 1957 | version = "0.5.2" 1958 | source = "registry+https://github.com/rust-lang/crates.io-index" 1959 | checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 1960 | dependencies = [ 1961 | "futures-core", 1962 | "futures-util", 1963 | "pin-project-lite", 1964 | "sync_wrapper", 1965 | "tokio", 1966 | "tower-layer", 1967 | "tower-service", 1968 | ] 1969 | 1970 | [[package]] 1971 | name = "tower-layer" 1972 | version = "0.3.3" 1973 | source = "registry+https://github.com/rust-lang/crates.io-index" 1974 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 1975 | 1976 | [[package]] 1977 | name = "tower-service" 1978 | version = "0.3.3" 1979 | source = "registry+https://github.com/rust-lang/crates.io-index" 1980 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1981 | 1982 | [[package]] 1983 | name = "tracing" 1984 | version = "0.1.41" 1985 | source = "registry+https://github.com/rust-lang/crates.io-index" 1986 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1987 | dependencies = [ 1988 | "pin-project-lite", 1989 | "tracing-attributes", 1990 | "tracing-core", 1991 | ] 1992 | 1993 | [[package]] 1994 | name = "tracing-attributes" 1995 | version = "0.1.28" 1996 | source = "registry+https://github.com/rust-lang/crates.io-index" 1997 | checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" 1998 | dependencies = [ 1999 | "proc-macro2", 2000 | "quote", 2001 | "syn", 2002 | ] 2003 | 2004 | [[package]] 2005 | name = "tracing-core" 2006 | version = "0.1.33" 2007 | source = "registry+https://github.com/rust-lang/crates.io-index" 2008 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 2009 | dependencies = [ 2010 | "once_cell", 2011 | "valuable", 2012 | ] 2013 | 2014 | [[package]] 2015 | name = "tracing-log" 2016 | version = "0.2.0" 2017 | source = "registry+https://github.com/rust-lang/crates.io-index" 2018 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 2019 | dependencies = [ 2020 | "log", 2021 | "once_cell", 2022 | "tracing-core", 2023 | ] 2024 | 2025 | [[package]] 2026 | name = "tracing-subscriber" 2027 | version = "0.3.19" 2028 | source = "registry+https://github.com/rust-lang/crates.io-index" 2029 | checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" 2030 | dependencies = [ 2031 | "matchers", 2032 | "nu-ansi-term", 2033 | "once_cell", 2034 | "regex", 2035 | "sharded-slab", 2036 | "smallvec", 2037 | "thread_local", 2038 | "tracing", 2039 | "tracing-core", 2040 | "tracing-log", 2041 | ] 2042 | 2043 | [[package]] 2044 | name = "try-lock" 2045 | version = "0.2.5" 2046 | source = "registry+https://github.com/rust-lang/crates.io-index" 2047 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 2048 | 2049 | [[package]] 2050 | name = "typenum" 2051 | version = "1.18.0" 2052 | source = "registry+https://github.com/rust-lang/crates.io-index" 2053 | checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" 2054 | 2055 | [[package]] 2056 | name = "ucd-trie" 2057 | version = "0.1.7" 2058 | source = "registry+https://github.com/rust-lang/crates.io-index" 2059 | checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" 2060 | 2061 | [[package]] 2062 | name = "umami-alerts" 2063 | version = "0.1.1" 2064 | dependencies = [ 2065 | "chrono", 2066 | "chrono-tz", 2067 | "clap", 2068 | "futures", 2069 | "handlebars", 2070 | "lettre", 2071 | "mockito", 2072 | "reqwest", 2073 | "serde", 2074 | "serde_json", 2075 | "thiserror", 2076 | "tokio", 2077 | "toml", 2078 | "tracing", 2079 | "tracing-subscriber", 2080 | "url", 2081 | ] 2082 | 2083 | [[package]] 2084 | name = "unicode-ident" 2085 | version = "1.0.18" 2086 | source = "registry+https://github.com/rust-lang/crates.io-index" 2087 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 2088 | 2089 | [[package]] 2090 | name = "untrusted" 2091 | version = "0.9.0" 2092 | source = "registry+https://github.com/rust-lang/crates.io-index" 2093 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 2094 | 2095 | [[package]] 2096 | name = "url" 2097 | version = "2.5.4" 2098 | source = "registry+https://github.com/rust-lang/crates.io-index" 2099 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 2100 | dependencies = [ 2101 | "form_urlencoded", 2102 | "idna", 2103 | "percent-encoding", 2104 | ] 2105 | 2106 | [[package]] 2107 | name = "utf8_iter" 2108 | version = "1.0.4" 2109 | source = "registry+https://github.com/rust-lang/crates.io-index" 2110 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 2111 | 2112 | [[package]] 2113 | name = "utf8parse" 2114 | version = "0.2.2" 2115 | source = "registry+https://github.com/rust-lang/crates.io-index" 2116 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 2117 | 2118 | [[package]] 2119 | name = "valuable" 2120 | version = "0.1.1" 2121 | source = "registry+https://github.com/rust-lang/crates.io-index" 2122 | checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 2123 | 2124 | [[package]] 2125 | name = "version_check" 2126 | version = "0.9.5" 2127 | source = "registry+https://github.com/rust-lang/crates.io-index" 2128 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 2129 | 2130 | [[package]] 2131 | name = "walkdir" 2132 | version = "2.5.0" 2133 | source = "registry+https://github.com/rust-lang/crates.io-index" 2134 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 2135 | dependencies = [ 2136 | "same-file", 2137 | "winapi-util", 2138 | ] 2139 | 2140 | [[package]] 2141 | name = "want" 2142 | version = "0.3.1" 2143 | source = "registry+https://github.com/rust-lang/crates.io-index" 2144 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 2145 | dependencies = [ 2146 | "try-lock", 2147 | ] 2148 | 2149 | [[package]] 2150 | name = "wasi" 2151 | version = "0.11.0+wasi-snapshot-preview1" 2152 | source = "registry+https://github.com/rust-lang/crates.io-index" 2153 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 2154 | 2155 | [[package]] 2156 | name = "wasi" 2157 | version = "0.14.2+wasi-0.2.4" 2158 | source = "registry+https://github.com/rust-lang/crates.io-index" 2159 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 2160 | dependencies = [ 2161 | "wit-bindgen-rt", 2162 | ] 2163 | 2164 | [[package]] 2165 | name = "wasm-bindgen" 2166 | version = "0.2.100" 2167 | source = "registry+https://github.com/rust-lang/crates.io-index" 2168 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 2169 | dependencies = [ 2170 | "cfg-if", 2171 | "once_cell", 2172 | "rustversion", 2173 | "wasm-bindgen-macro", 2174 | ] 2175 | 2176 | [[package]] 2177 | name = "wasm-bindgen-backend" 2178 | version = "0.2.100" 2179 | source = "registry+https://github.com/rust-lang/crates.io-index" 2180 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 2181 | dependencies = [ 2182 | "bumpalo", 2183 | "log", 2184 | "proc-macro2", 2185 | "quote", 2186 | "syn", 2187 | "wasm-bindgen-shared", 2188 | ] 2189 | 2190 | [[package]] 2191 | name = "wasm-bindgen-futures" 2192 | version = "0.4.50" 2193 | source = "registry+https://github.com/rust-lang/crates.io-index" 2194 | checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" 2195 | dependencies = [ 2196 | "cfg-if", 2197 | "js-sys", 2198 | "once_cell", 2199 | "wasm-bindgen", 2200 | "web-sys", 2201 | ] 2202 | 2203 | [[package]] 2204 | name = "wasm-bindgen-macro" 2205 | version = "0.2.100" 2206 | source = "registry+https://github.com/rust-lang/crates.io-index" 2207 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 2208 | dependencies = [ 2209 | "quote", 2210 | "wasm-bindgen-macro-support", 2211 | ] 2212 | 2213 | [[package]] 2214 | name = "wasm-bindgen-macro-support" 2215 | version = "0.2.100" 2216 | source = "registry+https://github.com/rust-lang/crates.io-index" 2217 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 2218 | dependencies = [ 2219 | "proc-macro2", 2220 | "quote", 2221 | "syn", 2222 | "wasm-bindgen-backend", 2223 | "wasm-bindgen-shared", 2224 | ] 2225 | 2226 | [[package]] 2227 | name = "wasm-bindgen-shared" 2228 | version = "0.2.100" 2229 | source = "registry+https://github.com/rust-lang/crates.io-index" 2230 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 2231 | dependencies = [ 2232 | "unicode-ident", 2233 | ] 2234 | 2235 | [[package]] 2236 | name = "web-sys" 2237 | version = "0.3.77" 2238 | source = "registry+https://github.com/rust-lang/crates.io-index" 2239 | checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 2240 | dependencies = [ 2241 | "js-sys", 2242 | "wasm-bindgen", 2243 | ] 2244 | 2245 | [[package]] 2246 | name = "web-time" 2247 | version = "1.1.0" 2248 | source = "registry+https://github.com/rust-lang/crates.io-index" 2249 | checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 2250 | dependencies = [ 2251 | "js-sys", 2252 | "wasm-bindgen", 2253 | ] 2254 | 2255 | [[package]] 2256 | name = "webpki-roots" 2257 | version = "0.26.11" 2258 | source = "registry+https://github.com/rust-lang/crates.io-index" 2259 | checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" 2260 | dependencies = [ 2261 | "webpki-roots 1.0.0", 2262 | ] 2263 | 2264 | [[package]] 2265 | name = "webpki-roots" 2266 | version = "1.0.0" 2267 | source = "registry+https://github.com/rust-lang/crates.io-index" 2268 | checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" 2269 | dependencies = [ 2270 | "rustls-pki-types", 2271 | ] 2272 | 2273 | [[package]] 2274 | name = "winapi" 2275 | version = "0.3.9" 2276 | source = "registry+https://github.com/rust-lang/crates.io-index" 2277 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 2278 | dependencies = [ 2279 | "winapi-i686-pc-windows-gnu", 2280 | "winapi-x86_64-pc-windows-gnu", 2281 | ] 2282 | 2283 | [[package]] 2284 | name = "winapi-i686-pc-windows-gnu" 2285 | version = "0.4.0" 2286 | source = "registry+https://github.com/rust-lang/crates.io-index" 2287 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 2288 | 2289 | [[package]] 2290 | name = "winapi-util" 2291 | version = "0.1.9" 2292 | source = "registry+https://github.com/rust-lang/crates.io-index" 2293 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 2294 | dependencies = [ 2295 | "windows-sys 0.59.0", 2296 | ] 2297 | 2298 | [[package]] 2299 | name = "winapi-x86_64-pc-windows-gnu" 2300 | version = "0.4.0" 2301 | source = "registry+https://github.com/rust-lang/crates.io-index" 2302 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 2303 | 2304 | [[package]] 2305 | name = "windows-core" 2306 | version = "0.61.2" 2307 | source = "registry+https://github.com/rust-lang/crates.io-index" 2308 | checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" 2309 | dependencies = [ 2310 | "windows-implement", 2311 | "windows-interface", 2312 | "windows-link", 2313 | "windows-result", 2314 | "windows-strings 0.4.2", 2315 | ] 2316 | 2317 | [[package]] 2318 | name = "windows-implement" 2319 | version = "0.60.0" 2320 | source = "registry+https://github.com/rust-lang/crates.io-index" 2321 | checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" 2322 | dependencies = [ 2323 | "proc-macro2", 2324 | "quote", 2325 | "syn", 2326 | ] 2327 | 2328 | [[package]] 2329 | name = "windows-interface" 2330 | version = "0.59.1" 2331 | source = "registry+https://github.com/rust-lang/crates.io-index" 2332 | checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" 2333 | dependencies = [ 2334 | "proc-macro2", 2335 | "quote", 2336 | "syn", 2337 | ] 2338 | 2339 | [[package]] 2340 | name = "windows-link" 2341 | version = "0.1.1" 2342 | source = "registry+https://github.com/rust-lang/crates.io-index" 2343 | checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" 2344 | 2345 | [[package]] 2346 | name = "windows-registry" 2347 | version = "0.4.0" 2348 | source = "registry+https://github.com/rust-lang/crates.io-index" 2349 | checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" 2350 | dependencies = [ 2351 | "windows-result", 2352 | "windows-strings 0.3.1", 2353 | "windows-targets 0.53.0", 2354 | ] 2355 | 2356 | [[package]] 2357 | name = "windows-result" 2358 | version = "0.3.4" 2359 | source = "registry+https://github.com/rust-lang/crates.io-index" 2360 | checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" 2361 | dependencies = [ 2362 | "windows-link", 2363 | ] 2364 | 2365 | [[package]] 2366 | name = "windows-strings" 2367 | version = "0.3.1" 2368 | source = "registry+https://github.com/rust-lang/crates.io-index" 2369 | checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" 2370 | dependencies = [ 2371 | "windows-link", 2372 | ] 2373 | 2374 | [[package]] 2375 | name = "windows-strings" 2376 | version = "0.4.2" 2377 | source = "registry+https://github.com/rust-lang/crates.io-index" 2378 | checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 2379 | dependencies = [ 2380 | "windows-link", 2381 | ] 2382 | 2383 | [[package]] 2384 | name = "windows-sys" 2385 | version = "0.52.0" 2386 | source = "registry+https://github.com/rust-lang/crates.io-index" 2387 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 2388 | dependencies = [ 2389 | "windows-targets 0.52.6", 2390 | ] 2391 | 2392 | [[package]] 2393 | name = "windows-sys" 2394 | version = "0.59.0" 2395 | source = "registry+https://github.com/rust-lang/crates.io-index" 2396 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 2397 | dependencies = [ 2398 | "windows-targets 0.52.6", 2399 | ] 2400 | 2401 | [[package]] 2402 | name = "windows-targets" 2403 | version = "0.52.6" 2404 | source = "registry+https://github.com/rust-lang/crates.io-index" 2405 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 2406 | dependencies = [ 2407 | "windows_aarch64_gnullvm 0.52.6", 2408 | "windows_aarch64_msvc 0.52.6", 2409 | "windows_i686_gnu 0.52.6", 2410 | "windows_i686_gnullvm 0.52.6", 2411 | "windows_i686_msvc 0.52.6", 2412 | "windows_x86_64_gnu 0.52.6", 2413 | "windows_x86_64_gnullvm 0.52.6", 2414 | "windows_x86_64_msvc 0.52.6", 2415 | ] 2416 | 2417 | [[package]] 2418 | name = "windows-targets" 2419 | version = "0.53.0" 2420 | source = "registry+https://github.com/rust-lang/crates.io-index" 2421 | checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" 2422 | dependencies = [ 2423 | "windows_aarch64_gnullvm 0.53.0", 2424 | "windows_aarch64_msvc 0.53.0", 2425 | "windows_i686_gnu 0.53.0", 2426 | "windows_i686_gnullvm 0.53.0", 2427 | "windows_i686_msvc 0.53.0", 2428 | "windows_x86_64_gnu 0.53.0", 2429 | "windows_x86_64_gnullvm 0.53.0", 2430 | "windows_x86_64_msvc 0.53.0", 2431 | ] 2432 | 2433 | [[package]] 2434 | name = "windows_aarch64_gnullvm" 2435 | version = "0.52.6" 2436 | source = "registry+https://github.com/rust-lang/crates.io-index" 2437 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 2438 | 2439 | [[package]] 2440 | name = "windows_aarch64_gnullvm" 2441 | version = "0.53.0" 2442 | source = "registry+https://github.com/rust-lang/crates.io-index" 2443 | checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 2444 | 2445 | [[package]] 2446 | name = "windows_aarch64_msvc" 2447 | version = "0.52.6" 2448 | source = "registry+https://github.com/rust-lang/crates.io-index" 2449 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 2450 | 2451 | [[package]] 2452 | name = "windows_aarch64_msvc" 2453 | version = "0.53.0" 2454 | source = "registry+https://github.com/rust-lang/crates.io-index" 2455 | checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 2456 | 2457 | [[package]] 2458 | name = "windows_i686_gnu" 2459 | version = "0.52.6" 2460 | source = "registry+https://github.com/rust-lang/crates.io-index" 2461 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 2462 | 2463 | [[package]] 2464 | name = "windows_i686_gnu" 2465 | version = "0.53.0" 2466 | source = "registry+https://github.com/rust-lang/crates.io-index" 2467 | checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 2468 | 2469 | [[package]] 2470 | name = "windows_i686_gnullvm" 2471 | version = "0.52.6" 2472 | source = "registry+https://github.com/rust-lang/crates.io-index" 2473 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 2474 | 2475 | [[package]] 2476 | name = "windows_i686_gnullvm" 2477 | version = "0.53.0" 2478 | source = "registry+https://github.com/rust-lang/crates.io-index" 2479 | checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 2480 | 2481 | [[package]] 2482 | name = "windows_i686_msvc" 2483 | version = "0.52.6" 2484 | source = "registry+https://github.com/rust-lang/crates.io-index" 2485 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 2486 | 2487 | [[package]] 2488 | name = "windows_i686_msvc" 2489 | version = "0.53.0" 2490 | source = "registry+https://github.com/rust-lang/crates.io-index" 2491 | checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 2492 | 2493 | [[package]] 2494 | name = "windows_x86_64_gnu" 2495 | version = "0.52.6" 2496 | source = "registry+https://github.com/rust-lang/crates.io-index" 2497 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 2498 | 2499 | [[package]] 2500 | name = "windows_x86_64_gnu" 2501 | version = "0.53.0" 2502 | source = "registry+https://github.com/rust-lang/crates.io-index" 2503 | checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 2504 | 2505 | [[package]] 2506 | name = "windows_x86_64_gnullvm" 2507 | version = "0.52.6" 2508 | source = "registry+https://github.com/rust-lang/crates.io-index" 2509 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 2510 | 2511 | [[package]] 2512 | name = "windows_x86_64_gnullvm" 2513 | version = "0.53.0" 2514 | source = "registry+https://github.com/rust-lang/crates.io-index" 2515 | checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 2516 | 2517 | [[package]] 2518 | name = "windows_x86_64_msvc" 2519 | version = "0.52.6" 2520 | source = "registry+https://github.com/rust-lang/crates.io-index" 2521 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 2522 | 2523 | [[package]] 2524 | name = "windows_x86_64_msvc" 2525 | version = "0.53.0" 2526 | source = "registry+https://github.com/rust-lang/crates.io-index" 2527 | checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 2528 | 2529 | [[package]] 2530 | name = "winnow" 2531 | version = "0.7.10" 2532 | source = "registry+https://github.com/rust-lang/crates.io-index" 2533 | checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" 2534 | dependencies = [ 2535 | "memchr", 2536 | ] 2537 | 2538 | [[package]] 2539 | name = "wit-bindgen-rt" 2540 | version = "0.39.0" 2541 | source = "registry+https://github.com/rust-lang/crates.io-index" 2542 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 2543 | dependencies = [ 2544 | "bitflags", 2545 | ] 2546 | 2547 | [[package]] 2548 | name = "writeable" 2549 | version = "0.6.1" 2550 | source = "registry+https://github.com/rust-lang/crates.io-index" 2551 | checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" 2552 | 2553 | [[package]] 2554 | name = "yoke" 2555 | version = "0.8.0" 2556 | source = "registry+https://github.com/rust-lang/crates.io-index" 2557 | checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" 2558 | dependencies = [ 2559 | "serde", 2560 | "stable_deref_trait", 2561 | "yoke-derive", 2562 | "zerofrom", 2563 | ] 2564 | 2565 | [[package]] 2566 | name = "yoke-derive" 2567 | version = "0.8.0" 2568 | source = "registry+https://github.com/rust-lang/crates.io-index" 2569 | checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" 2570 | dependencies = [ 2571 | "proc-macro2", 2572 | "quote", 2573 | "syn", 2574 | "synstructure", 2575 | ] 2576 | 2577 | [[package]] 2578 | name = "zerocopy" 2579 | version = "0.8.25" 2580 | source = "registry+https://github.com/rust-lang/crates.io-index" 2581 | checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" 2582 | dependencies = [ 2583 | "zerocopy-derive", 2584 | ] 2585 | 2586 | [[package]] 2587 | name = "zerocopy-derive" 2588 | version = "0.8.25" 2589 | source = "registry+https://github.com/rust-lang/crates.io-index" 2590 | checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" 2591 | dependencies = [ 2592 | "proc-macro2", 2593 | "quote", 2594 | "syn", 2595 | ] 2596 | 2597 | [[package]] 2598 | name = "zerofrom" 2599 | version = "0.1.6" 2600 | source = "registry+https://github.com/rust-lang/crates.io-index" 2601 | checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 2602 | dependencies = [ 2603 | "zerofrom-derive", 2604 | ] 2605 | 2606 | [[package]] 2607 | name = "zerofrom-derive" 2608 | version = "0.1.6" 2609 | source = "registry+https://github.com/rust-lang/crates.io-index" 2610 | checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 2611 | dependencies = [ 2612 | "proc-macro2", 2613 | "quote", 2614 | "syn", 2615 | "synstructure", 2616 | ] 2617 | 2618 | [[package]] 2619 | name = "zeroize" 2620 | version = "1.8.1" 2621 | source = "registry+https://github.com/rust-lang/crates.io-index" 2622 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 2623 | 2624 | [[package]] 2625 | name = "zerotrie" 2626 | version = "0.2.2" 2627 | source = "registry+https://github.com/rust-lang/crates.io-index" 2628 | checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" 2629 | dependencies = [ 2630 | "displaydoc", 2631 | "yoke", 2632 | "zerofrom", 2633 | ] 2634 | 2635 | [[package]] 2636 | name = "zerovec" 2637 | version = "0.11.2" 2638 | source = "registry+https://github.com/rust-lang/crates.io-index" 2639 | checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" 2640 | dependencies = [ 2641 | "yoke", 2642 | "zerofrom", 2643 | "zerovec-derive", 2644 | ] 2645 | 2646 | [[package]] 2647 | name = "zerovec-derive" 2648 | version = "0.11.1" 2649 | source = "registry+https://github.com/rust-lang/crates.io-index" 2650 | checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" 2651 | dependencies = [ 2652 | "proc-macro2", 2653 | "quote", 2654 | "syn", 2655 | ] 2656 | --------------------------------------------------------------------------------