├── .dockerignore ├── .envrc ├── .github └── workflows │ └── image.yml ├── .gitignore ├── .vscode └── launch.json ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── flake.lock ├── flake.nix ├── shell.nix └── src ├── alert.rs ├── args.rs ├── macros.rs ├── mail.rs ├── main.rs └── run.rs /.dockerignore: -------------------------------------------------------------------------------- 1 | /target/debug 2 | /data 3 | Dockerfile -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.github/workflows/image.yml: -------------------------------------------------------------------------------- 1 | name: Build giggio/speedtest image 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Install Rust Stable toolchain 18 | run: rustup update stable && rustup default stable 19 | 20 | - name: Install Cross 21 | run: | 22 | curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash 23 | cargo binstall cross 24 | 25 | - name: Set up Docker Buildx 26 | uses: docker/setup-buildx-action@v3 27 | with: 28 | version: latest 29 | 30 | - name: Set up QEMU 31 | uses: docker/setup-qemu-action@v3 32 | 33 | - name: Login to docker hub 34 | run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u giggio --password-stdin 35 | 36 | - name: Check format 37 | run: cargo fmt -- --check 38 | 39 | - name: Build arm64 40 | run: cargo clippy --all-features 41 | 42 | - name: Build arm64 43 | run: cross build --release --target aarch64-unknown-linux-musl 44 | 45 | - name: Build amd64 46 | run: cross build --release --target x86_64-unknown-linux-musl 47 | 48 | - name: Build arm32v7 49 | run: cross build --release --target armv7-unknown-linux-musleabihf 50 | 51 | - name: Test 52 | run: cross test --target x86_64-unknown-linux-musl 53 | 54 | - name: Release the image 55 | run: make release_with_docker_only 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /data 3 | *.log 4 | .direnv 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "lldb", 6 | "request": "launch", 7 | "name": "Debug trackspeedtest run (simulated)", 8 | "cargo": { 9 | "args": [ 10 | "build", 11 | "--bin=trackspeedtest", 12 | "--package=trackspeedtest" 13 | ], 14 | "filter": { 15 | "name": "trackspeedtest", 16 | "kind": "bin" 17 | } 18 | }, 19 | "args": [ 20 | "run", 21 | "-vs" 22 | ], 23 | "cwd": "${workspaceFolder}" 24 | }, 25 | { 26 | "type": "lldb", 27 | "request": "launch", 28 | "name": "Debug trackspeedtest run (with real speed test)", 29 | "cargo": { 30 | "args": [ 31 | "build", 32 | "--bin=trackspeedtest", 33 | "--package=trackspeedtest" 34 | ], 35 | "filter": { 36 | "name": "trackspeedtest", 37 | "kind": "bin" 38 | } 39 | }, 40 | "args": [ 41 | "run", 42 | "-v" 43 | ], 44 | "cwd": "${workspaceFolder}" 45 | }, 46 | { 47 | "type": "lldb", 48 | "request": "launch", 49 | "name": "Debug trackspeedtest alert", 50 | "cargo": { 51 | "args": [ 52 | "build", 53 | "--bin=trackspeedtest", 54 | "--package=trackspeedtest" 55 | ], 56 | "filter": { 57 | "name": "trackspeedtest", 58 | "kind": "bin" 59 | } 60 | }, 61 | "args": [ 62 | "alert", 63 | "from@giggio.net", 64 | "giggio@giggio.net", 65 | "smtp.gmail.com:465", 66 | "300", 67 | "150", 68 | "--count", 69 | "3", 70 | "--username", 71 | "foo", 72 | "--password", 73 | "bar", 74 | "--simulate", 75 | ], 76 | "cwd": "${workspaceFolder}" 77 | }, 78 | { 79 | "type": "lldb", 80 | "request": "launch", 81 | "name": "Debug unit tests in executable 'trackspeedtest'", 82 | "cargo": { 83 | "args": [ 84 | "test", 85 | "--no-run", 86 | "--bin=trackspeedtest", 87 | "--package=trackspeedtest" 88 | ], 89 | "filter": { 90 | "name": "trackspeedtest", 91 | "kind": "bin" 92 | } 93 | }, 94 | "args": [], 95 | "cwd": "${workspaceFolder}" 96 | } 97 | ] 98 | } -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 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.11" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 25 | dependencies = [ 26 | "cfg-if", 27 | "once_cell", 28 | "version_check 0.9.5", 29 | "zerocopy", 30 | ] 31 | 32 | [[package]] 33 | name = "allocator-api2" 34 | version = "0.2.20" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9" 37 | 38 | [[package]] 39 | name = "android-tzdata" 40 | version = "0.1.1" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 43 | 44 | [[package]] 45 | name = "android_system_properties" 46 | version = "0.1.5" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 49 | dependencies = [ 50 | "libc", 51 | ] 52 | 53 | [[package]] 54 | name = "ansi_term" 55 | version = "0.12.1" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 58 | dependencies = [ 59 | "winapi", 60 | ] 61 | 62 | [[package]] 63 | name = "ascii_utils" 64 | version = "0.9.3" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a" 67 | 68 | [[package]] 69 | name = "atty" 70 | version = "0.2.14" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 73 | dependencies = [ 74 | "hermit-abi 0.1.19", 75 | "libc", 76 | "winapi", 77 | ] 78 | 79 | [[package]] 80 | name = "autocfg" 81 | version = "0.1.8" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" 84 | dependencies = [ 85 | "autocfg 1.4.0", 86 | ] 87 | 88 | [[package]] 89 | name = "autocfg" 90 | version = "1.4.0" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 93 | 94 | [[package]] 95 | name = "backtrace" 96 | version = "0.3.74" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 99 | dependencies = [ 100 | "addr2line", 101 | "cfg-if", 102 | "libc", 103 | "miniz_oxide", 104 | "object", 105 | "rustc-demangle", 106 | "windows-targets", 107 | ] 108 | 109 | [[package]] 110 | name = "base64" 111 | version = "0.9.3" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643" 114 | dependencies = [ 115 | "byteorder", 116 | "safemem", 117 | ] 118 | 119 | [[package]] 120 | name = "base64" 121 | version = "0.10.1" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" 124 | dependencies = [ 125 | "byteorder", 126 | ] 127 | 128 | [[package]] 129 | name = "base64" 130 | version = "0.22.1" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 133 | 134 | [[package]] 135 | name = "bitflags" 136 | version = "1.3.2" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 139 | 140 | [[package]] 141 | name = "bitflags" 142 | version = "2.6.0" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 145 | 146 | [[package]] 147 | name = "bumpalo" 148 | version = "3.16.0" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 151 | 152 | [[package]] 153 | name = "byteorder" 154 | version = "1.5.0" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 157 | 158 | [[package]] 159 | name = "cc" 160 | version = "1.2.1" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" 163 | dependencies = [ 164 | "shlex", 165 | ] 166 | 167 | [[package]] 168 | name = "cfg-if" 169 | version = "1.0.0" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 172 | 173 | [[package]] 174 | name = "chrono" 175 | version = "0.4.38" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" 178 | dependencies = [ 179 | "android-tzdata", 180 | "iana-time-zone", 181 | "js-sys", 182 | "num-traits", 183 | "wasm-bindgen", 184 | "windows-targets", 185 | ] 186 | 187 | [[package]] 188 | name = "chumsky" 189 | version = "0.9.3" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" 192 | dependencies = [ 193 | "hashbrown", 194 | "stacker", 195 | ] 196 | 197 | [[package]] 198 | name = "clap" 199 | version = "2.34.0" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" 202 | dependencies = [ 203 | "ansi_term", 204 | "atty", 205 | "bitflags 1.3.2", 206 | "strsim", 207 | "textwrap", 208 | "unicode-width", 209 | "vec_map", 210 | ] 211 | 212 | [[package]] 213 | name = "cloudabi" 214 | version = "0.0.3" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" 217 | dependencies = [ 218 | "bitflags 1.3.2", 219 | ] 220 | 221 | [[package]] 222 | name = "core-foundation" 223 | version = "0.9.4" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 226 | dependencies = [ 227 | "core-foundation-sys", 228 | "libc", 229 | ] 230 | 231 | [[package]] 232 | name = "core-foundation-sys" 233 | version = "0.8.7" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 236 | 237 | [[package]] 238 | name = "csv" 239 | version = "1.3.1" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" 242 | dependencies = [ 243 | "csv-core", 244 | "itoa", 245 | "ryu", 246 | "serde", 247 | ] 248 | 249 | [[package]] 250 | name = "csv-core" 251 | version = "0.1.11" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" 254 | dependencies = [ 255 | "memchr", 256 | ] 257 | 258 | [[package]] 259 | name = "derivative" 260 | version = "2.2.0" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" 263 | dependencies = [ 264 | "proc-macro2", 265 | "quote", 266 | "syn 1.0.109", 267 | ] 268 | 269 | [[package]] 270 | name = "diff" 271 | version = "0.1.13" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" 274 | 275 | [[package]] 276 | name = "displaydoc" 277 | version = "0.2.5" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 280 | dependencies = [ 281 | "proc-macro2", 282 | "quote", 283 | "syn 2.0.89", 284 | ] 285 | 286 | [[package]] 287 | name = "either" 288 | version = "1.13.0" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 291 | 292 | [[package]] 293 | name = "email" 294 | version = "0.0.20" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "91549a51bb0241165f13d57fc4c72cef063b4088fb078b019ecbf464a45f22e4" 297 | dependencies = [ 298 | "base64 0.9.3", 299 | "chrono", 300 | "encoding", 301 | "lazy_static", 302 | "rand 0.4.6", 303 | "time", 304 | "version_check 0.1.5", 305 | ] 306 | 307 | [[package]] 308 | name = "email-encoding" 309 | version = "0.3.1" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "ea3d894bbbab314476b265f9b2d46bf24b123a36dd0e96b06a1b49545b9d9dcc" 312 | dependencies = [ 313 | "base64 0.22.1", 314 | "memchr", 315 | ] 316 | 317 | [[package]] 318 | name = "email_address" 319 | version = "0.2.9" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" 322 | 323 | [[package]] 324 | name = "encoding" 325 | version = "0.2.33" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" 328 | dependencies = [ 329 | "encoding-index-japanese", 330 | "encoding-index-korean", 331 | "encoding-index-simpchinese", 332 | "encoding-index-singlebyte", 333 | "encoding-index-tradchinese", 334 | ] 335 | 336 | [[package]] 337 | name = "encoding-index-japanese" 338 | version = "1.20141219.5" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" 341 | dependencies = [ 342 | "encoding_index_tests", 343 | ] 344 | 345 | [[package]] 346 | name = "encoding-index-korean" 347 | version = "1.20141219.5" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" 350 | dependencies = [ 351 | "encoding_index_tests", 352 | ] 353 | 354 | [[package]] 355 | name = "encoding-index-simpchinese" 356 | version = "1.20141219.5" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" 359 | dependencies = [ 360 | "encoding_index_tests", 361 | ] 362 | 363 | [[package]] 364 | name = "encoding-index-singlebyte" 365 | version = "1.20141219.5" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" 368 | dependencies = [ 369 | "encoding_index_tests", 370 | ] 371 | 372 | [[package]] 373 | name = "encoding-index-tradchinese" 374 | version = "1.20141219.5" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" 377 | dependencies = [ 378 | "encoding_index_tests", 379 | ] 380 | 381 | [[package]] 382 | name = "encoding_index_tests" 383 | version = "0.1.4" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" 386 | 387 | [[package]] 388 | name = "errno" 389 | version = "0.3.10" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 392 | dependencies = [ 393 | "libc", 394 | "windows-sys 0.59.0", 395 | ] 396 | 397 | [[package]] 398 | name = "fast_chemail" 399 | version = "0.9.6" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4" 402 | dependencies = [ 403 | "ascii_utils", 404 | ] 405 | 406 | [[package]] 407 | name = "fastrand" 408 | version = "2.2.0" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" 411 | 412 | [[package]] 413 | name = "foreign-types" 414 | version = "0.3.2" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 417 | dependencies = [ 418 | "foreign-types-shared", 419 | ] 420 | 421 | [[package]] 422 | name = "foreign-types-shared" 423 | version = "0.1.1" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 426 | 427 | [[package]] 428 | name = "form_urlencoded" 429 | version = "1.2.1" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 432 | dependencies = [ 433 | "percent-encoding", 434 | ] 435 | 436 | [[package]] 437 | name = "fuchsia-cprng" 438 | version = "0.1.1" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" 441 | 442 | [[package]] 443 | name = "futures-core" 444 | version = "0.3.31" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 447 | 448 | [[package]] 449 | name = "futures-io" 450 | version = "0.3.31" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 453 | 454 | [[package]] 455 | name = "futures-task" 456 | version = "0.3.31" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 459 | 460 | [[package]] 461 | name = "futures-util" 462 | version = "0.3.31" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 465 | dependencies = [ 466 | "futures-core", 467 | "futures-io", 468 | "futures-task", 469 | "memchr", 470 | "pin-project-lite", 471 | "pin-utils", 472 | "slab", 473 | ] 474 | 475 | [[package]] 476 | name = "getrandom" 477 | version = "0.2.15" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 480 | dependencies = [ 481 | "cfg-if", 482 | "libc", 483 | "wasi 0.11.0+wasi-snapshot-preview1", 484 | ] 485 | 486 | [[package]] 487 | name = "gimli" 488 | version = "0.31.1" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 491 | 492 | [[package]] 493 | name = "hashbrown" 494 | version = "0.14.5" 495 | source = "registry+https://github.com/rust-lang/crates.io-index" 496 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 497 | dependencies = [ 498 | "ahash", 499 | "allocator-api2", 500 | ] 501 | 502 | [[package]] 503 | name = "hermit-abi" 504 | version = "0.1.19" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 507 | dependencies = [ 508 | "libc", 509 | ] 510 | 511 | [[package]] 512 | name = "hermit-abi" 513 | version = "0.3.9" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 516 | 517 | [[package]] 518 | name = "home" 519 | version = "0.5.9" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" 522 | dependencies = [ 523 | "windows-sys 0.52.0", 524 | ] 525 | 526 | [[package]] 527 | name = "hostname" 528 | version = "0.4.0" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba" 531 | dependencies = [ 532 | "cfg-if", 533 | "libc", 534 | "windows", 535 | ] 536 | 537 | [[package]] 538 | name = "httpdate" 539 | version = "1.0.3" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 542 | 543 | [[package]] 544 | name = "iana-time-zone" 545 | version = "0.1.61" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" 548 | dependencies = [ 549 | "android_system_properties", 550 | "core-foundation-sys", 551 | "iana-time-zone-haiku", 552 | "js-sys", 553 | "wasm-bindgen", 554 | "windows-core", 555 | ] 556 | 557 | [[package]] 558 | name = "iana-time-zone-haiku" 559 | version = "0.1.2" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 562 | dependencies = [ 563 | "cc", 564 | ] 565 | 566 | [[package]] 567 | name = "icu_collections" 568 | version = "1.5.0" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 571 | dependencies = [ 572 | "displaydoc", 573 | "yoke", 574 | "zerofrom", 575 | "zerovec", 576 | ] 577 | 578 | [[package]] 579 | name = "icu_locid" 580 | version = "1.5.0" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 583 | dependencies = [ 584 | "displaydoc", 585 | "litemap", 586 | "tinystr", 587 | "writeable", 588 | "zerovec", 589 | ] 590 | 591 | [[package]] 592 | name = "icu_locid_transform" 593 | version = "1.5.0" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 596 | dependencies = [ 597 | "displaydoc", 598 | "icu_locid", 599 | "icu_locid_transform_data", 600 | "icu_provider", 601 | "tinystr", 602 | "zerovec", 603 | ] 604 | 605 | [[package]] 606 | name = "icu_locid_transform_data" 607 | version = "1.5.0" 608 | source = "registry+https://github.com/rust-lang/crates.io-index" 609 | checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" 610 | 611 | [[package]] 612 | name = "icu_normalizer" 613 | version = "1.5.0" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 616 | dependencies = [ 617 | "displaydoc", 618 | "icu_collections", 619 | "icu_normalizer_data", 620 | "icu_properties", 621 | "icu_provider", 622 | "smallvec", 623 | "utf16_iter", 624 | "utf8_iter", 625 | "write16", 626 | "zerovec", 627 | ] 628 | 629 | [[package]] 630 | name = "icu_normalizer_data" 631 | version = "1.5.0" 632 | source = "registry+https://github.com/rust-lang/crates.io-index" 633 | checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" 634 | 635 | [[package]] 636 | name = "icu_properties" 637 | version = "1.5.1" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 640 | dependencies = [ 641 | "displaydoc", 642 | "icu_collections", 643 | "icu_locid_transform", 644 | "icu_properties_data", 645 | "icu_provider", 646 | "tinystr", 647 | "zerovec", 648 | ] 649 | 650 | [[package]] 651 | name = "icu_properties_data" 652 | version = "1.5.0" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" 655 | 656 | [[package]] 657 | name = "icu_provider" 658 | version = "1.5.0" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 661 | dependencies = [ 662 | "displaydoc", 663 | "icu_locid", 664 | "icu_provider_macros", 665 | "stable_deref_trait", 666 | "tinystr", 667 | "writeable", 668 | "yoke", 669 | "zerofrom", 670 | "zerovec", 671 | ] 672 | 673 | [[package]] 674 | name = "icu_provider_macros" 675 | version = "1.5.0" 676 | source = "registry+https://github.com/rust-lang/crates.io-index" 677 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 678 | dependencies = [ 679 | "proc-macro2", 680 | "quote", 681 | "syn 2.0.89", 682 | ] 683 | 684 | [[package]] 685 | name = "idna" 686 | version = "1.0.3" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 689 | dependencies = [ 690 | "idna_adapter", 691 | "smallvec", 692 | "utf8_iter", 693 | ] 694 | 695 | [[package]] 696 | name = "idna_adapter" 697 | version = "1.2.0" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 700 | dependencies = [ 701 | "icu_normalizer", 702 | "icu_properties", 703 | ] 704 | 705 | [[package]] 706 | name = "itoa" 707 | version = "1.0.14" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 710 | 711 | [[package]] 712 | name = "js-sys" 713 | version = "0.3.72" 714 | source = "registry+https://github.com/rust-lang/crates.io-index" 715 | checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" 716 | dependencies = [ 717 | "wasm-bindgen", 718 | ] 719 | 720 | [[package]] 721 | name = "lazy_static" 722 | version = "1.5.0" 723 | source = "registry+https://github.com/rust-lang/crates.io-index" 724 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 725 | 726 | [[package]] 727 | name = "lettre" 728 | version = "0.9.6" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "86ed8677138975b573ab4949c35613931a4addeadd0a8a6aa0327e2a979660de" 731 | dependencies = [ 732 | "fast_chemail", 733 | "log", 734 | ] 735 | 736 | [[package]] 737 | name = "lettre" 738 | version = "0.11.10" 739 | source = "registry+https://github.com/rust-lang/crates.io-index" 740 | checksum = "0161e452348e399deb685ba05e55ee116cae9410f4f51fe42d597361444521d9" 741 | dependencies = [ 742 | "base64 0.22.1", 743 | "chumsky", 744 | "email-encoding", 745 | "email_address", 746 | "fastrand", 747 | "futures-util", 748 | "hostname", 749 | "httpdate", 750 | "idna", 751 | "mime", 752 | "native-tls", 753 | "nom", 754 | "percent-encoding", 755 | "quoted_printable", 756 | "rustls", 757 | "rustls-pemfile", 758 | "rustls-pki-types", 759 | "socket2", 760 | "tokio", 761 | "url", 762 | "webpki-roots", 763 | ] 764 | 765 | [[package]] 766 | name = "lettre_email" 767 | version = "0.9.4" 768 | source = "registry+https://github.com/rust-lang/crates.io-index" 769 | checksum = "fd02480f8dcf48798e62113974d6ccca2129a51d241fa20f1ea349c8a42559d5" 770 | dependencies = [ 771 | "base64 0.10.1", 772 | "email", 773 | "lettre 0.9.6", 774 | "mime", 775 | "time", 776 | "uuid", 777 | ] 778 | 779 | [[package]] 780 | name = "libc" 781 | version = "0.2.166" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "c2ccc108bbc0b1331bd061864e7cd823c0cab660bbe6970e66e2c0614decde36" 784 | 785 | [[package]] 786 | name = "linux-raw-sys" 787 | version = "0.4.14" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 790 | 791 | [[package]] 792 | name = "litemap" 793 | version = "0.7.4" 794 | source = "registry+https://github.com/rust-lang/crates.io-index" 795 | checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" 796 | 797 | [[package]] 798 | name = "log" 799 | version = "0.4.22" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 802 | 803 | [[package]] 804 | name = "memchr" 805 | version = "2.7.4" 806 | source = "registry+https://github.com/rust-lang/crates.io-index" 807 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 808 | 809 | [[package]] 810 | name = "mime" 811 | version = "0.3.17" 812 | source = "registry+https://github.com/rust-lang/crates.io-index" 813 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 814 | 815 | [[package]] 816 | name = "minimal-lexical" 817 | version = "0.2.1" 818 | source = "registry+https://github.com/rust-lang/crates.io-index" 819 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 820 | 821 | [[package]] 822 | name = "miniz_oxide" 823 | version = "0.8.0" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" 826 | dependencies = [ 827 | "adler2", 828 | ] 829 | 830 | [[package]] 831 | name = "mio" 832 | version = "1.0.2" 833 | source = "registry+https://github.com/rust-lang/crates.io-index" 834 | checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" 835 | dependencies = [ 836 | "hermit-abi 0.3.9", 837 | "libc", 838 | "wasi 0.11.0+wasi-snapshot-preview1", 839 | "windows-sys 0.52.0", 840 | ] 841 | 842 | [[package]] 843 | name = "native-tls" 844 | version = "0.2.12" 845 | source = "registry+https://github.com/rust-lang/crates.io-index" 846 | checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" 847 | dependencies = [ 848 | "libc", 849 | "log", 850 | "openssl", 851 | "openssl-probe", 852 | "openssl-sys", 853 | "schannel", 854 | "security-framework", 855 | "security-framework-sys", 856 | "tempfile", 857 | ] 858 | 859 | [[package]] 860 | name = "nom" 861 | version = "7.1.3" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 864 | dependencies = [ 865 | "memchr", 866 | "minimal-lexical", 867 | ] 868 | 869 | [[package]] 870 | name = "num-traits" 871 | version = "0.2.19" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 874 | dependencies = [ 875 | "autocfg 1.4.0", 876 | ] 877 | 878 | [[package]] 879 | name = "object" 880 | version = "0.36.5" 881 | source = "registry+https://github.com/rust-lang/crates.io-index" 882 | checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" 883 | dependencies = [ 884 | "memchr", 885 | ] 886 | 887 | [[package]] 888 | name = "once_cell" 889 | version = "1.20.2" 890 | source = "registry+https://github.com/rust-lang/crates.io-index" 891 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 892 | 893 | [[package]] 894 | name = "openssl" 895 | version = "0.10.68" 896 | source = "registry+https://github.com/rust-lang/crates.io-index" 897 | checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" 898 | dependencies = [ 899 | "bitflags 2.6.0", 900 | "cfg-if", 901 | "foreign-types", 902 | "libc", 903 | "once_cell", 904 | "openssl-macros", 905 | "openssl-sys", 906 | ] 907 | 908 | [[package]] 909 | name = "openssl-macros" 910 | version = "0.1.1" 911 | source = "registry+https://github.com/rust-lang/crates.io-index" 912 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 913 | dependencies = [ 914 | "proc-macro2", 915 | "quote", 916 | "syn 2.0.89", 917 | ] 918 | 919 | [[package]] 920 | name = "openssl-probe" 921 | version = "0.1.5" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 924 | 925 | [[package]] 926 | name = "openssl-src" 927 | version = "300.4.1+3.4.0" 928 | source = "registry+https://github.com/rust-lang/crates.io-index" 929 | checksum = "faa4eac4138c62414b5622d1b31c5c304f34b406b013c079c2bbc652fdd6678c" 930 | dependencies = [ 931 | "cc", 932 | ] 933 | 934 | [[package]] 935 | name = "openssl-sys" 936 | version = "0.9.104" 937 | source = "registry+https://github.com/rust-lang/crates.io-index" 938 | checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" 939 | dependencies = [ 940 | "cc", 941 | "libc", 942 | "openssl-src", 943 | "pkg-config", 944 | "vcpkg", 945 | ] 946 | 947 | [[package]] 948 | name = "percent-encoding" 949 | version = "2.3.1" 950 | source = "registry+https://github.com/rust-lang/crates.io-index" 951 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 952 | 953 | [[package]] 954 | name = "pin-project-lite" 955 | version = "0.2.15" 956 | source = "registry+https://github.com/rust-lang/crates.io-index" 957 | checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" 958 | 959 | [[package]] 960 | name = "pin-utils" 961 | version = "0.1.0" 962 | source = "registry+https://github.com/rust-lang/crates.io-index" 963 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 964 | 965 | [[package]] 966 | name = "pkg-config" 967 | version = "0.3.31" 968 | source = "registry+https://github.com/rust-lang/crates.io-index" 969 | checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" 970 | 971 | [[package]] 972 | name = "pretty_assertions" 973 | version = "1.4.1" 974 | source = "registry+https://github.com/rust-lang/crates.io-index" 975 | checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" 976 | dependencies = [ 977 | "diff", 978 | "yansi", 979 | ] 980 | 981 | [[package]] 982 | name = "proc-macro2" 983 | version = "1.0.92" 984 | source = "registry+https://github.com/rust-lang/crates.io-index" 985 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 986 | dependencies = [ 987 | "unicode-ident", 988 | ] 989 | 990 | [[package]] 991 | name = "psm" 992 | version = "0.1.24" 993 | source = "registry+https://github.com/rust-lang/crates.io-index" 994 | checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810" 995 | dependencies = [ 996 | "cc", 997 | ] 998 | 999 | [[package]] 1000 | name = "quote" 1001 | version = "1.0.37" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 1004 | dependencies = [ 1005 | "proc-macro2", 1006 | ] 1007 | 1008 | [[package]] 1009 | name = "quoted_printable" 1010 | version = "0.5.1" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" 1013 | 1014 | [[package]] 1015 | name = "rand" 1016 | version = "0.4.6" 1017 | source = "registry+https://github.com/rust-lang/crates.io-index" 1018 | checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" 1019 | dependencies = [ 1020 | "fuchsia-cprng", 1021 | "libc", 1022 | "rand_core 0.3.1", 1023 | "rdrand", 1024 | "winapi", 1025 | ] 1026 | 1027 | [[package]] 1028 | name = "rand" 1029 | version = "0.6.5" 1030 | source = "registry+https://github.com/rust-lang/crates.io-index" 1031 | checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" 1032 | dependencies = [ 1033 | "autocfg 0.1.8", 1034 | "libc", 1035 | "rand_chacha", 1036 | "rand_core 0.4.2", 1037 | "rand_hc", 1038 | "rand_isaac", 1039 | "rand_jitter", 1040 | "rand_os", 1041 | "rand_pcg", 1042 | "rand_xorshift", 1043 | "winapi", 1044 | ] 1045 | 1046 | [[package]] 1047 | name = "rand_chacha" 1048 | version = "0.1.1" 1049 | source = "registry+https://github.com/rust-lang/crates.io-index" 1050 | checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" 1051 | dependencies = [ 1052 | "autocfg 0.1.8", 1053 | "rand_core 0.3.1", 1054 | ] 1055 | 1056 | [[package]] 1057 | name = "rand_core" 1058 | version = "0.3.1" 1059 | source = "registry+https://github.com/rust-lang/crates.io-index" 1060 | checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" 1061 | dependencies = [ 1062 | "rand_core 0.4.2", 1063 | ] 1064 | 1065 | [[package]] 1066 | name = "rand_core" 1067 | version = "0.4.2" 1068 | source = "registry+https://github.com/rust-lang/crates.io-index" 1069 | checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" 1070 | 1071 | [[package]] 1072 | name = "rand_hc" 1073 | version = "0.1.0" 1074 | source = "registry+https://github.com/rust-lang/crates.io-index" 1075 | checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" 1076 | dependencies = [ 1077 | "rand_core 0.3.1", 1078 | ] 1079 | 1080 | [[package]] 1081 | name = "rand_isaac" 1082 | version = "0.1.1" 1083 | source = "registry+https://github.com/rust-lang/crates.io-index" 1084 | checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" 1085 | dependencies = [ 1086 | "rand_core 0.3.1", 1087 | ] 1088 | 1089 | [[package]] 1090 | name = "rand_jitter" 1091 | version = "0.1.4" 1092 | source = "registry+https://github.com/rust-lang/crates.io-index" 1093 | checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" 1094 | dependencies = [ 1095 | "libc", 1096 | "rand_core 0.4.2", 1097 | "winapi", 1098 | ] 1099 | 1100 | [[package]] 1101 | name = "rand_os" 1102 | version = "0.1.3" 1103 | source = "registry+https://github.com/rust-lang/crates.io-index" 1104 | checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" 1105 | dependencies = [ 1106 | "cloudabi", 1107 | "fuchsia-cprng", 1108 | "libc", 1109 | "rand_core 0.4.2", 1110 | "rdrand", 1111 | "winapi", 1112 | ] 1113 | 1114 | [[package]] 1115 | name = "rand_pcg" 1116 | version = "0.1.2" 1117 | source = "registry+https://github.com/rust-lang/crates.io-index" 1118 | checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" 1119 | dependencies = [ 1120 | "autocfg 0.1.8", 1121 | "rand_core 0.4.2", 1122 | ] 1123 | 1124 | [[package]] 1125 | name = "rand_xorshift" 1126 | version = "0.1.1" 1127 | source = "registry+https://github.com/rust-lang/crates.io-index" 1128 | checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" 1129 | dependencies = [ 1130 | "rand_core 0.3.1", 1131 | ] 1132 | 1133 | [[package]] 1134 | name = "rdrand" 1135 | version = "0.4.0" 1136 | source = "registry+https://github.com/rust-lang/crates.io-index" 1137 | checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" 1138 | dependencies = [ 1139 | "rand_core 0.3.1", 1140 | ] 1141 | 1142 | [[package]] 1143 | name = "rev_lines" 1144 | version = "0.3.0" 1145 | source = "registry+https://github.com/rust-lang/crates.io-index" 1146 | checksum = "ed62916ac7a5ccbf13fa5e1d303029ff015600fee841756dfc134a1ac62bf05f" 1147 | dependencies = [ 1148 | "thiserror", 1149 | ] 1150 | 1151 | [[package]] 1152 | name = "ring" 1153 | version = "0.17.8" 1154 | source = "registry+https://github.com/rust-lang/crates.io-index" 1155 | checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" 1156 | dependencies = [ 1157 | "cc", 1158 | "cfg-if", 1159 | "getrandom", 1160 | "libc", 1161 | "spin", 1162 | "untrusted", 1163 | "windows-sys 0.52.0", 1164 | ] 1165 | 1166 | [[package]] 1167 | name = "rustc-demangle" 1168 | version = "0.1.24" 1169 | source = "registry+https://github.com/rust-lang/crates.io-index" 1170 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 1171 | 1172 | [[package]] 1173 | name = "rustix" 1174 | version = "0.38.41" 1175 | source = "registry+https://github.com/rust-lang/crates.io-index" 1176 | checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" 1177 | dependencies = [ 1178 | "bitflags 2.6.0", 1179 | "errno", 1180 | "libc", 1181 | "linux-raw-sys", 1182 | "windows-sys 0.52.0", 1183 | ] 1184 | 1185 | [[package]] 1186 | name = "rustls" 1187 | version = "0.23.19" 1188 | source = "registry+https://github.com/rust-lang/crates.io-index" 1189 | checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1" 1190 | dependencies = [ 1191 | "log", 1192 | "once_cell", 1193 | "ring", 1194 | "rustls-pki-types", 1195 | "rustls-webpki", 1196 | "subtle", 1197 | "zeroize", 1198 | ] 1199 | 1200 | [[package]] 1201 | name = "rustls-pemfile" 1202 | version = "2.2.0" 1203 | source = "registry+https://github.com/rust-lang/crates.io-index" 1204 | checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" 1205 | dependencies = [ 1206 | "rustls-pki-types", 1207 | ] 1208 | 1209 | [[package]] 1210 | name = "rustls-pki-types" 1211 | version = "1.10.0" 1212 | source = "registry+https://github.com/rust-lang/crates.io-index" 1213 | checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" 1214 | 1215 | [[package]] 1216 | name = "rustls-webpki" 1217 | version = "0.102.8" 1218 | source = "registry+https://github.com/rust-lang/crates.io-index" 1219 | checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" 1220 | dependencies = [ 1221 | "ring", 1222 | "rustls-pki-types", 1223 | "untrusted", 1224 | ] 1225 | 1226 | [[package]] 1227 | name = "ryu" 1228 | version = "1.0.18" 1229 | source = "registry+https://github.com/rust-lang/crates.io-index" 1230 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 1231 | 1232 | [[package]] 1233 | name = "safemem" 1234 | version = "0.3.3" 1235 | source = "registry+https://github.com/rust-lang/crates.io-index" 1236 | checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" 1237 | 1238 | [[package]] 1239 | name = "schannel" 1240 | version = "0.1.27" 1241 | source = "registry+https://github.com/rust-lang/crates.io-index" 1242 | checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" 1243 | dependencies = [ 1244 | "windows-sys 0.59.0", 1245 | ] 1246 | 1247 | [[package]] 1248 | name = "security-framework" 1249 | version = "2.11.1" 1250 | source = "registry+https://github.com/rust-lang/crates.io-index" 1251 | checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 1252 | dependencies = [ 1253 | "bitflags 2.6.0", 1254 | "core-foundation", 1255 | "core-foundation-sys", 1256 | "libc", 1257 | "security-framework-sys", 1258 | ] 1259 | 1260 | [[package]] 1261 | name = "security-framework-sys" 1262 | version = "2.12.1" 1263 | source = "registry+https://github.com/rust-lang/crates.io-index" 1264 | checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" 1265 | dependencies = [ 1266 | "core-foundation-sys", 1267 | "libc", 1268 | ] 1269 | 1270 | [[package]] 1271 | name = "serde" 1272 | version = "1.0.215" 1273 | source = "registry+https://github.com/rust-lang/crates.io-index" 1274 | checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" 1275 | dependencies = [ 1276 | "serde_derive", 1277 | ] 1278 | 1279 | [[package]] 1280 | name = "serde_derive" 1281 | version = "1.0.215" 1282 | source = "registry+https://github.com/rust-lang/crates.io-index" 1283 | checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" 1284 | dependencies = [ 1285 | "proc-macro2", 1286 | "quote", 1287 | "syn 2.0.89", 1288 | ] 1289 | 1290 | [[package]] 1291 | name = "serde_json" 1292 | version = "1.0.133" 1293 | source = "registry+https://github.com/rust-lang/crates.io-index" 1294 | checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" 1295 | dependencies = [ 1296 | "itoa", 1297 | "memchr", 1298 | "ryu", 1299 | "serde", 1300 | ] 1301 | 1302 | [[package]] 1303 | name = "shlex" 1304 | version = "1.3.0" 1305 | source = "registry+https://github.com/rust-lang/crates.io-index" 1306 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1307 | 1308 | [[package]] 1309 | name = "slab" 1310 | version = "0.4.9" 1311 | source = "registry+https://github.com/rust-lang/crates.io-index" 1312 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1313 | dependencies = [ 1314 | "autocfg 1.4.0", 1315 | ] 1316 | 1317 | [[package]] 1318 | name = "smallvec" 1319 | version = "1.13.2" 1320 | source = "registry+https://github.com/rust-lang/crates.io-index" 1321 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 1322 | 1323 | [[package]] 1324 | name = "socket2" 1325 | version = "0.5.8" 1326 | source = "registry+https://github.com/rust-lang/crates.io-index" 1327 | checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" 1328 | dependencies = [ 1329 | "libc", 1330 | "windows-sys 0.52.0", 1331 | ] 1332 | 1333 | [[package]] 1334 | name = "spin" 1335 | version = "0.9.8" 1336 | source = "registry+https://github.com/rust-lang/crates.io-index" 1337 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 1338 | 1339 | [[package]] 1340 | name = "stable_deref_trait" 1341 | version = "1.2.0" 1342 | source = "registry+https://github.com/rust-lang/crates.io-index" 1343 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1344 | 1345 | [[package]] 1346 | name = "stacker" 1347 | version = "0.1.17" 1348 | source = "registry+https://github.com/rust-lang/crates.io-index" 1349 | checksum = "799c883d55abdb5e98af1a7b3f23b9b6de8ecada0ecac058672d7635eb48ca7b" 1350 | dependencies = [ 1351 | "cc", 1352 | "cfg-if", 1353 | "libc", 1354 | "psm", 1355 | "windows-sys 0.59.0", 1356 | ] 1357 | 1358 | [[package]] 1359 | name = "strsim" 1360 | version = "0.8.0" 1361 | source = "registry+https://github.com/rust-lang/crates.io-index" 1362 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 1363 | 1364 | [[package]] 1365 | name = "subtle" 1366 | version = "2.6.1" 1367 | source = "registry+https://github.com/rust-lang/crates.io-index" 1368 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 1369 | 1370 | [[package]] 1371 | name = "syn" 1372 | version = "1.0.109" 1373 | source = "registry+https://github.com/rust-lang/crates.io-index" 1374 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1375 | dependencies = [ 1376 | "proc-macro2", 1377 | "quote", 1378 | "unicode-ident", 1379 | ] 1380 | 1381 | [[package]] 1382 | name = "syn" 1383 | version = "2.0.89" 1384 | source = "registry+https://github.com/rust-lang/crates.io-index" 1385 | checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" 1386 | dependencies = [ 1387 | "proc-macro2", 1388 | "quote", 1389 | "unicode-ident", 1390 | ] 1391 | 1392 | [[package]] 1393 | name = "synstructure" 1394 | version = "0.13.1" 1395 | source = "registry+https://github.com/rust-lang/crates.io-index" 1396 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 1397 | dependencies = [ 1398 | "proc-macro2", 1399 | "quote", 1400 | "syn 2.0.89", 1401 | ] 1402 | 1403 | [[package]] 1404 | name = "tempfile" 1405 | version = "3.14.0" 1406 | source = "registry+https://github.com/rust-lang/crates.io-index" 1407 | checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" 1408 | dependencies = [ 1409 | "cfg-if", 1410 | "fastrand", 1411 | "once_cell", 1412 | "rustix", 1413 | "windows-sys 0.59.0", 1414 | ] 1415 | 1416 | [[package]] 1417 | name = "textwrap" 1418 | version = "0.11.0" 1419 | source = "registry+https://github.com/rust-lang/crates.io-index" 1420 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 1421 | dependencies = [ 1422 | "unicode-width", 1423 | ] 1424 | 1425 | [[package]] 1426 | name = "thiserror" 1427 | version = "1.0.69" 1428 | source = "registry+https://github.com/rust-lang/crates.io-index" 1429 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 1430 | dependencies = [ 1431 | "thiserror-impl", 1432 | ] 1433 | 1434 | [[package]] 1435 | name = "thiserror-impl" 1436 | version = "1.0.69" 1437 | source = "registry+https://github.com/rust-lang/crates.io-index" 1438 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 1439 | dependencies = [ 1440 | "proc-macro2", 1441 | "quote", 1442 | "syn 2.0.89", 1443 | ] 1444 | 1445 | [[package]] 1446 | name = "time" 1447 | version = "0.1.45" 1448 | source = "registry+https://github.com/rust-lang/crates.io-index" 1449 | checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" 1450 | dependencies = [ 1451 | "libc", 1452 | "wasi 0.10.0+wasi-snapshot-preview1", 1453 | "winapi", 1454 | ] 1455 | 1456 | [[package]] 1457 | name = "tinystr" 1458 | version = "0.7.6" 1459 | source = "registry+https://github.com/rust-lang/crates.io-index" 1460 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 1461 | dependencies = [ 1462 | "displaydoc", 1463 | "zerovec", 1464 | ] 1465 | 1466 | [[package]] 1467 | name = "tokio" 1468 | version = "1.41.1" 1469 | source = "registry+https://github.com/rust-lang/crates.io-index" 1470 | checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" 1471 | dependencies = [ 1472 | "backtrace", 1473 | "libc", 1474 | "mio", 1475 | "pin-project-lite", 1476 | "socket2", 1477 | "windows-sys 0.52.0", 1478 | ] 1479 | 1480 | [[package]] 1481 | name = "trackspeedtest" 1482 | version = "0.4.0" 1483 | dependencies = [ 1484 | "chrono", 1485 | "clap", 1486 | "csv", 1487 | "derivative", 1488 | "lettre 0.11.10", 1489 | "lettre_email", 1490 | "openssl", 1491 | "pretty_assertions", 1492 | "rev_lines", 1493 | "serde", 1494 | "serde_json", 1495 | "which", 1496 | ] 1497 | 1498 | [[package]] 1499 | name = "unicode-ident" 1500 | version = "1.0.14" 1501 | source = "registry+https://github.com/rust-lang/crates.io-index" 1502 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 1503 | 1504 | [[package]] 1505 | name = "unicode-width" 1506 | version = "0.1.14" 1507 | source = "registry+https://github.com/rust-lang/crates.io-index" 1508 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 1509 | 1510 | [[package]] 1511 | name = "untrusted" 1512 | version = "0.9.0" 1513 | source = "registry+https://github.com/rust-lang/crates.io-index" 1514 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 1515 | 1516 | [[package]] 1517 | name = "url" 1518 | version = "2.5.4" 1519 | source = "registry+https://github.com/rust-lang/crates.io-index" 1520 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 1521 | dependencies = [ 1522 | "form_urlencoded", 1523 | "idna", 1524 | "percent-encoding", 1525 | ] 1526 | 1527 | [[package]] 1528 | name = "utf16_iter" 1529 | version = "1.0.5" 1530 | source = "registry+https://github.com/rust-lang/crates.io-index" 1531 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 1532 | 1533 | [[package]] 1534 | name = "utf8_iter" 1535 | version = "1.0.4" 1536 | source = "registry+https://github.com/rust-lang/crates.io-index" 1537 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1538 | 1539 | [[package]] 1540 | name = "uuid" 1541 | version = "0.7.4" 1542 | source = "registry+https://github.com/rust-lang/crates.io-index" 1543 | checksum = "90dbc611eb48397705a6b0f6e917da23ae517e4d127123d2cf7674206627d32a" 1544 | dependencies = [ 1545 | "rand 0.6.5", 1546 | ] 1547 | 1548 | [[package]] 1549 | name = "vcpkg" 1550 | version = "0.2.15" 1551 | source = "registry+https://github.com/rust-lang/crates.io-index" 1552 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1553 | 1554 | [[package]] 1555 | name = "vec_map" 1556 | version = "0.8.2" 1557 | source = "registry+https://github.com/rust-lang/crates.io-index" 1558 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 1559 | 1560 | [[package]] 1561 | name = "version_check" 1562 | version = "0.1.5" 1563 | source = "registry+https://github.com/rust-lang/crates.io-index" 1564 | checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" 1565 | 1566 | [[package]] 1567 | name = "version_check" 1568 | version = "0.9.5" 1569 | source = "registry+https://github.com/rust-lang/crates.io-index" 1570 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1571 | 1572 | [[package]] 1573 | name = "wasi" 1574 | version = "0.10.0+wasi-snapshot-preview1" 1575 | source = "registry+https://github.com/rust-lang/crates.io-index" 1576 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 1577 | 1578 | [[package]] 1579 | name = "wasi" 1580 | version = "0.11.0+wasi-snapshot-preview1" 1581 | source = "registry+https://github.com/rust-lang/crates.io-index" 1582 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1583 | 1584 | [[package]] 1585 | name = "wasm-bindgen" 1586 | version = "0.2.95" 1587 | source = "registry+https://github.com/rust-lang/crates.io-index" 1588 | checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" 1589 | dependencies = [ 1590 | "cfg-if", 1591 | "once_cell", 1592 | "wasm-bindgen-macro", 1593 | ] 1594 | 1595 | [[package]] 1596 | name = "wasm-bindgen-backend" 1597 | version = "0.2.95" 1598 | source = "registry+https://github.com/rust-lang/crates.io-index" 1599 | checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" 1600 | dependencies = [ 1601 | "bumpalo", 1602 | "log", 1603 | "once_cell", 1604 | "proc-macro2", 1605 | "quote", 1606 | "syn 2.0.89", 1607 | "wasm-bindgen-shared", 1608 | ] 1609 | 1610 | [[package]] 1611 | name = "wasm-bindgen-macro" 1612 | version = "0.2.95" 1613 | source = "registry+https://github.com/rust-lang/crates.io-index" 1614 | checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" 1615 | dependencies = [ 1616 | "quote", 1617 | "wasm-bindgen-macro-support", 1618 | ] 1619 | 1620 | [[package]] 1621 | name = "wasm-bindgen-macro-support" 1622 | version = "0.2.95" 1623 | source = "registry+https://github.com/rust-lang/crates.io-index" 1624 | checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" 1625 | dependencies = [ 1626 | "proc-macro2", 1627 | "quote", 1628 | "syn 2.0.89", 1629 | "wasm-bindgen-backend", 1630 | "wasm-bindgen-shared", 1631 | ] 1632 | 1633 | [[package]] 1634 | name = "wasm-bindgen-shared" 1635 | version = "0.2.95" 1636 | source = "registry+https://github.com/rust-lang/crates.io-index" 1637 | checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" 1638 | 1639 | [[package]] 1640 | name = "webpki-roots" 1641 | version = "0.26.7" 1642 | source = "registry+https://github.com/rust-lang/crates.io-index" 1643 | checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" 1644 | dependencies = [ 1645 | "rustls-pki-types", 1646 | ] 1647 | 1648 | [[package]] 1649 | name = "which" 1650 | version = "7.0.0" 1651 | source = "registry+https://github.com/rust-lang/crates.io-index" 1652 | checksum = "c9cad3279ade7346b96e38731a641d7343dd6a53d55083dd54eadfa5a1b38c6b" 1653 | dependencies = [ 1654 | "either", 1655 | "home", 1656 | "rustix", 1657 | "winsafe", 1658 | ] 1659 | 1660 | [[package]] 1661 | name = "winapi" 1662 | version = "0.3.9" 1663 | source = "registry+https://github.com/rust-lang/crates.io-index" 1664 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1665 | dependencies = [ 1666 | "winapi-i686-pc-windows-gnu", 1667 | "winapi-x86_64-pc-windows-gnu", 1668 | ] 1669 | 1670 | [[package]] 1671 | name = "winapi-i686-pc-windows-gnu" 1672 | version = "0.4.0" 1673 | source = "registry+https://github.com/rust-lang/crates.io-index" 1674 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1675 | 1676 | [[package]] 1677 | name = "winapi-x86_64-pc-windows-gnu" 1678 | version = "0.4.0" 1679 | source = "registry+https://github.com/rust-lang/crates.io-index" 1680 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1681 | 1682 | [[package]] 1683 | name = "windows" 1684 | version = "0.52.0" 1685 | source = "registry+https://github.com/rust-lang/crates.io-index" 1686 | checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" 1687 | dependencies = [ 1688 | "windows-core", 1689 | "windows-targets", 1690 | ] 1691 | 1692 | [[package]] 1693 | name = "windows-core" 1694 | version = "0.52.0" 1695 | source = "registry+https://github.com/rust-lang/crates.io-index" 1696 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1697 | dependencies = [ 1698 | "windows-targets", 1699 | ] 1700 | 1701 | [[package]] 1702 | name = "windows-sys" 1703 | version = "0.52.0" 1704 | source = "registry+https://github.com/rust-lang/crates.io-index" 1705 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1706 | dependencies = [ 1707 | "windows-targets", 1708 | ] 1709 | 1710 | [[package]] 1711 | name = "windows-sys" 1712 | version = "0.59.0" 1713 | source = "registry+https://github.com/rust-lang/crates.io-index" 1714 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1715 | dependencies = [ 1716 | "windows-targets", 1717 | ] 1718 | 1719 | [[package]] 1720 | name = "windows-targets" 1721 | version = "0.52.6" 1722 | source = "registry+https://github.com/rust-lang/crates.io-index" 1723 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1724 | dependencies = [ 1725 | "windows_aarch64_gnullvm", 1726 | "windows_aarch64_msvc", 1727 | "windows_i686_gnu", 1728 | "windows_i686_gnullvm", 1729 | "windows_i686_msvc", 1730 | "windows_x86_64_gnu", 1731 | "windows_x86_64_gnullvm", 1732 | "windows_x86_64_msvc", 1733 | ] 1734 | 1735 | [[package]] 1736 | name = "windows_aarch64_gnullvm" 1737 | version = "0.52.6" 1738 | source = "registry+https://github.com/rust-lang/crates.io-index" 1739 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1740 | 1741 | [[package]] 1742 | name = "windows_aarch64_msvc" 1743 | version = "0.52.6" 1744 | source = "registry+https://github.com/rust-lang/crates.io-index" 1745 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1746 | 1747 | [[package]] 1748 | name = "windows_i686_gnu" 1749 | version = "0.52.6" 1750 | source = "registry+https://github.com/rust-lang/crates.io-index" 1751 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1752 | 1753 | [[package]] 1754 | name = "windows_i686_gnullvm" 1755 | version = "0.52.6" 1756 | source = "registry+https://github.com/rust-lang/crates.io-index" 1757 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1758 | 1759 | [[package]] 1760 | name = "windows_i686_msvc" 1761 | version = "0.52.6" 1762 | source = "registry+https://github.com/rust-lang/crates.io-index" 1763 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1764 | 1765 | [[package]] 1766 | name = "windows_x86_64_gnu" 1767 | version = "0.52.6" 1768 | source = "registry+https://github.com/rust-lang/crates.io-index" 1769 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1770 | 1771 | [[package]] 1772 | name = "windows_x86_64_gnullvm" 1773 | version = "0.52.6" 1774 | source = "registry+https://github.com/rust-lang/crates.io-index" 1775 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1776 | 1777 | [[package]] 1778 | name = "windows_x86_64_msvc" 1779 | version = "0.52.6" 1780 | source = "registry+https://github.com/rust-lang/crates.io-index" 1781 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1782 | 1783 | [[package]] 1784 | name = "winsafe" 1785 | version = "0.0.19" 1786 | source = "registry+https://github.com/rust-lang/crates.io-index" 1787 | checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" 1788 | 1789 | [[package]] 1790 | name = "write16" 1791 | version = "1.0.0" 1792 | source = "registry+https://github.com/rust-lang/crates.io-index" 1793 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 1794 | 1795 | [[package]] 1796 | name = "writeable" 1797 | version = "0.5.5" 1798 | source = "registry+https://github.com/rust-lang/crates.io-index" 1799 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 1800 | 1801 | [[package]] 1802 | name = "yansi" 1803 | version = "1.0.1" 1804 | source = "registry+https://github.com/rust-lang/crates.io-index" 1805 | checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" 1806 | 1807 | [[package]] 1808 | name = "yoke" 1809 | version = "0.7.5" 1810 | source = "registry+https://github.com/rust-lang/crates.io-index" 1811 | checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 1812 | dependencies = [ 1813 | "serde", 1814 | "stable_deref_trait", 1815 | "yoke-derive", 1816 | "zerofrom", 1817 | ] 1818 | 1819 | [[package]] 1820 | name = "yoke-derive" 1821 | version = "0.7.5" 1822 | source = "registry+https://github.com/rust-lang/crates.io-index" 1823 | checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 1824 | dependencies = [ 1825 | "proc-macro2", 1826 | "quote", 1827 | "syn 2.0.89", 1828 | "synstructure", 1829 | ] 1830 | 1831 | [[package]] 1832 | name = "zerocopy" 1833 | version = "0.7.35" 1834 | source = "registry+https://github.com/rust-lang/crates.io-index" 1835 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 1836 | dependencies = [ 1837 | "zerocopy-derive", 1838 | ] 1839 | 1840 | [[package]] 1841 | name = "zerocopy-derive" 1842 | version = "0.7.35" 1843 | source = "registry+https://github.com/rust-lang/crates.io-index" 1844 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 1845 | dependencies = [ 1846 | "proc-macro2", 1847 | "quote", 1848 | "syn 2.0.89", 1849 | ] 1850 | 1851 | [[package]] 1852 | name = "zerofrom" 1853 | version = "0.1.5" 1854 | source = "registry+https://github.com/rust-lang/crates.io-index" 1855 | checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" 1856 | dependencies = [ 1857 | "zerofrom-derive", 1858 | ] 1859 | 1860 | [[package]] 1861 | name = "zerofrom-derive" 1862 | version = "0.1.5" 1863 | source = "registry+https://github.com/rust-lang/crates.io-index" 1864 | checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" 1865 | dependencies = [ 1866 | "proc-macro2", 1867 | "quote", 1868 | "syn 2.0.89", 1869 | "synstructure", 1870 | ] 1871 | 1872 | [[package]] 1873 | name = "zeroize" 1874 | version = "1.8.1" 1875 | source = "registry+https://github.com/rust-lang/crates.io-index" 1876 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 1877 | 1878 | [[package]] 1879 | name = "zerovec" 1880 | version = "0.10.4" 1881 | source = "registry+https://github.com/rust-lang/crates.io-index" 1882 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 1883 | dependencies = [ 1884 | "yoke", 1885 | "zerofrom", 1886 | "zerovec-derive", 1887 | ] 1888 | 1889 | [[package]] 1890 | name = "zerovec-derive" 1891 | version = "0.10.3" 1892 | source = "registry+https://github.com/rust-lang/crates.io-index" 1893 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 1894 | dependencies = [ 1895 | "proc-macro2", 1896 | "quote", 1897 | "syn 2.0.89", 1898 | ] 1899 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 2 | [package] 3 | name = "trackspeedtest" 4 | version = "0.4.0" 5 | authors = ["Giovanni Bassi "] 6 | edition = "2021" 7 | exclude = [ 8 | "Dockerfile", 9 | ".*", 10 | "data/", 11 | "target/", 12 | "Makefile", 13 | ".vscode/", 14 | "github/", 15 | ] 16 | 17 | [profile.release] 18 | lto = true 19 | strip = "symbols" 20 | 21 | [dependencies] 22 | chrono = "0.4.38" 23 | clap = "2.*" 24 | csv = "1.3.1" 25 | derivative = "2.2.0" 26 | lettre = { version = "0.11.10", features = ["rustls-tls"] } 27 | lettre_email = "0.9.4" 28 | openssl = { version = "0.10.68", features = ["vendored"] } 29 | rev_lines = "0.3.0" 30 | serde = { version = "1.0.215", features = ["derive"] } 31 | serde_json = "1.0.133" 32 | which = "7.0.0" 33 | 34 | [dev-dependencies] 35 | pretty_assertions = "1.4.1" 36 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.20 AS bins 2 | ARG PLATFORM 3 | RUN wget https://install.speedtest.net/app/cli/ookla-speedtest-1.2.0-linux-${PLATFORM}.tgz -O speedtest.tgz && \ 4 | tar -xvzf speedtest.tgz && \ 5 | mv ./speedtest /usr/bin/ && \ 6 | rm speedtest.* 7 | COPY target/output/trackspeedtest /app/trackspeedtest 8 | RUN apk add binutils && strip /app/trackspeedtest 9 | 10 | FROM opensuse/leap:15.5 AS opensuse 11 | RUN ldd /bin/echo | tr -s '[:blank:]' '\n' | grep '^/' | \ 12 | xargs -I % sh -c 'mkdir -p $(dirname deps%); cp % deps%;' 13 | 14 | FROM scratch 15 | LABEL maintainer="giggio@giggio.net" 16 | ENTRYPOINT [ "/trackspeedtest" ] 17 | COPY --from=bins /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 18 | COPY --from=bins /usr/bin/speedtest . 19 | COPY --from=opensuse /bin/echo . 20 | COPY --from=opensuse /deps / 21 | COPY --from=bins /app/trackspeedtest . 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Giovanni Bassi 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. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: default build test clean run run_without_network build_release build_amd64_static docker_build_amd64_static release_amd64_static build_armv7_static docker_build_armv7_static release_armv7_static build_arm64_static docker_build_arm64_static release_arm64_static release_with_docker_only release 2 | 3 | amd64_target := x86_64-unknown-linux-musl 4 | arm32v7_target := armv7-unknown-linux-musleabihf 5 | arm64_target := aarch64-unknown-linux-musl 6 | default: release_static_arm 7 | 8 | build: 9 | cargo build 10 | 11 | test: 12 | cargo test 13 | 14 | clean: 15 | cargo clean 16 | 17 | run: 18 | cargo run 19 | 20 | run_without_network: 21 | unshare -r -n -- cargo run 22 | 23 | build_release: 24 | cargo build --release 25 | 26 | build_amd64_static: 27 | cross build --release --target $(amd64_target) 28 | 29 | docker_build_amd64_static: 30 | mkdir -p target/output 31 | cp target/$(amd64_target)/release/trackspeedtest target/output/ 32 | VERSION=$$(./target/output/trackspeedtest --version | cut -f2 -d ' '); \ 33 | docker buildx build -t giggio/speedtest:$$VERSION-amd64 -t giggio/speedtest:amd64 --platform linux/amd64 --build-arg PLATFORM=x86_64 --push . 34 | 35 | release_amd64_static: build_amd64_static docker_build_amd64_static 36 | 37 | build_armv7_static: 38 | cross build --release --target $(arm32v7_target) 39 | 40 | docker_build_armv7_static: 41 | mkdir -p target/output 42 | cp target/$(arm32v7_target)/release/trackspeedtest target/output/ 43 | VERSION=$$(./target/output/trackspeedtest --version | cut -f2 -d ' '); \ 44 | docker buildx build -t giggio/speedtest:$$VERSION-arm32v7 -t giggio/speedtest:arm32v7 --platform linux/arm/v7 --build-arg PLATFORM=armhf --push . 45 | 46 | release_armv7_static: build_armv7_static docker_build_armv7_static 47 | 48 | build_arm64_static: 49 | cross build --release --target $(arm64_target) 50 | 51 | docker_build_arm64_static: 52 | mkdir -p target/output 53 | cp target/$(arm64_target)/release/trackspeedtest target/output/ 54 | VERSION=$$(./target/output/trackspeedtest --version | cut -f2 -d ' '); \ 55 | docker buildx build -t giggio/speedtest:$$VERSION-arm64 -t giggio/speedtest:arm64 --platform linux/arm64 --build-arg PLATFORM=aarch64 --push . 56 | 57 | release_arm64_static: build_arm64_static docker_build_arm64_static 58 | 59 | release_with_docker_only: docker_build_amd64_static docker_build_armv7_static docker_build_arm64_static 60 | docker buildx imagetools create -t giggio/speedtest:latest \ 61 | giggio/speedtest:amd64 \ 62 | giggio/speedtest:arm32v7 \ 63 | giggio/speedtest:arm64; \ 64 | VERSION=$$(./target/$(amd64_target)/release/trackspeedtest --version | cut -f2 -d ' '); \ 65 | docker buildx imagetools create -t giggio/speedtest:$$VERSION \ 66 | giggio/speedtest:$$VERSION-amd64 \ 67 | giggio/speedtest:$$VERSION-arm32v7 \ 68 | giggio/speedtest:$$VERSION-arm64; 69 | 70 | release: release_amd64_static release_armv7_static release_arm64_static release_with_docker_only 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Speed test 2 | 3 | [![Docker Stars](https://img.shields.io/docker/stars/giggio/speedtest.svg)](https://hub.docker.com/r/giggio/speedtest/) 4 | [![Docker Pulls](https://img.shields.io/docker/pulls/giggio/speedtest.svg)](https://hub.docker.com/r/giggio/speedtest/) 5 | 6 | This app runs a speed test and saves the history in .json files and an aggregate .csv 7 | file. It can also alert you with an e-mail if it finds that the bandwidth is bellow 8 | an specified value. 9 | 10 | This can be run on Linux for AMD64 and ARMv7. 11 | 12 | ## Upstream Links 13 | 14 | * Docker Registry @ [giggio/speedtest](https://hub.docker.com/r/giggio/speedtest/) 15 | * GitHub @ [giggio/speedtest](https://github.com/giggio/speedtest) 16 | 17 | ## Quick Start 18 | 19 | You need to mount a volume to `/data`, and the files will be saved there. 20 | Run it like this: 21 | 22 | ````bash 23 | docker run --rm -ti -v `pwd`/data:/data giggio/speedtest run 24 | ```` 25 | 26 | After running will have a .json file with a date/time structure 27 | (e.g. 202011212124.json) and a `speed.csv` file. 28 | 29 | ## Add a cron 30 | 31 | To have a history a good idea is to add a cron job (with `crontab -e`) like 32 | this: 33 | 34 | ````cron 35 | 0 */3 * * * docker run --rm -ti -v /path/to/my/data:/data giggio/speedtest run 36 | ```` 37 | 38 | ### Detailed commands 39 | 40 | There are two commands: `run` and `alert`. The former runs the speed test, the 41 | second alerts you for a bandwidth bellow specification. 42 | 43 | All commands have a `-v` option for verbose output, and you can get help by 44 | running `docker run --rm giggio/speedtest --help`. 45 | 46 | #### Running a speed test 47 | 48 | To view available args run: 49 | 50 | ````bash 51 | docker run --rm giggio/speedtest run --help 52 | ```` 53 | 54 | This command has a simulated argument, which will make it simply drop some results 55 | into the data folder. It is useful to help you setup your infrastructure without 56 | having to wait for a full speed test to run and also does not use any bandwidth. 57 | It simply saves the files. 58 | 59 | This command can optionally sends an e-mail when the measurement fails and will 60 | require mail parameters if you want to use that functionality (see bellow). 61 | 62 | #### Alerting 63 | 64 | To view available args run: 65 | 66 | ````bash 67 | docker run --rm giggio/speedtest alert --help 68 | ```` 69 | 70 | This command sends e-mails and will require mail parameters (see bellow). 71 | It also has a simulate argument. It will not send the email, but simply write to 72 | the terminal on stdout what it would send through in an e-mail. 73 | 74 | It will take the last 8 results (customizable with `--count`) and average them. 75 | If the value is above the threshold the e-mail is sent. 76 | 77 | You need to supply the expected upload and download bandwidth, and you may 78 | optionally supply a threshold to when the e-mail should be sent (defaults to 20%). 79 | 80 | #### E-mail options 81 | 82 | Commands that send e-mail will do so using SMTP. You have to supply the values 83 | like server, port, sender and destination e-mail addresses etc. Authentication 84 | information is optional, but most mail servers will require it. 85 | 86 | ## Background 87 | 88 | This project was previosly made up of a few bash scripts and a Node.js tool 89 | to measure the results. 90 | This new version is written in Rust and is using the official 91 | [CLI from Ookla](https://www.speedtest.net/apps/cli). It is much faster 92 | (Rust <3) and, due to using the official Ookla CLI, more acurate. The 93 | container is also much smaller, simply containing the binaries, written from 94 | scratch, without any distro files. 95 | 96 | Ookla's tool does not support a some of the information that the Node.js tool 97 | supported (server latitude, longitude, distance and server ping). It still 98 | supplies the most important values, like upload and download bandwidth, ping 99 | latency, ISP, server host, city and country. The columns in the CSV file that 100 | had that information are now null and will be eventually removed. 101 | 102 | Also, the .json files format is now in a different format from before. 103 | 104 | ## Contributing 105 | 106 | Questions, comments, bug reports, and pull requests are all welcome. Submit them at 107 | [the project on GitHub](https://github.com/giggio/speedtest/). 108 | 109 | Bug reports that include steps-to-reproduce (including code) are the 110 | best. Even better, make them in the form of pull requests. 111 | 112 | ## Author 113 | 114 | [Giovanni Bassi](https://github.com/giggio) 115 | 116 | ## License 117 | 118 | Licensed under the MIT license. 119 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "fenix": { 4 | "inputs": { 5 | "nixpkgs": "nixpkgs", 6 | "rust-analyzer-src": "rust-analyzer-src" 7 | }, 8 | "locked": { 9 | "lastModified": 1732689334, 10 | "narHash": "sha256-yKI1KiZ0+bvDvfPTQ1ZT3oP/nIu3jPYm4dnbRd6hYg4=", 11 | "owner": "nix-community", 12 | "repo": "fenix", 13 | "rev": "a8a983027ca02b363dfc82fbe3f7d9548a8d3dce", 14 | "type": "github" 15 | }, 16 | "original": { 17 | "owner": "nix-community", 18 | "repo": "fenix", 19 | "type": "github" 20 | } 21 | }, 22 | "flake-utils": { 23 | "inputs": { 24 | "systems": "systems" 25 | }, 26 | "locked": { 27 | "lastModified": 1731533236, 28 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 29 | "owner": "numtide", 30 | "repo": "flake-utils", 31 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 32 | "type": "github" 33 | }, 34 | "original": { 35 | "owner": "numtide", 36 | "repo": "flake-utils", 37 | "type": "github" 38 | } 39 | }, 40 | "nixpkgs": { 41 | "locked": { 42 | "lastModified": 1732521221, 43 | "narHash": "sha256-2ThgXBUXAE1oFsVATK1ZX9IjPcS4nKFOAjhPNKuiMn0=", 44 | "owner": "nixos", 45 | "repo": "nixpkgs", 46 | "rev": "4633a7c72337ea8fd23a4f2ba3972865e3ec685d", 47 | "type": "github" 48 | }, 49 | "original": { 50 | "owner": "nixos", 51 | "ref": "nixos-unstable", 52 | "repo": "nixpkgs", 53 | "type": "github" 54 | } 55 | }, 56 | "nixpkgs_2": { 57 | "locked": { 58 | "lastModified": 1732617236, 59 | "narHash": "sha256-PYkz6U0bSEaEB1al7O1XsqVNeSNS+s3NVclJw7YC43w=", 60 | "owner": "NixOS", 61 | "repo": "nixpkgs", 62 | "rev": "af51545ec9a44eadf3fe3547610a5cdd882bc34e", 63 | "type": "github" 64 | }, 65 | "original": { 66 | "id": "nixpkgs", 67 | "type": "indirect" 68 | } 69 | }, 70 | "root": { 71 | "inputs": { 72 | "fenix": "fenix", 73 | "flake-utils": "flake-utils", 74 | "nixpkgs": "nixpkgs_2" 75 | } 76 | }, 77 | "rust-analyzer-src": { 78 | "flake": false, 79 | "locked": { 80 | "lastModified": 1732633904, 81 | "narHash": "sha256-7VKcoLug9nbAN2txqVksWHHJplqK9Ou8dXjIZAIYSGc=", 82 | "owner": "rust-lang", 83 | "repo": "rust-analyzer", 84 | "rev": "8d5e91c94f80c257ce6dbdfba7bd63a5e8a03fa6", 85 | "type": "github" 86 | }, 87 | "original": { 88 | "owner": "rust-lang", 89 | "ref": "nightly", 90 | "repo": "rust-analyzer", 91 | "type": "github" 92 | } 93 | }, 94 | "systems": { 95 | "locked": { 96 | "lastModified": 1681028828, 97 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 98 | "owner": "nix-systems", 99 | "repo": "default", 100 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 101 | "type": "github" 102 | }, 103 | "original": { 104 | "owner": "nix-systems", 105 | "repo": "default", 106 | "type": "github" 107 | } 108 | } 109 | }, 110 | "root": "root", 111 | "version": 7 112 | } 113 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Speed test"; 3 | 4 | inputs = { 5 | flake-utils.url = "github:numtide/flake-utils"; 6 | fenix.url = "github:nix-community/fenix"; 7 | }; 8 | 9 | outputs = { self, nixpkgs, flake-utils, fenix }: 10 | flake-utils.lib.eachDefaultSystem (system: 11 | let 12 | pkgs = import nixpkgs { 13 | inherit system; 14 | overlays = [ fenix.overlays.default ]; 15 | }; 16 | in 17 | { 18 | devShells.default = import ./shell.nix { inherit pkgs; }; 19 | } 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | pkgs.mkShell { 3 | # nativeBuildInputs is usually what you want -- tools you need to run 4 | nativeBuildInputs = with pkgs.buildPackages; [ 5 | (fenix.stable.withComponents [ 6 | "cargo" 7 | "clippy" 8 | "rust-src" 9 | "rustc" 10 | "rustfmt" 11 | ]) 12 | ]; 13 | } -------------------------------------------------------------------------------- /src/alert.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, NaiveDateTime, Utc}; 2 | use rev_lines::RevLines; 3 | use serde::{de, Deserialize, Deserializer}; 4 | use std::fs::File; 5 | use std::io::prelude::*; 6 | use std::io::BufReader; 7 | 8 | use crate::args::Alert; 9 | use crate::mail; 10 | 11 | pub fn alert(alert: Alert) -> Result<(), Option> { 12 | let results = match get_latest_results(alert.count)? { 13 | Some(results) => results, 14 | None => { 15 | println!("Not enough results to report yet."); 16 | return Ok(()); 17 | } 18 | }; 19 | let average = get_average(results); 20 | if average_is_bellow(&average, &alert) { 21 | send_email(average, alert)?; 22 | } 23 | Ok(()) 24 | } 25 | 26 | fn send_email(average: Average, alert: Alert) -> Result<(), String> { 27 | let message_body = format!( 28 | "Latest bandwidth measurements found a discrepancy.\n\ 29 | Expected badwidth was {} mpbs for download and {} mbps for upload.\n\ 30 | Found {:.2} mbps for download and {:.2} mbps for upload, for the last ~{} hours ({} samples).", 31 | alert.expected_download, 32 | alert.expected_upload, 33 | average.download, 34 | average.upload, 35 | average.period_in_hours, 36 | alert.count 37 | ); 38 | mail::send_mail( 39 | alert.simulate, 40 | alert.email, 41 | "Bandwith bellow expectation", 42 | &message_body, 43 | alert.smtp, 44 | )?; 45 | Ok(()) 46 | } 47 | 48 | fn average_is_bellow(average: &Average, alert: &Alert) -> bool { 49 | average.upload < alert.expected_upload * (1.0 - alert.threshold as f64 / 100.0) 50 | || average.download < alert.expected_download * (1.0 - alert.threshold as f64 / 100.0) 51 | } 52 | 53 | fn get_average(results: Vec) -> Average { 54 | let mut dl = 0.0; 55 | let mut ul = 0.0; 56 | let len = results.len(); 57 | let mut min_date = DateTime::::MAX_UTC; 58 | let mut max_date = DateTime::::MIN_UTC; 59 | for result in results.into_iter() { 60 | dl += result.speeds_download; 61 | ul += result.speeds_upload; 62 | if result.date < min_date { 63 | min_date = result.date; 64 | } 65 | if result.date > max_date { 66 | max_date = result.date; 67 | } 68 | } 69 | Average { 70 | download: dl / len as f64, 71 | upload: ul / len as f64, 72 | period_in_hours: ((max_date - min_date).num_minutes() as f64 / 60.0).round() as i64, 73 | } 74 | } 75 | 76 | fn get_latest_results(count: u8) -> Result>, String> { 77 | let cwd = std::env::current_dir() 78 | .map_err(|err| format!("Error when finding current working directory: {}", err))?; 79 | let data_dir = cwd.join("data"); 80 | let file_path = data_dir.join("speed.csv"); 81 | let file = if file_path.exists() { 82 | File::open(&file_path).map_err(|err| format!("Error when opening summary file: {}", err))? 83 | } else { 84 | return Ok(None); 85 | }; 86 | let mut lines = BufReader::new(&file).lines(); 87 | let first_line = lines.next(); 88 | if lines.count() < count as usize { 89 | return Ok(None); 90 | } 91 | let revlines = RevLines::new(&file); 92 | let mut last_lines: Vec = Result::from_iter(revlines.take(count as usize)) 93 | .map_err(|err| format!("Error when opening file: {}", err))?; 94 | last_lines.splice(0..0, vec![first_line.unwrap().unwrap()]); 95 | let text = last_lines.into_iter().fold(String::new(), |mut str, item| { 96 | str.push_str(&item); 97 | str.push('\n'); 98 | str 99 | }); 100 | let mut rdr = csv::Reader::from_reader(text.as_bytes()); 101 | let results: Vec = rdr 102 | .deserialize::() 103 | .filter_map(|result| result.ok()) 104 | .collect(); 105 | if results.iter().len() != count as usize { 106 | return Err("Error deserializing csv.".to_owned()); 107 | } 108 | Ok(Some(results)) 109 | } 110 | 111 | fn date_time_from_str<'de, D>(deserializer: D) -> Result, D::Error> 112 | where 113 | D: Deserializer<'de>, 114 | { 115 | let s: String = Deserialize::deserialize(deserializer)?; 116 | let ndt = NaiveDateTime::parse_from_str(&s, "%Y/%m/%d %H:%M:%S").map_err(de::Error::custom)?; 117 | Ok(DateTime::from_naive_utc_and_offset(ndt, Utc)) 118 | } 119 | 120 | #[derive(Debug, Deserialize)] 121 | #[serde()] 122 | struct ResultCsv { 123 | #[serde(deserialize_with = "date_time_from_str")] 124 | date: DateTime, 125 | speeds_download: f64, 126 | speeds_upload: f64, 127 | } 128 | 129 | #[derive(PartialEq, Debug)] 130 | struct Average { 131 | upload: f64, 132 | download: f64, 133 | period_in_hours: i64, 134 | } 135 | 136 | #[cfg(test)] 137 | mod tests { 138 | mod calculate_average { 139 | use chrono::prelude::*; 140 | use chrono::Utc; 141 | use pretty_assertions::assert_eq; 142 | 143 | use super::super::*; 144 | #[test] 145 | fn average_calculated_with_single_item() { 146 | assert_eq!( 147 | Average { 148 | download: 100.0, 149 | upload: 200.0, 150 | period_in_hours: 0 151 | }, 152 | get_average(vec![ResultCsv { 153 | date: Utc::now(), 154 | speeds_download: 100.0, 155 | speeds_upload: 200.0, 156 | }]) 157 | ); 158 | } 159 | 160 | #[test] 161 | fn average_calculated_with_two_items() { 162 | assert_eq!( 163 | Average { 164 | download: 60.0, 165 | upload: 120.0, 166 | period_in_hours: 2 167 | }, 168 | get_average(vec![ 169 | ResultCsv { 170 | date: Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(), 171 | speeds_download: 20.0, 172 | speeds_upload: 40.0, 173 | }, 174 | ResultCsv { 175 | date: Utc.with_ymd_and_hms(2021, 1, 1, 2, 0, 0).unwrap(), 176 | speeds_download: 100.0, 177 | speeds_upload: 200.0, 178 | } 179 | ]) 180 | ); 181 | } 182 | 183 | #[test] 184 | fn average_approximate_hours() { 185 | assert_eq!( 186 | Average { 187 | download: 1.0, 188 | upload: 1.0, 189 | period_in_hours: 2 190 | }, 191 | get_average(vec![ 192 | ResultCsv { 193 | date: Utc.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(), 194 | speeds_download: 1.0, 195 | speeds_upload: 1.0, 196 | }, 197 | ResultCsv { 198 | date: Utc.with_ymd_and_hms(2021, 1, 1, 1, 59, 0).unwrap(), 199 | speeds_download: 1.0, 200 | speeds_upload: 1.0, 201 | } 202 | ]) 203 | ); 204 | } 205 | } 206 | 207 | mod check_average { 208 | use crate::args::Smtp; 209 | 210 | use super::super::*; 211 | #[test] 212 | fn when_has_one_value_exactly_at_the_average_it_is_ok() { 213 | assert!(!average_is_bellow( 214 | &Average { 215 | download: 100.0, 216 | upload: 100.0, 217 | period_in_hours: 5 218 | }, 219 | &create_alert(0, 100.0, 100.0) 220 | )); 221 | } 222 | 223 | #[test] 224 | fn when_has_download_bellow_average_it_is_not_ok() { 225 | assert!(average_is_bellow( 226 | &Average { 227 | download: 10.0, 228 | upload: 100.0, 229 | period_in_hours: 5 230 | }, 231 | &create_alert(0, 100.0, 100.0) 232 | )); 233 | } 234 | 235 | #[test] 236 | fn when_has_upload_bellow_average_it_is_not_ok() { 237 | assert!(average_is_bellow( 238 | &Average { 239 | download: 100.0, 240 | upload: 10.0, 241 | period_in_hours: 5 242 | }, 243 | &create_alert(0, 100.0, 100.0) 244 | )); 245 | } 246 | 247 | #[test] 248 | fn when_has_one_value_bellow_average_but_within_threshold_it_is_ok() { 249 | assert!(!average_is_bellow( 250 | &Average { 251 | download: 90.0, 252 | upload: 90.0, 253 | period_in_hours: 5 254 | }, 255 | &create_alert(20, 100.0, 100.0) 256 | )); 257 | } 258 | 259 | fn create_alert(threshold: u8, download: f64, upload: f64) -> Alert { 260 | Alert { 261 | simulate: false, 262 | count: 1, 263 | threshold, 264 | expected_download: download, 265 | expected_upload: upload, 266 | email: "".to_owned(), 267 | smtp: Smtp { 268 | email: "".to_owned(), 269 | server: "".to_owned(), 270 | port: 0, 271 | credentials: None, 272 | }, 273 | } 274 | } 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | use clap::{App, AppSettings, Arg, SubCommand}; 2 | 3 | #[derive(Debug)] 4 | pub struct Args { 5 | pub verbose: bool, 6 | pub command: Option, 7 | } 8 | 9 | #[derive(Debug)] 10 | pub enum Command { 11 | Run(Run), 12 | Alert(Alert), 13 | } 14 | 15 | impl Args { 16 | pub fn new() -> Args { 17 | Args::new_from(&mut std::env::args_os()).unwrap_or_else(|err| err.exit()) 18 | } 19 | fn new_from(args: I) -> Result 20 | where 21 | I: Iterator, 22 | T: Into + Clone, 23 | { 24 | let args = Args::get_args_app().get_matches_from_safe(args)?; 25 | Ok(Args { 26 | verbose: args.occurrences_of("v") > 0, 27 | command: Args::get_config_from_cl(args), 28 | }) 29 | } 30 | 31 | fn get_args_app<'a, 'b>() -> App<'a, 'b> { 32 | App::new("trackspeedtest") 33 | .version(env!("CARGO_PKG_VERSION")) 34 | .author("Giovanni Bassi ") 35 | .about("Runs and manages speed tests") 36 | .setting(AppSettings::ArgRequiredElseHelp) 37 | .arg( 38 | Arg::with_name("v") 39 | .short("v") 40 | .long("verbose") 41 | .global(true) 42 | .multiple(true) 43 | .help("Sets the level of verbosity"), 44 | ) 45 | .subcommand( 46 | SubCommand::with_name("alert") 47 | .about("Sends an e-mail message if the average of the last measurements is bellow a bandwith value") 48 | .arg( 49 | Arg::with_name("sender email") 50 | .takes_value(true) 51 | .index(1) 52 | .required(true) 53 | .help("E-mail address to send the alert message from"), 54 | ) 55 | .arg( 56 | Arg::with_name("email") 57 | .takes_value(true) 58 | .index(2) 59 | .required(true) 60 | .help("E-mail address to send the alert message to"), 61 | ) 62 | .arg( 63 | Arg::with_name("smtp server") 64 | .long("smtp") 65 | .takes_value(true) 66 | .index(3) 67 | .required(true) 68 | .help("SMTP server and port to use, use server:port") 69 | .validator(|server_and_port| { 70 | let parts: Vec<&str> = server_and_port.split(':').collect(); 71 | if parts.len() != 2 { 72 | return Err("Not valid server".to_owned()); 73 | } 74 | if parts[1].parse::().is_err() { 75 | return Err("Port is not in the correct format.".to_owned()); 76 | } 77 | Ok(()) 78 | }), 79 | ) 80 | .arg( 81 | Arg::with_name("upload") 82 | .long("upload") 83 | .takes_value(true) 84 | .index(4) 85 | .required(true) 86 | .help("Expected upload bandwidth, in mbps (e.g. 123.45)") 87 | .validator(|v| { 88 | if v.parse::().is_err() { 89 | return Err("Upload bandwidth is not in the correct format.".to_owned()); 90 | } 91 | Ok(()) 92 | }), 93 | ) 94 | .arg( 95 | Arg::with_name("download") 96 | .long("download") 97 | .takes_value(true) 98 | .index(5) 99 | .required(true) 100 | .help("Expected download bandwidth, in mbps (e.g. 123.45)") 101 | .validator(|v| { 102 | if v.parse::().is_err() { 103 | return Err("Download bandwidth is not in the correct format.".to_owned()); 104 | } 105 | Ok(()) 106 | }), 107 | ) 108 | .arg( 109 | Arg::with_name("simulate") 110 | .short("s") 111 | .long("simulate") 112 | .help("Should write email to stdout instead of sending e-mail"), 113 | ) 114 | .arg( 115 | Arg::with_name("threshold") 116 | .short("t") 117 | .long("threshold") 118 | .takes_value(true) 119 | .required(true) 120 | .help("Threshold percentage. If measured values follow bellow this amount an e-mail message is sent. It has to be an integer.") 121 | .default_value("20") 122 | .validator(|v| { 123 | if v.parse::().is_err() { 124 | return Err("Threshold is not in the correct format.".to_owned()); 125 | } 126 | Ok(()) 127 | }), 128 | ) 129 | .arg( 130 | Arg::with_name("count") 131 | .short("c") 132 | .long("count") 133 | .takes_value(true) 134 | .required(true) 135 | .help("How many measurements are used to make up the average") 136 | .default_value("8") 137 | .validator(|v| { 138 | if v.parse::().is_err() { 139 | return Err("Measurement count is not in the correct format.".to_owned()); 140 | } 141 | Ok(()) 142 | }), 143 | ) 144 | .arg( 145 | Arg::with_name("username") 146 | .short("u") 147 | .long("username") 148 | .takes_value(true) 149 | .requires("password") 150 | .help("SMTP server user for authentication"), 151 | ) 152 | .arg( 153 | Arg::with_name("password") 154 | .short("p") 155 | .long("password") 156 | .takes_value(true) 157 | .requires("username") 158 | .help("SMTP server password for authentication"), 159 | ) 160 | .arg( 161 | Arg::with_name("hours") 162 | .short("H") 163 | .long("hours") 164 | .default_value("24") 165 | .help("Last hours to use as average"), 166 | ), 167 | ) 168 | .subcommand( 169 | SubCommand::with_name("run") 170 | .about("Runs the speed test.") 171 | .arg( 172 | Arg::with_name("simulate") 173 | .short("s") 174 | .long("simulate") 175 | .help("Should simulate instead of running speed test"), 176 | ) 177 | .arg( 178 | Arg::with_name("show_results") 179 | .long("show-results") 180 | .help("Sends results to stdout, one result per line: download, upload, ping"), 181 | ) 182 | .arg( 183 | Arg::with_name("sender email") 184 | .short("e") 185 | .long("sender") 186 | .takes_value(true) 187 | .requires_all(&["email", "smtp server"]) 188 | .help("E-mail address to send the alert message from"), 189 | ) 190 | .arg( 191 | Arg::with_name("email") 192 | .long("to") 193 | .takes_value(true) 194 | .requires_all(&["sender email", "smtp server"]) 195 | .help("E-mail address to send the alert message to"), 196 | ) 197 | .arg( 198 | Arg::with_name("smtp server") 199 | .long("smtp") 200 | .takes_value(true) 201 | .requires_all(&["sender email", "email"]) 202 | .help("SMTP server and port to use, use server:port") 203 | .validator(|server_and_port| { 204 | let parts: Vec<&str> = server_and_port.split(':').collect(); 205 | if parts.len() != 2 { 206 | return Err("Not valid server".to_owned()); 207 | } 208 | if parts[1].parse::().is_err() { 209 | return Err("Port is not in the correct format.".to_owned()); 210 | } 211 | Ok(()) 212 | }), 213 | ) 214 | .arg( 215 | Arg::with_name("username") 216 | .short("u") 217 | .long("username") 218 | .requires("password") 219 | .takes_value(true) 220 | .help("SMTP server user for authentication"), 221 | ) 222 | .arg( 223 | Arg::with_name("password") 224 | .short("p") 225 | .long("password") 226 | .requires("username") 227 | .takes_value(true) 228 | .help("SMTP server password for authentication"), 229 | ), 230 | ) 231 | } 232 | 233 | fn get_smtp_from_cl(args: &clap::ArgMatches) -> Option { 234 | let smtp = if let Some(server_and_port) = args.value_of("smtp server") { 235 | let parts: Vec<&str> = server_and_port.split(':').collect(); 236 | let server = parts[0]; 237 | let port = parts[1].parse::().ok()?; 238 | let credentials = if let (Some(username), Some(password)) = 239 | (args.value_of("username"), args.value_of("password")) 240 | { 241 | Some(Credentials { 242 | username: username.to_owned(), 243 | password: password.to_owned(), 244 | }) 245 | } else { 246 | None 247 | }; 248 | let email = args.value_of("sender email")?; 249 | Some(Smtp { 250 | email: email.to_owned(), 251 | server: server.to_owned(), 252 | port, 253 | credentials, 254 | }) 255 | } else { 256 | None 257 | }; 258 | smtp 259 | } 260 | 261 | fn get_config_from_cl(args: clap::ArgMatches) -> Option { 262 | match args.subcommand() { 263 | ("run", Some(run_args)) => Some(Command::Run(Run { 264 | simulate: run_args.is_present("simulate"), 265 | email_options: EmailOptions::new_from_args(run_args), 266 | show_results: run_args.is_present("show_results"), 267 | })), 268 | ("alert", Some(alert_args)) => Some(Command::Alert(Alert { 269 | simulate: alert_args.is_present("simulate"), 270 | email: alert_args.value_of("email").unwrap().to_owned(), 271 | expected_download: alert_args 272 | .value_of("download") 273 | .unwrap() 274 | .parse::() 275 | .unwrap(), 276 | expected_upload: alert_args 277 | .value_of("upload") 278 | .unwrap() 279 | .parse::() 280 | .unwrap(), 281 | threshold: alert_args 282 | .value_of("threshold") 283 | .unwrap() 284 | .parse::() 285 | .unwrap(), 286 | count: alert_args.value_of("count").unwrap().parse::().unwrap(), 287 | smtp: Args::get_smtp_from_cl(alert_args).unwrap(), 288 | })), 289 | _ => None, 290 | } 291 | } 292 | } 293 | 294 | #[derive(Debug)] 295 | pub struct Run { 296 | pub simulate: bool, 297 | pub email_options: Option, 298 | pub show_results: bool, 299 | } 300 | 301 | #[derive(Debug)] 302 | pub struct Alert { 303 | pub simulate: bool, 304 | pub email: String, 305 | pub expected_download: f64, 306 | pub expected_upload: f64, 307 | pub threshold: u8, 308 | pub count: u8, 309 | pub smtp: Smtp, 310 | } 311 | 312 | #[derive(Debug)] 313 | pub struct Smtp { 314 | pub server: String, 315 | pub email: String, 316 | pub port: u16, 317 | pub credentials: Option, 318 | } 319 | 320 | #[derive(Debug)] 321 | pub struct Credentials { 322 | pub username: String, 323 | pub password: String, 324 | } 325 | 326 | #[derive(Debug)] 327 | pub struct EmailOptions { 328 | pub email: String, 329 | pub smtp: Smtp, 330 | } 331 | impl EmailOptions { 332 | pub fn new_from_args(args: &clap::ArgMatches) -> Option { 333 | if let (Some(email), Some(smtp)) = ( 334 | args.value_of("email").map(|str| str.to_owned()), 335 | Args::get_smtp_from_cl(args), 336 | ) { 337 | Some(EmailOptions { email, smtp }) 338 | } else { 339 | None 340 | } 341 | } 342 | } 343 | 344 | #[cfg(test)] 345 | mod tests { 346 | use super::*; 347 | 348 | #[test] 349 | fn args_run_simulated() { 350 | let run = match Args::new_from(["trackspeedtest", "run", "--simulate"].iter()) 351 | .unwrap() 352 | .command 353 | .unwrap() 354 | { 355 | Command::Alert(_) => panic!("Should not be alert"), 356 | Command::Run(run) => run, 357 | }; 358 | assert!(run.simulate); 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | macro_rules! printlnv { 2 | ($($arg:tt)*) => ({ 3 | unsafe { 4 | if $crate::VERBOSE { 5 | println!($($arg)*); 6 | } 7 | } 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /src/mail.rs: -------------------------------------------------------------------------------- 1 | use std::str; 2 | 3 | use lettre::transport::smtp::authentication::Credentials; 4 | use lettre::{Message, SmtpTransport, Transport}; 5 | 6 | use crate::args::Smtp; 7 | 8 | fn get_mailer(smtp: &Smtp) -> Result { 9 | let mut smtp_transport_builder = SmtpTransport::relay(&smtp.server) 10 | .map_err(|err| { 11 | format!( 12 | "Could not create smtp transport with relay '{}'. Error: {}", 13 | smtp.server, err 14 | ) 15 | })? 16 | .port(smtp.port); 17 | if let Some(credentials) = &smtp.credentials { 18 | let creds = Credentials::new(credentials.username.clone(), credentials.password.clone()); 19 | smtp_transport_builder = smtp_transport_builder.credentials(creds); 20 | } 21 | Ok(smtp_transport_builder.build()) 22 | } 23 | 24 | pub fn send_mail( 25 | simulate: bool, 26 | email_address: String, 27 | subject: &str, 28 | message_body: &str, 29 | smtp: Smtp, 30 | ) -> Result<(), String> { 31 | if simulate { 32 | println!( 33 | "--------------\nWould be sending e-mail message to: {}\nSubject: {}\nBody:\n{}\n--------------\n", 34 | email_address, subject, message_body 35 | ); 36 | } else { 37 | printlnv!("Preparing e-mail..."); 38 | let email = Message::builder() 39 | .from(smtp.email.parse().map_err(|err| { 40 | format!( 41 | "Could not convert email for 'from' from text '{}'. Error: {}", 42 | smtp.email, err 43 | ) 44 | })?) 45 | .to(email_address.parse().map_err(|err| { 46 | format!( 47 | "Could not convert email for 'to' from text '{}'. Error: {}", 48 | email_address, err 49 | ) 50 | })?) 51 | .subject(subject) 52 | .body(message_body.to_owned()) 53 | .map_err(|err| format!("Error when creating email: {}", err))?; 54 | printlnv!("Preparing mailer..."); 55 | let mailer = get_mailer(&smtp)?; 56 | printlnv!( 57 | "Sending e-mail message to: {}\nSubject: {}\nBody:\n{}", 58 | email_address, 59 | subject, 60 | message_body 61 | ); 62 | let result = mailer.send(&email); 63 | if let Err(err) = result { 64 | printlnv!("E-mail message was NOT sent successfully.\nError:\n{}", err); 65 | return Err("Could not send email.".to_owned()); 66 | } else { 67 | printlnv!("E-mail message was sent successfully."); 68 | } 69 | } 70 | Ok(()) 71 | } 72 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | mod macros; 3 | #[macro_use] 4 | extern crate derivative; 5 | mod alert; 6 | mod args; 7 | mod mail; 8 | mod run; 9 | use args::{Args, Command}; 10 | 11 | static mut VERBOSE: bool = false; 12 | 13 | fn main() { 14 | match run() { 15 | Err(None) => std::process::exit(1), 16 | Err(Some(x)) => { 17 | eprintln!("{}", x); 18 | std::process::exit(1); 19 | } 20 | Ok(_) => std::process::exit(0), 21 | } 22 | } 23 | 24 | fn run() -> Result<(), Option> { 25 | let args = Args::new(); 26 | unsafe { 27 | VERBOSE = args.verbose; 28 | } 29 | printlnv!("Args are {:?}.", args); 30 | match args.command { 31 | Some(config) => match config { 32 | Command::Run(run) => run::run(run), 33 | Command::Alert(alert) => alert::alert(alert), 34 | }, 35 | _ => Err(None), 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/run.rs: -------------------------------------------------------------------------------- 1 | use crate::args::EmailOptions; 2 | use crate::args::Run; 3 | use crate::mail; 4 | use chrono::{DateTime, Utc}; 5 | use serde::Deserialize; 6 | use std::env; 7 | use std::fs::{self, File, OpenOptions}; 8 | use std::io::prelude::*; 9 | use std::path::PathBuf; 10 | use std::process::Stdio; 11 | 12 | pub fn run(run: Run) -> Result<(), Option> { 13 | let json_result = run_speedtest(run.simulate, run.email_options)?; 14 | let result = convert_json(json_result)?; 15 | write_to_result_file(&result)?; 16 | append_to_summary_file(&result)?; 17 | if run.show_results { 18 | println!("{}", &result.download); 19 | println!("{}", &result.upload); 20 | println!("{}", &result.ping); 21 | } 22 | printlnv!("Got results:\n{:?}", &result); 23 | Ok(()) 24 | } 25 | 26 | fn write_to_result_file(result: &SpeedResult) -> Result<(), String> { 27 | let cwd = env::current_dir() 28 | .map_err(|err| format!("Error when finding current working directory: {}", err))?; 29 | let data_dir = cwd.join("data"); 30 | if !data_dir.exists() { 31 | std::fs::create_dir(&data_dir) 32 | .map_err(|err| format!("Error when creating data directory: {}", err))?; 33 | } 34 | let file_name = format!("{}.json", result.date.format("%Y%m%d%H%M%S")); 35 | let file_path = data_dir.join(file_name); 36 | fs::write(file_path, result.jsonresult.as_bytes()) 37 | .map_err(|err| format!("Error when writing to file: {}", err))?; 38 | Ok(()) 39 | } 40 | 41 | fn append_to_summary_file(result: &SpeedResult) -> Result<(), String> { 42 | let cwd = env::current_dir() 43 | .map_err(|err| format!("Error when finding current working directory: {}", err))?; 44 | let data_dir = cwd.join("data"); 45 | let file_path = data_dir.join("speed.csv"); 46 | let mut file = if file_path.exists() { 47 | OpenOptions::new() 48 | .create(true) 49 | .append(true) 50 | .open(file_path) 51 | .map_err(|err| format!("Error when creating file: {}", err))? 52 | } else { 53 | let mut file = 54 | File::create(&file_path).map_err(|err| format!("Error creating file: {}", err))?; 55 | file.write_all("date,ping,speeds_download,speeds_upload,client_ip,client_isp,server_host,server_lat,server_lon,server_location,server_country,location_distance,server_ping,server_id\n".as_bytes()) 56 | .map_err(|err| format!("Error writing header to file: {}", err))?; 57 | file 58 | }; 59 | let line = format!( 60 | r#"{},{},{:.2},{:.2},"{}","{}","{}",null,null,"{}","{}",null,null,{}{}"#, 61 | result.date.format("%Y/%m/%d %H:%M:%S"), 62 | result.ping, 63 | result.download * 8.0 / 1024.0 / 1024.0, 64 | result.upload * 8.0 / 1024.0 / 1024.0, 65 | result.client_ip, 66 | result.client_isp, 67 | result.server_host, 68 | result.server_location, 69 | result.server_country, 70 | result.server_id, 71 | "\n" 72 | ); 73 | file.write(line.as_bytes()) 74 | .map_err(|err| format!("Error when writing to file: {}", err))?; 75 | Ok(()) 76 | } 77 | 78 | fn run_speedtest(simulate: bool, email_options: Option) -> Result { 79 | let (speedtestbin, args) = find_speedtest_binary_and_args(simulate)?; 80 | let child = std::process::Command::new(&speedtestbin) 81 | .args(args) 82 | .stdout(Stdio::piped()) 83 | .stderr(Stdio::piped()) 84 | .spawn() 85 | .map_err(|err| { 86 | format!( 87 | "Could not run {}.\nError:\n{}", 88 | speedtestbin.to_str().unwrap_or(""), 89 | err 90 | ) 91 | })?; 92 | let output = child 93 | .wait_with_output() 94 | .map_err(|e| format!("Could wait for speedtest execution.\nError:\n{}", e))?; 95 | if output.status.success() { 96 | Ok(String::from_utf8_lossy(&output.stdout).to_string()) 97 | } else { 98 | let stdout_text = String::from_utf8_lossy(&output.stdout); 99 | let mut error_message = if stdout_text.is_empty() { 100 | format!( 101 | "Speedtest executable exited with an error and no output. Errors:\n{}", 102 | String::from_utf8_lossy(&output.stderr) 103 | ) 104 | } else { 105 | format!( 106 | "Speedtest executable exited with an error. Output:\n{}\nErrors:\n{}", 107 | stdout_text, 108 | String::from_utf8_lossy(&output.stderr) 109 | ) 110 | }; 111 | if let Err(msg) = send_email_on_error(simulate, &error_message, email_options) { 112 | error_message += &format!("\nAlso, could not send e-mail. Error:\n{}", &msg); 113 | }; 114 | Err(error_message) 115 | } 116 | } 117 | 118 | fn find_speedtest_binary_and_args<'a>(simulate: bool) -> Result<(PathBuf, Vec<&'a str>), String> { 119 | let (bin, args) = if simulate { 120 | ( 121 | "echo", 122 | vec![ 123 | r#"{"type":"result","timestamp":"2021-01-03T12:10:00Z","ping":{"jitter":0.28499999999999998,"latency":5.7279999999999998},"download":{"bandwidth":20309419,"bytes":176063552,"elapsed":8815},"upload":{"bandwidth":13206885,"bytes":195610380,"elapsed":15015},"packetLoss":0,"isp":"Some ISP","interface":{"internalIp":"192.168.1.2","name":"eth0","macAddr":"99:99:99:99:99:99","isVpn":false,"externalIp":"84.6.0.1"},"server":{"id":99999,"name":"Some Server","location":"São Paulo","country":"Brazil","host":"someserver.nonexistentxyz.com","port":10000,"ip":"15.22.77.1"},"result":{"id":"babad438-ac4b-47db-bc28-2de7e257bd28","url":"https://www.fakespeedtest.net/result/c/babad438-ac4b-47db-bc28-2de7e257bd28"}}"#, 124 | ], 125 | ) 126 | } else { 127 | ( 128 | "speedtest", 129 | vec![ 130 | "--accept-license", 131 | "--accept-gdpr", 132 | "--format=json", 133 | "--progress=no", 134 | ], 135 | ) 136 | }; 137 | match which::which(bin) { 138 | Ok(speedtestbin) => Ok((speedtestbin, args)), 139 | Err(_) => { 140 | let cwd = env::current_dir() 141 | .map_err(|err| format!("Error when finding current working directory: {}", err))?; 142 | let speedtestbin = cwd.join(bin); 143 | if speedtestbin.exists() { 144 | Ok((speedtestbin, args)) 145 | } else { 146 | Err("Could not find speedtest binary.".to_owned()) 147 | } 148 | } 149 | } 150 | } 151 | 152 | fn convert_json(json: String) -> Result { 153 | let result: serde_json::Result = serde_json::from_str(&json); 154 | match result { 155 | Ok(raw_result) => Ok(SpeedResult { 156 | client_ip: raw_result.interface.external_ip, 157 | client_isp: raw_result.isp, 158 | date: Utc::now(), 159 | download: raw_result.download.bandwidth, 160 | upload: raw_result.upload.bandwidth, 161 | ping: raw_result.ping.latency, 162 | server_country: raw_result.server.country, 163 | server_host: raw_result.server.host, 164 | server_id: raw_result.server.id, 165 | server_location: raw_result.server.location, 166 | jsonresult: json, 167 | }), 168 | Err(err) => { 169 | let msg = format!( 170 | "Could not parse result. Json:\n{}\nError:{}", 171 | String::from_utf8_lossy(json.as_bytes()), 172 | err 173 | ); 174 | Err(msg) 175 | } 176 | } 177 | } 178 | 179 | fn send_email_on_error( 180 | simulate: bool, 181 | message_body: &str, 182 | optinal_email_options: Option, 183 | ) -> Result<(), String> { 184 | if let Some(email_options) = optinal_email_options { 185 | mail::send_mail( 186 | simulate, 187 | email_options.email, 188 | "Could not measure bandwidth", 189 | message_body, 190 | email_options.smtp, 191 | )?; 192 | } 193 | Ok(()) 194 | } 195 | 196 | #[derive(Derivative)] 197 | #[derivative(Debug)] 198 | struct SpeedResult { 199 | date: DateTime, 200 | ping: f64, 201 | download: f64, 202 | upload: f64, 203 | client_ip: String, 204 | client_isp: String, 205 | server_host: String, 206 | server_location: String, 207 | server_country: String, 208 | server_id: u32, 209 | #[derivative(Debug = "ignore")] 210 | jsonresult: String, 211 | } 212 | 213 | #[derive(Deserialize)] 214 | struct RawSpeedResult { 215 | ping: RawPing, 216 | download: RawBandwidth, 217 | upload: RawBandwidth, 218 | interface: RawInterface, 219 | isp: String, 220 | server: RawServer, 221 | } 222 | #[derive(Deserialize)] 223 | struct RawPing { 224 | latency: f64, 225 | } 226 | #[derive(Deserialize)] 227 | struct RawBandwidth { 228 | bandwidth: f64, 229 | } 230 | #[derive(Deserialize)] 231 | #[serde(rename_all = "camelCase")] 232 | struct RawInterface { 233 | external_ip: String, 234 | } 235 | #[derive(Deserialize)] 236 | struct RawServer { 237 | host: String, 238 | location: String, 239 | country: String, 240 | id: u32, 241 | } 242 | --------------------------------------------------------------------------------