├── .env.sample ├── .github └── workflows │ └── rust.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── flux-mail └── .nixpacks │ ├── Dockerfile │ ├── build.sh │ └── nixpkgs-ef56e777fedaa4da8c66a150081523c5de1e0171.nix ├── nixpacks.toml ├── src ├── database.rs ├── errors.rs ├── lib.rs ├── main.rs ├── server.rs ├── smtp.rs └── types.rs └── ui ├── .gitignore ├── README.md ├── app ├── actions │ └── actions.ts ├── layout.tsx ├── page.tsx └── search │ ├── SearchResults.tsx │ └── page.tsx ├── components ├── AnimatedBackground.tsx ├── DesignElement.tsx ├── InteractiveTitle.tsx └── ThemeToggle.tsx ├── contexts └── ThemeContext.tsx ├── env.sample ├── eslint.config.mjs ├── hooks └── parseEmail.ts ├── lib └── db.ts ├── next.config.ts ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── styles └── globals.css ├── tailwind.config.ts └── tsconfig.json /.env.sample: -------------------------------------------------------------------------------- 1 | DB_HOST=YOUR_POSTGRES_DB_HOST 2 | DB_USER=YOUR_POSTGRES_DB_USER 3 | DB_PASSWORD=YOUR_POSTGRES_DB_PASSWORD 4 | DB_NAME=YOUR_POSTGRES_DB_NAME -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .env -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | shubh622005@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "android-tzdata" 22 | version = "0.1.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 25 | 26 | [[package]] 27 | name = "android_system_properties" 28 | version = "0.1.5" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 31 | dependencies = [ 32 | "libc", 33 | ] 34 | 35 | [[package]] 36 | name = "async-trait" 37 | version = "0.1.83" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" 40 | dependencies = [ 41 | "proc-macro2", 42 | "quote", 43 | "syn", 44 | ] 45 | 46 | [[package]] 47 | name = "autocfg" 48 | version = "1.4.0" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 51 | 52 | [[package]] 53 | name = "backtrace" 54 | version = "0.3.74" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 57 | dependencies = [ 58 | "addr2line", 59 | "cfg-if", 60 | "libc", 61 | "miniz_oxide", 62 | "object", 63 | "rustc-demangle", 64 | "windows-targets", 65 | ] 66 | 67 | [[package]] 68 | name = "base64" 69 | version = "0.22.1" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 72 | 73 | [[package]] 74 | name = "bitflags" 75 | version = "2.6.0" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 78 | 79 | [[package]] 80 | name = "block-buffer" 81 | version = "0.10.4" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 84 | dependencies = [ 85 | "generic-array", 86 | ] 87 | 88 | [[package]] 89 | name = "bumpalo" 90 | version = "3.16.0" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 93 | 94 | [[package]] 95 | name = "byteorder" 96 | version = "1.5.0" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 99 | 100 | [[package]] 101 | name = "bytes" 102 | version = "1.8.0" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" 105 | 106 | [[package]] 107 | name = "cc" 108 | version = "1.2.5" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e" 111 | dependencies = [ 112 | "shlex", 113 | ] 114 | 115 | [[package]] 116 | name = "cfg-if" 117 | version = "1.0.0" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 120 | 121 | [[package]] 122 | name = "chrono" 123 | version = "0.4.39" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" 126 | dependencies = [ 127 | "android-tzdata", 128 | "iana-time-zone", 129 | "js-sys", 130 | "num-traits", 131 | "wasm-bindgen", 132 | "windows-targets", 133 | ] 134 | 135 | [[package]] 136 | name = "core-foundation-sys" 137 | version = "0.8.7" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 140 | 141 | [[package]] 142 | name = "cpufeatures" 143 | version = "0.2.16" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" 146 | dependencies = [ 147 | "libc", 148 | ] 149 | 150 | [[package]] 151 | name = "crypto-common" 152 | version = "0.1.6" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 155 | dependencies = [ 156 | "generic-array", 157 | "typenum", 158 | ] 159 | 160 | [[package]] 161 | name = "digest" 162 | version = "0.10.7" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 165 | dependencies = [ 166 | "block-buffer", 167 | "crypto-common", 168 | "subtle", 169 | ] 170 | 171 | [[package]] 172 | name = "dotenv" 173 | version = "0.15.0" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" 176 | 177 | [[package]] 178 | name = "fallible-iterator" 179 | version = "0.2.0" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" 182 | 183 | [[package]] 184 | name = "flux-mail" 185 | version = "0.2.1" 186 | dependencies = [ 187 | "chrono", 188 | "dotenv", 189 | "tokio", 190 | "tokio-postgres", 191 | "tracing", 192 | "tracing-subscriber", 193 | ] 194 | 195 | [[package]] 196 | name = "futures-channel" 197 | version = "0.3.31" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 200 | dependencies = [ 201 | "futures-core", 202 | "futures-sink", 203 | ] 204 | 205 | [[package]] 206 | name = "futures-core" 207 | version = "0.3.31" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 210 | 211 | [[package]] 212 | name = "futures-macro" 213 | version = "0.3.31" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 216 | dependencies = [ 217 | "proc-macro2", 218 | "quote", 219 | "syn", 220 | ] 221 | 222 | [[package]] 223 | name = "futures-sink" 224 | version = "0.3.31" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 227 | 228 | [[package]] 229 | name = "futures-task" 230 | version = "0.3.31" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 233 | 234 | [[package]] 235 | name = "futures-util" 236 | version = "0.3.31" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 239 | dependencies = [ 240 | "futures-core", 241 | "futures-macro", 242 | "futures-sink", 243 | "futures-task", 244 | "pin-project-lite", 245 | "pin-utils", 246 | "slab", 247 | ] 248 | 249 | [[package]] 250 | name = "generic-array" 251 | version = "0.14.7" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 254 | dependencies = [ 255 | "typenum", 256 | "version_check", 257 | ] 258 | 259 | [[package]] 260 | name = "getrandom" 261 | version = "0.2.15" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 264 | dependencies = [ 265 | "cfg-if", 266 | "libc", 267 | "wasi", 268 | ] 269 | 270 | [[package]] 271 | name = "gimli" 272 | version = "0.31.1" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 275 | 276 | [[package]] 277 | name = "hermit-abi" 278 | version = "0.3.9" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 281 | 282 | [[package]] 283 | name = "hmac" 284 | version = "0.12.1" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 287 | dependencies = [ 288 | "digest", 289 | ] 290 | 291 | [[package]] 292 | name = "iana-time-zone" 293 | version = "0.1.61" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" 296 | dependencies = [ 297 | "android_system_properties", 298 | "core-foundation-sys", 299 | "iana-time-zone-haiku", 300 | "js-sys", 301 | "wasm-bindgen", 302 | "windows-core", 303 | ] 304 | 305 | [[package]] 306 | name = "iana-time-zone-haiku" 307 | version = "0.1.2" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 310 | dependencies = [ 311 | "cc", 312 | ] 313 | 314 | [[package]] 315 | name = "js-sys" 316 | version = "0.3.76" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" 319 | dependencies = [ 320 | "once_cell", 321 | "wasm-bindgen", 322 | ] 323 | 324 | [[package]] 325 | name = "lazy_static" 326 | version = "1.5.0" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 329 | 330 | [[package]] 331 | name = "libc" 332 | version = "0.2.162" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" 335 | 336 | [[package]] 337 | name = "lock_api" 338 | version = "0.4.12" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 341 | dependencies = [ 342 | "autocfg", 343 | "scopeguard", 344 | ] 345 | 346 | [[package]] 347 | name = "log" 348 | version = "0.4.22" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 351 | 352 | [[package]] 353 | name = "md-5" 354 | version = "0.10.6" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" 357 | dependencies = [ 358 | "cfg-if", 359 | "digest", 360 | ] 361 | 362 | [[package]] 363 | name = "memchr" 364 | version = "2.7.4" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 367 | 368 | [[package]] 369 | name = "miniz_oxide" 370 | version = "0.8.0" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" 373 | dependencies = [ 374 | "adler2", 375 | ] 376 | 377 | [[package]] 378 | name = "mio" 379 | version = "1.0.2" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" 382 | dependencies = [ 383 | "hermit-abi", 384 | "libc", 385 | "wasi", 386 | "windows-sys", 387 | ] 388 | 389 | [[package]] 390 | name = "nu-ansi-term" 391 | version = "0.46.0" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 394 | dependencies = [ 395 | "overload", 396 | "winapi", 397 | ] 398 | 399 | [[package]] 400 | name = "num-traits" 401 | version = "0.2.19" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 404 | dependencies = [ 405 | "autocfg", 406 | ] 407 | 408 | [[package]] 409 | name = "object" 410 | version = "0.36.5" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" 413 | dependencies = [ 414 | "memchr", 415 | ] 416 | 417 | [[package]] 418 | name = "once_cell" 419 | version = "1.20.2" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 422 | 423 | [[package]] 424 | name = "overload" 425 | version = "0.1.1" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 428 | 429 | [[package]] 430 | name = "parking_lot" 431 | version = "0.12.3" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 434 | dependencies = [ 435 | "lock_api", 436 | "parking_lot_core", 437 | ] 438 | 439 | [[package]] 440 | name = "parking_lot_core" 441 | version = "0.9.10" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 444 | dependencies = [ 445 | "cfg-if", 446 | "libc", 447 | "redox_syscall", 448 | "smallvec", 449 | "windows-targets", 450 | ] 451 | 452 | [[package]] 453 | name = "percent-encoding" 454 | version = "2.3.1" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 457 | 458 | [[package]] 459 | name = "phf" 460 | version = "0.11.2" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" 463 | dependencies = [ 464 | "phf_shared", 465 | ] 466 | 467 | [[package]] 468 | name = "phf_shared" 469 | version = "0.11.2" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" 472 | dependencies = [ 473 | "siphasher", 474 | ] 475 | 476 | [[package]] 477 | name = "pin-project-lite" 478 | version = "0.2.15" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" 481 | 482 | [[package]] 483 | name = "pin-utils" 484 | version = "0.1.0" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 487 | 488 | [[package]] 489 | name = "postgres-protocol" 490 | version = "0.6.7" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "acda0ebdebc28befa84bee35e651e4c5f09073d668c7aed4cf7e23c3cda84b23" 493 | dependencies = [ 494 | "base64", 495 | "byteorder", 496 | "bytes", 497 | "fallible-iterator", 498 | "hmac", 499 | "md-5", 500 | "memchr", 501 | "rand", 502 | "sha2", 503 | "stringprep", 504 | ] 505 | 506 | [[package]] 507 | name = "postgres-types" 508 | version = "0.2.8" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "f66ea23a2d0e5734297357705193335e0a957696f34bed2f2faefacb2fec336f" 511 | dependencies = [ 512 | "bytes", 513 | "fallible-iterator", 514 | "postgres-protocol", 515 | ] 516 | 517 | [[package]] 518 | name = "ppv-lite86" 519 | version = "0.2.20" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 522 | dependencies = [ 523 | "zerocopy", 524 | ] 525 | 526 | [[package]] 527 | name = "proc-macro2" 528 | version = "1.0.89" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" 531 | dependencies = [ 532 | "unicode-ident", 533 | ] 534 | 535 | [[package]] 536 | name = "quote" 537 | version = "1.0.37" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 540 | dependencies = [ 541 | "proc-macro2", 542 | ] 543 | 544 | [[package]] 545 | name = "rand" 546 | version = "0.8.5" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 549 | dependencies = [ 550 | "libc", 551 | "rand_chacha", 552 | "rand_core", 553 | ] 554 | 555 | [[package]] 556 | name = "rand_chacha" 557 | version = "0.3.1" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 560 | dependencies = [ 561 | "ppv-lite86", 562 | "rand_core", 563 | ] 564 | 565 | [[package]] 566 | name = "rand_core" 567 | version = "0.6.4" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 570 | dependencies = [ 571 | "getrandom", 572 | ] 573 | 574 | [[package]] 575 | name = "redox_syscall" 576 | version = "0.5.7" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" 579 | dependencies = [ 580 | "bitflags", 581 | ] 582 | 583 | [[package]] 584 | name = "rustc-demangle" 585 | version = "0.1.24" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 588 | 589 | [[package]] 590 | name = "scopeguard" 591 | version = "1.2.0" 592 | source = "registry+https://github.com/rust-lang/crates.io-index" 593 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 594 | 595 | [[package]] 596 | name = "sha2" 597 | version = "0.10.8" 598 | source = "registry+https://github.com/rust-lang/crates.io-index" 599 | checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 600 | dependencies = [ 601 | "cfg-if", 602 | "cpufeatures", 603 | "digest", 604 | ] 605 | 606 | [[package]] 607 | name = "sharded-slab" 608 | version = "0.1.7" 609 | source = "registry+https://github.com/rust-lang/crates.io-index" 610 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 611 | dependencies = [ 612 | "lazy_static", 613 | ] 614 | 615 | [[package]] 616 | name = "shlex" 617 | version = "1.3.0" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 620 | 621 | [[package]] 622 | name = "signal-hook-registry" 623 | version = "1.4.2" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 626 | dependencies = [ 627 | "libc", 628 | ] 629 | 630 | [[package]] 631 | name = "siphasher" 632 | version = "0.3.11" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" 635 | 636 | [[package]] 637 | name = "slab" 638 | version = "0.4.9" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 641 | dependencies = [ 642 | "autocfg", 643 | ] 644 | 645 | [[package]] 646 | name = "smallvec" 647 | version = "1.13.2" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 650 | 651 | [[package]] 652 | name = "socket2" 653 | version = "0.5.7" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" 656 | dependencies = [ 657 | "libc", 658 | "windows-sys", 659 | ] 660 | 661 | [[package]] 662 | name = "stringprep" 663 | version = "0.1.5" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" 666 | dependencies = [ 667 | "unicode-bidi", 668 | "unicode-normalization", 669 | "unicode-properties", 670 | ] 671 | 672 | [[package]] 673 | name = "subtle" 674 | version = "2.6.1" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 677 | 678 | [[package]] 679 | name = "syn" 680 | version = "2.0.87" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" 683 | dependencies = [ 684 | "proc-macro2", 685 | "quote", 686 | "unicode-ident", 687 | ] 688 | 689 | [[package]] 690 | name = "thread_local" 691 | version = "1.1.8" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 694 | dependencies = [ 695 | "cfg-if", 696 | "once_cell", 697 | ] 698 | 699 | [[package]] 700 | name = "tinyvec" 701 | version = "1.8.1" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" 704 | dependencies = [ 705 | "tinyvec_macros", 706 | ] 707 | 708 | [[package]] 709 | name = "tinyvec_macros" 710 | version = "0.1.1" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 713 | 714 | [[package]] 715 | name = "tokio" 716 | version = "1.41.1" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" 719 | dependencies = [ 720 | "backtrace", 721 | "bytes", 722 | "libc", 723 | "mio", 724 | "parking_lot", 725 | "pin-project-lite", 726 | "signal-hook-registry", 727 | "socket2", 728 | "tokio-macros", 729 | "windows-sys", 730 | ] 731 | 732 | [[package]] 733 | name = "tokio-macros" 734 | version = "2.4.0" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" 737 | dependencies = [ 738 | "proc-macro2", 739 | "quote", 740 | "syn", 741 | ] 742 | 743 | [[package]] 744 | name = "tokio-postgres" 745 | version = "0.7.12" 746 | source = "registry+https://github.com/rust-lang/crates.io-index" 747 | checksum = "3b5d3742945bc7d7f210693b0c58ae542c6fd47b17adbbda0885f3dcb34a6bdb" 748 | dependencies = [ 749 | "async-trait", 750 | "byteorder", 751 | "bytes", 752 | "fallible-iterator", 753 | "futures-channel", 754 | "futures-util", 755 | "log", 756 | "parking_lot", 757 | "percent-encoding", 758 | "phf", 759 | "pin-project-lite", 760 | "postgres-protocol", 761 | "postgres-types", 762 | "rand", 763 | "socket2", 764 | "tokio", 765 | "tokio-util", 766 | "whoami", 767 | ] 768 | 769 | [[package]] 770 | name = "tokio-util" 771 | version = "0.7.13" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" 774 | dependencies = [ 775 | "bytes", 776 | "futures-core", 777 | "futures-sink", 778 | "pin-project-lite", 779 | "tokio", 780 | ] 781 | 782 | [[package]] 783 | name = "tracing" 784 | version = "0.1.40" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 787 | dependencies = [ 788 | "pin-project-lite", 789 | "tracing-attributes", 790 | "tracing-core", 791 | ] 792 | 793 | [[package]] 794 | name = "tracing-attributes" 795 | version = "0.1.27" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" 798 | dependencies = [ 799 | "proc-macro2", 800 | "quote", 801 | "syn", 802 | ] 803 | 804 | [[package]] 805 | name = "tracing-core" 806 | version = "0.1.32" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 809 | dependencies = [ 810 | "once_cell", 811 | "valuable", 812 | ] 813 | 814 | [[package]] 815 | name = "tracing-log" 816 | version = "0.2.0" 817 | source = "registry+https://github.com/rust-lang/crates.io-index" 818 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 819 | dependencies = [ 820 | "log", 821 | "once_cell", 822 | "tracing-core", 823 | ] 824 | 825 | [[package]] 826 | name = "tracing-subscriber" 827 | version = "0.3.18" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" 830 | dependencies = [ 831 | "nu-ansi-term", 832 | "sharded-slab", 833 | "smallvec", 834 | "thread_local", 835 | "tracing-core", 836 | "tracing-log", 837 | ] 838 | 839 | [[package]] 840 | name = "typenum" 841 | version = "1.17.0" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 844 | 845 | [[package]] 846 | name = "unicode-bidi" 847 | version = "0.3.18" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" 850 | 851 | [[package]] 852 | name = "unicode-ident" 853 | version = "1.0.13" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 856 | 857 | [[package]] 858 | name = "unicode-normalization" 859 | version = "0.1.24" 860 | source = "registry+https://github.com/rust-lang/crates.io-index" 861 | checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" 862 | dependencies = [ 863 | "tinyvec", 864 | ] 865 | 866 | [[package]] 867 | name = "unicode-properties" 868 | version = "0.1.3" 869 | source = "registry+https://github.com/rust-lang/crates.io-index" 870 | checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" 871 | 872 | [[package]] 873 | name = "valuable" 874 | version = "0.1.0" 875 | source = "registry+https://github.com/rust-lang/crates.io-index" 876 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 877 | 878 | [[package]] 879 | name = "version_check" 880 | version = "0.9.5" 881 | source = "registry+https://github.com/rust-lang/crates.io-index" 882 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 883 | 884 | [[package]] 885 | name = "wasi" 886 | version = "0.11.0+wasi-snapshot-preview1" 887 | source = "registry+https://github.com/rust-lang/crates.io-index" 888 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 889 | 890 | [[package]] 891 | name = "wasite" 892 | version = "0.1.0" 893 | source = "registry+https://github.com/rust-lang/crates.io-index" 894 | checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" 895 | 896 | [[package]] 897 | name = "wasm-bindgen" 898 | version = "0.2.99" 899 | source = "registry+https://github.com/rust-lang/crates.io-index" 900 | checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" 901 | dependencies = [ 902 | "cfg-if", 903 | "once_cell", 904 | "wasm-bindgen-macro", 905 | ] 906 | 907 | [[package]] 908 | name = "wasm-bindgen-backend" 909 | version = "0.2.99" 910 | source = "registry+https://github.com/rust-lang/crates.io-index" 911 | checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" 912 | dependencies = [ 913 | "bumpalo", 914 | "log", 915 | "proc-macro2", 916 | "quote", 917 | "syn", 918 | "wasm-bindgen-shared", 919 | ] 920 | 921 | [[package]] 922 | name = "wasm-bindgen-macro" 923 | version = "0.2.99" 924 | source = "registry+https://github.com/rust-lang/crates.io-index" 925 | checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" 926 | dependencies = [ 927 | "quote", 928 | "wasm-bindgen-macro-support", 929 | ] 930 | 931 | [[package]] 932 | name = "wasm-bindgen-macro-support" 933 | version = "0.2.99" 934 | source = "registry+https://github.com/rust-lang/crates.io-index" 935 | checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" 936 | dependencies = [ 937 | "proc-macro2", 938 | "quote", 939 | "syn", 940 | "wasm-bindgen-backend", 941 | "wasm-bindgen-shared", 942 | ] 943 | 944 | [[package]] 945 | name = "wasm-bindgen-shared" 946 | version = "0.2.99" 947 | source = "registry+https://github.com/rust-lang/crates.io-index" 948 | checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" 949 | 950 | [[package]] 951 | name = "web-sys" 952 | version = "0.3.76" 953 | source = "registry+https://github.com/rust-lang/crates.io-index" 954 | checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" 955 | dependencies = [ 956 | "js-sys", 957 | "wasm-bindgen", 958 | ] 959 | 960 | [[package]] 961 | name = "whoami" 962 | version = "1.5.2" 963 | source = "registry+https://github.com/rust-lang/crates.io-index" 964 | checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" 965 | dependencies = [ 966 | "redox_syscall", 967 | "wasite", 968 | "web-sys", 969 | ] 970 | 971 | [[package]] 972 | name = "winapi" 973 | version = "0.3.9" 974 | source = "registry+https://github.com/rust-lang/crates.io-index" 975 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 976 | dependencies = [ 977 | "winapi-i686-pc-windows-gnu", 978 | "winapi-x86_64-pc-windows-gnu", 979 | ] 980 | 981 | [[package]] 982 | name = "winapi-i686-pc-windows-gnu" 983 | version = "0.4.0" 984 | source = "registry+https://github.com/rust-lang/crates.io-index" 985 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 986 | 987 | [[package]] 988 | name = "winapi-x86_64-pc-windows-gnu" 989 | version = "0.4.0" 990 | source = "registry+https://github.com/rust-lang/crates.io-index" 991 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 992 | 993 | [[package]] 994 | name = "windows-core" 995 | version = "0.52.0" 996 | source = "registry+https://github.com/rust-lang/crates.io-index" 997 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 998 | dependencies = [ 999 | "windows-targets", 1000 | ] 1001 | 1002 | [[package]] 1003 | name = "windows-sys" 1004 | version = "0.52.0" 1005 | source = "registry+https://github.com/rust-lang/crates.io-index" 1006 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1007 | dependencies = [ 1008 | "windows-targets", 1009 | ] 1010 | 1011 | [[package]] 1012 | name = "windows-targets" 1013 | version = "0.52.6" 1014 | source = "registry+https://github.com/rust-lang/crates.io-index" 1015 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1016 | dependencies = [ 1017 | "windows_aarch64_gnullvm", 1018 | "windows_aarch64_msvc", 1019 | "windows_i686_gnu", 1020 | "windows_i686_gnullvm", 1021 | "windows_i686_msvc", 1022 | "windows_x86_64_gnu", 1023 | "windows_x86_64_gnullvm", 1024 | "windows_x86_64_msvc", 1025 | ] 1026 | 1027 | [[package]] 1028 | name = "windows_aarch64_gnullvm" 1029 | version = "0.52.6" 1030 | source = "registry+https://github.com/rust-lang/crates.io-index" 1031 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1032 | 1033 | [[package]] 1034 | name = "windows_aarch64_msvc" 1035 | version = "0.52.6" 1036 | source = "registry+https://github.com/rust-lang/crates.io-index" 1037 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1038 | 1039 | [[package]] 1040 | name = "windows_i686_gnu" 1041 | version = "0.52.6" 1042 | source = "registry+https://github.com/rust-lang/crates.io-index" 1043 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1044 | 1045 | [[package]] 1046 | name = "windows_i686_gnullvm" 1047 | version = "0.52.6" 1048 | source = "registry+https://github.com/rust-lang/crates.io-index" 1049 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1050 | 1051 | [[package]] 1052 | name = "windows_i686_msvc" 1053 | version = "0.52.6" 1054 | source = "registry+https://github.com/rust-lang/crates.io-index" 1055 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1056 | 1057 | [[package]] 1058 | name = "windows_x86_64_gnu" 1059 | version = "0.52.6" 1060 | source = "registry+https://github.com/rust-lang/crates.io-index" 1061 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1062 | 1063 | [[package]] 1064 | name = "windows_x86_64_gnullvm" 1065 | version = "0.52.6" 1066 | source = "registry+https://github.com/rust-lang/crates.io-index" 1067 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1068 | 1069 | [[package]] 1070 | name = "windows_x86_64_msvc" 1071 | version = "0.52.6" 1072 | source = "registry+https://github.com/rust-lang/crates.io-index" 1073 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1074 | 1075 | [[package]] 1076 | name = "zerocopy" 1077 | version = "0.7.35" 1078 | source = "registry+https://github.com/rust-lang/crates.io-index" 1079 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 1080 | dependencies = [ 1081 | "byteorder", 1082 | "zerocopy-derive", 1083 | ] 1084 | 1085 | [[package]] 1086 | name = "zerocopy-derive" 1087 | version = "0.7.35" 1088 | source = "registry+https://github.com/rust-lang/crates.io-index" 1089 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 1090 | dependencies = [ 1091 | "proc-macro2", 1092 | "quote", 1093 | "syn", 1094 | ] 1095 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flux-mail" 3 | description = "A simple implementation of SMTP Protocol as a temporary mail service in Rust" 4 | version = "0.2.1" 5 | edition = "2021" 6 | author = "Shubham singh " 7 | repository = "https://github.com/shubhexists/flux-mail" 8 | license-file = "LICENSE" 9 | categories = ["email"] 10 | keywords = ["smtp", "email", "mail"] 11 | readme = "README.md" 12 | 13 | [dependencies] 14 | tokio = { version = "1.41.1", features = ["full"] } 15 | tracing = "0.1.40" 16 | tracing-subscriber = "0.3.18" 17 | tokio-postgres = "0.7" 18 | chrono = "0.4.39" 19 | dotenv = "0.15" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Shubham 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FLUX-MAIL 🦀 2 | 3 | A self-hosted Temporary Email Service written in Rust. Create disposable email addresses instantly for your temporary needs. 4 | 5 |
6 | 7 | [![GitHub stars](https://img.shields.io/github/stars/shubhexists/flux-mail?style=social)](https://github.com/shubhexists/flux-mail/stargazers) 8 | [![Crates.io](https://img.shields.io/crates/v/flux-mail)](https://crates.io/crates/flux-mail) 9 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 10 | 11 | [View Demo](https://flux-mail.shubh.sh) • [Report Bug](https://github.com/shubhexists/flux-mail/issues) • [Request Feature](https://github.com/shubhexists/flux-mail/issues) 12 | 13 |
14 | 15 | ## 🌟 Features 16 | 17 | - **Instant Setup**: Create temporary email addresses in seconds 18 | - **No Registration**: Zero signup required 19 | - **Self-Hostable**: Run your own instance easily 20 | - **Rust-Powered**: Built with performance and reliability in mind 21 | - **Simple Interface**: Clean and intuitive user experience 22 | 23 | ## 📧 SMTP Server Details 24 | 25 | - **Server Address:** `flux.shubh.sh` 26 | - **Email Format:** `your-username@flux.shubh.sh` 27 | - All emails sent to `{username}@flux.shubh.sh` will be automatically handled 28 | 29 | ## 🚀 Quick Start 30 | 31 | 1. Visit [flux-mail.shubh.sh](https://flux-mail.shubh.sh) 32 | 2. Choose your username 33 | > ⚠️ **Security Note:** Your username is public. Do not use it for confidential communications. 34 | 3. Start using your temporary email: `{username}@flux.shubh.sh` 35 | 36 | ## 📸 Screenshot 37 | 38 | ![Flux Mail Interface](https://github.com/user-attachments/assets/d4a63fac-c3d1-4e33-a072-4e8003389e23) 39 | 40 | ## 💻 Installation 41 | 42 | ### Using as a Library 43 | 44 | Add Flux Mail to your Rust project: 45 | 46 | ```bash 47 | cargo add flux-mail 48 | ``` 49 | 50 | 2. Check [main.rs](https://github.com/shubhexists/flux-mail/blob/master/src/main.rs) for implementation details 51 | 52 | ## ⚠️ Limitations 53 | 54 | - Attachments are not displayed in the hosted version. 55 | - Email will be removed after 7 days from the database. 56 | 57 | ## 🤝 Contributing 58 | 59 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. 60 | 61 | 1. Fork the Project 62 | 2. Create your Feature Branch (`git checkout -b feature/feature_name`) 63 | 3. Commit your Changes (`git commit -m 'feature_name'`) 64 | 4. Push to the Branch (`git push origin feature/feature_name`) 65 | 5. Open a Pull Request 66 | 67 | ## 📜 License 68 | 69 | Distributed under the MIT License. See `LICENSE` for more information. 70 | 71 | ## 🌟 Show your support 72 | 73 | Give a ⭐️ if this project helped you! 74 | 75 | ## 📞 Contact 76 | 77 | Shubham - [@shubhexists](https://github.com/shubhexists) 78 | 79 | Project Link: [https://github.com/shubhexists/flux-mail](https://github.com/shubhexists/flux-mail) 80 | 81 | --- 82 | 83 |
84 | Made with ❤️ using Rust 85 |
86 | -------------------------------------------------------------------------------- /flux-mail/.nixpacks/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/railwayapp/nixpacks:ubuntu-1741046653 2 | 3 | ENTRYPOINT ["/bin/bash", "-l", "-c"] 4 | WORKDIR /app/ 5 | 6 | 7 | COPY .nixpacks/nixpkgs-ef56e777fedaa4da8c66a150081523c5de1e0171.nix .nixpacks/nixpkgs-ef56e777fedaa4da8c66a150081523c5de1e0171.nix 8 | RUN nix-env -if .nixpacks/nixpkgs-ef56e777fedaa4da8c66a150081523c5de1e0171.nix && nix-collect-garbage -d 9 | 10 | 11 | ARG CARGO_PROFILE NIXPACKS_METADATA ROCKET_ADDRESS 12 | ENV CARGO_PROFILE=$CARGO_PROFILE NIXPACKS_METADATA=$NIXPACKS_METADATA ROCKET_ADDRESS=$ROCKET_ADDRESS 13 | 14 | # setup phase 15 | # noop 16 | 17 | # start phase 18 | COPY . /app/. 19 | RUN sudo ./target/release/flux-mail 20 | 21 | # build phase 22 | COPY . /app/. 23 | RUN --mount=type=cache,id=pMsXS6LD90-/root/cargo/git,target=/root/.cargo/git --mount=type=cache,id=pMsXS6LD90-/root/cargo/registry,target=/root/.cargo/registry --mount=type=cache,id=pMsXS6LD90-target,target=/app/target cargo build --release 24 | 25 | 26 | 27 | 28 | 29 | # start 30 | FROM ubuntu:jammy 31 | ENTRYPOINT ["/bin/bash", "-l", "-c"] 32 | WORKDIR /app/ 33 | COPY --from=0 /etc/ssl/certs /etc/ssl/certs 34 | RUN true 35 | COPY --from=0 /app/bin/flux-mail /app/bin/flux-mail 36 | 37 | CMD ["./bin/flux-mail"] 38 | 39 | -------------------------------------------------------------------------------- /flux-mail/.nixpacks/build.sh: -------------------------------------------------------------------------------- 1 | docker build flux-mail -f flux-mail/.nixpacks/Dockerfile -t 1751ff20-3496-4819-a1f1-9a4a75be2936 --build-arg CARGO_PROFILE=release --build-arg NIXPACKS_METADATA=rust --build-arg ROCKET_ADDRESS=0.0.0.0 -------------------------------------------------------------------------------- /flux-mail/.nixpacks/nixpkgs-ef56e777fedaa4da8c66a150081523c5de1e0171.nix: -------------------------------------------------------------------------------- 1 | { }: 2 | 3 | let pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/ef56e777fedaa4da8c66a150081523c5de1e0171.tar.gz") { overlays = [ (import (builtins.fetchTarball "https://github.com/oxalica/rust-overlay/archive/master.tar.gz")) ]; }; 4 | in with pkgs; 5 | let 6 | APPEND_LIBRARY_PATH = "${lib.makeLibraryPath [ ] }"; 7 | myLibraries = writeText "libraries" '' 8 | export LD_LIBRARY_PATH="${APPEND_LIBRARY_PATH}:$LD_LIBRARY_PATH" 9 | 10 | ''; 11 | in 12 | buildEnv { 13 | name = "ef56e777fedaa4da8c66a150081523c5de1e0171-env"; 14 | paths = [ 15 | (runCommand "ef56e777fedaa4da8c66a150081523c5de1e0171-env" { } '' 16 | mkdir -p $out/etc/profile.d 17 | cp ${myLibraries} $out/etc/profile.d/ef56e777fedaa4da8c66a150081523c5de1e0171-env.sh 18 | '') 19 | curl gcc git openssl pkg-config 20 | ]; 21 | } 22 | -------------------------------------------------------------------------------- /nixpacks.toml: -------------------------------------------------------------------------------- 1 | EXPOSE="25" -------------------------------------------------------------------------------- /src/database.rs: -------------------------------------------------------------------------------- 1 | use chrono::DateTime; 2 | use std::{env, error::Error}; 3 | use tokio_postgres::{Client, NoTls}; 4 | use tracing::{error, info}; 5 | 6 | use crate::types::Email; 7 | 8 | pub struct DatabaseClient { 9 | pub db: Client, 10 | } 11 | 12 | impl DatabaseClient { 13 | pub async fn connect() -> Result> { 14 | let host = env::var("DB_HOST").expect("DB_HOST not set"); 15 | let user = env::var("DB_USER").expect("DB_USER not set"); 16 | let password = env::var("DB_PASSWORD").expect("DB_PASSWORD not set"); 17 | let dbname = env::var("DB_NAME").expect("DB_NAME not set"); 18 | 19 | let connection_string: String = format!( 20 | "host={} user={} password={} dbname={}", 21 | host, user, password, dbname 22 | ); 23 | 24 | let (client, connection) = match tokio_postgres::connect(&connection_string, NoTls).await { 25 | Ok((client, connection)) => (client, connection), 26 | Err(e) => { 27 | error!("Failed to connect to the database: {}", e); 28 | return Err(Box::new(e)); 29 | } 30 | }; 31 | 32 | tokio::spawn(async move { 33 | if let Err(e) = connection.await { 34 | error!("Connection error: {}", e); 35 | } 36 | }); 37 | 38 | let sql: &str = " 39 | CREATE TABLE IF NOT EXISTS mail ( 40 | date TEXT, 41 | sender TEXT, 42 | recipients TEXT, 43 | data TEXT 44 | ); 45 | CREATE INDEX IF NOT EXISTS mail_date ON mail(date); 46 | CREATE INDEX IF NOT EXISTS mail_recipients ON mail(recipients); 47 | CREATE INDEX IF NOT EXISTS mail_date_recipients ON mail(date, recipients); 48 | "; 49 | 50 | if let Err(e) = client.batch_execute(sql).await { 51 | error!("Failed to execute initialization queries: {}", e); 52 | return Err(Box::new(e)); 53 | } 54 | 55 | info!("Database initialized successfully"); 56 | Ok(DatabaseClient { db: client }) 57 | } 58 | 59 | pub async fn add_mail(&self, data: Email) -> Result> { 60 | let sql: &str = "INSERT INTO mail (date, sender, recipients, data) VALUES ($1, $2, $3, $4)"; 61 | let date: String = chrono::Utc::now() 62 | .format("%Y-%m-%d %H:%M:%S%.3f") 63 | .to_string(); 64 | 65 | match self 66 | .db 67 | .execute( 68 | sql, 69 | &[&date, &data.sender, &data.recipients[0], &data.content], 70 | ) 71 | .await 72 | { 73 | Ok(rows_affected) => Ok(rows_affected), 74 | Err(e) => { 75 | error!("Failed to add mail to the database: {}", e); 76 | Err(Box::new(e)) 77 | } 78 | } 79 | } 80 | 81 | pub async fn delete_old_mail(&self) -> Result> { 82 | let now: DateTime = chrono::offset::Utc::now(); 83 | let a_week_ago: DateTime = now - chrono::Duration::days(7); 84 | let a_week_ago: String = a_week_ago.format("%Y-%m-%d %H:%M:%S%.3f").to_string(); 85 | 86 | info!("Deleting old mail from before {a_week_ago}"); 87 | match self 88 | .db 89 | .execute("DELETE FROM mail WHERE date < $1", &[&a_week_ago]) 90 | .await 91 | { 92 | Ok(rows) => Ok(rows), 93 | Err(e) => { 94 | error!("Failed to delete old mail: {}", e); 95 | Err(Box::new(e)) 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub struct SmtpResponseError<'a> { 3 | pub code: &'a SmtpErrorCode, 4 | message: &'a str, 5 | } 6 | 7 | impl<'a> SmtpResponseError<'a> { 8 | pub fn new(code: &'a SmtpErrorCode) -> Self { 9 | Self { 10 | code, 11 | message: code.as_message(), 12 | } 13 | } 14 | 15 | pub fn format_response(&self) -> String { 16 | format!("{:?} {}\n", self.code, self.message) 17 | } 18 | } 19 | 20 | #[derive(Debug)] 21 | pub enum SmtpErrorCode { 22 | SyntaxError, 23 | CommandUnrecognized, 24 | InvalidParameters, 25 | MailboxUnavailable, 26 | InsufficientSystemStorage, 27 | MessageSizeExceedsLimit, 28 | TransactionFailed, 29 | } 30 | 31 | impl SmtpErrorCode { 32 | pub fn as_code(&self) -> u16 { 33 | match self { 34 | SmtpErrorCode::SyntaxError => 500, 35 | SmtpErrorCode::CommandUnrecognized => 500, 36 | SmtpErrorCode::InvalidParameters => 501, 37 | SmtpErrorCode::MailboxUnavailable => 550, 38 | SmtpErrorCode::InsufficientSystemStorage => 452, 39 | SmtpErrorCode::MessageSizeExceedsLimit => 552, 40 | SmtpErrorCode::TransactionFailed => 554, 41 | } 42 | } 43 | 44 | fn as_message(&self) -> &str { 45 | match self { 46 | SmtpErrorCode::SyntaxError => "Syntax error, command unrecognized", 47 | SmtpErrorCode::CommandUnrecognized => "Command unrecognized", 48 | SmtpErrorCode::InvalidParameters => "Syntax error in parameters or arguments", 49 | SmtpErrorCode::MailboxUnavailable => "Requested action not taken (mailbox unavailable)", 50 | SmtpErrorCode::InsufficientSystemStorage => { 51 | "Requested action not taken (insufficient system storage)" 52 | } 53 | SmtpErrorCode::MessageSizeExceedsLimit => { 54 | "Requested action aborted (message size exceeds limit)" 55 | } 56 | SmtpErrorCode::TransactionFailed => "Transaction failed", 57 | } 58 | } 59 | } 60 | 61 | impl Into for SmtpErrorCode { 62 | fn into(self) -> u16 { 63 | self.as_code() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod database; 2 | mod errors; 3 | pub mod server; 4 | mod smtp; 5 | mod types; 6 | use database::DatabaseClient; 7 | use server::Server; 8 | use std::sync::Arc; 9 | use std::time::Duration; 10 | use std::{error::Error, net::SocketAddr}; 11 | use tokio::net::{TcpListener, TcpStream}; 12 | use tokio::time::timeout; 13 | 14 | const MAX_EMAIL_SIZE: usize = 10_485_760; 15 | const TIMEOUT: Duration = Duration::from_secs(30); 16 | const MAX_RECIPIENT_COUNT: usize = 100; 17 | const INITIAL_GREETING: &'static [u8] = b"220 Flux Mail Service Ready\n"; 18 | const SUCCESS_RESPONSE: &'static [u8] = b"250 Ok\n"; 19 | const DATA_READY_PROMPT: &'static [u8] = b"354 End data with .\n"; 20 | const CLOSING_CONNECTION: &'static [u8] = b"221 Goodbye\n"; 21 | const AUTH_OK: &'static [u8] = b"235 Ok\n"; 22 | 23 | pub(crate) fn is_valid_email(email: &str) -> bool { 24 | email.contains('@') && !email.contains("..") && email.len() < 254 25 | } 26 | 27 | #[tokio::main] 28 | pub async fn start_server(addr: SocketAddr, domain: String) -> Result<(), Box> { 29 | let listener: TcpListener = TcpListener::bind(&addr).await?; 30 | let domain: Arc = Arc::new(domain); 31 | // let db: Arc = Arc::new(DatabaseClient::connect().await?); 32 | tracing::info!("Server Started On Port: {}", addr); 33 | 34 | loop { 35 | let (stream, _addr): (TcpStream, SocketAddr) = listener.accept().await?; 36 | let domain: Arc = Arc::clone(&domain); 37 | // let db: Arc = Arc::clone(&db); 38 | 39 | tokio::task::LocalSet::new() 40 | .run_until(async move { 41 | tracing::info!("Ping received on SMTP Server"); 42 | let smtp: Server = Server::new(domain.as_str(), stream).await?; 43 | match timeout(Duration::from_secs(300), smtp.connection()).await { 44 | Ok(Ok(_)) => Ok(()), 45 | Ok(Err(e)) => Err(e), 46 | Err(e) => Err(Box::new(e) as Box), 47 | } 48 | }) 49 | .await 50 | .ok(); 51 | } 52 | } 53 | 54 | pub fn clear_old_mails(period: tokio::time::Duration) { 55 | std::thread::spawn(move || -> Result<(), Box> { 56 | let runtime: tokio::runtime::Runtime = tokio::runtime::Builder::new_current_thread() 57 | .enable_time() 58 | .enable_io() 59 | .build() 60 | .map_err(|e: std::io::Error| format!("Failed to build async runtime: {}", e))?; 61 | 62 | runtime.block_on(async move { 63 | let local: tokio::task::LocalSet = tokio::task::LocalSet::new(); 64 | local.spawn_local(async move { 65 | let db: DatabaseClient = match DatabaseClient::connect().await { 66 | Ok(db) => db, 67 | Err(e) => { 68 | tracing::error!("Failed to connect to database: {}", e); 69 | return; 70 | } 71 | }; 72 | let mut interval: tokio::time::Interval = tokio::time::interval(period); 73 | interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); 74 | loop { 75 | interval.tick().await; 76 | if let Err(e) = db.delete_old_mail().await { 77 | tracing::error!("Failed to delete old mail: {}", e); 78 | } 79 | } 80 | }); 81 | local.await; 82 | }); 83 | Ok(()) 84 | }); 85 | } 86 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use dotenv::dotenv; 2 | use flux_mail::{clear_old_mails, start_server}; 3 | use tokio::time; 4 | use tracing::{error, info}; 5 | 6 | fn main() { 7 | tracing_subscriber::fmt::init(); 8 | if dotenv().is_err() { 9 | error!("Warning: Failed to load .env file. Default environment variables may be missing."); 10 | } else { 11 | info!("Info: .env file successfully loaded."); 12 | } 13 | 14 | let addr: std::net::SocketAddr = "0.0.0.0:25".parse().unwrap(); 15 | let domain: String = String::from("mail.flux.shubh.sh"); 16 | 17 | clear_old_mails(time::Duration::from_secs(3600)); 18 | if let Err(e) = start_server(addr, domain) { 19 | tracing::error!("Error starting server: {}", e); 20 | eprintln!("Error starting server: {}", e); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/server.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | database::DatabaseClient, errors::SmtpErrorCode, smtp::HandleCurrentState, CLOSING_CONNECTION, 3 | INITIAL_GREETING, TIMEOUT, 4 | }; 5 | use std::{error::Error, sync::Arc}; 6 | use tokio::{ 7 | io::{AsyncReadExt, AsyncWriteExt}, 8 | time::timeout, 9 | }; 10 | use tracing::{span::Entered, Level, Span}; 11 | 12 | pub struct Server { 13 | connection: tokio::net::TcpStream, 14 | state_handler: HandleCurrentState, 15 | } 16 | 17 | impl Server { 18 | pub async fn new( 19 | server_domain: impl AsRef, 20 | connection: tokio::net::TcpStream, 21 | ) -> Result> { 22 | Ok(Self { 23 | connection, 24 | state_handler: HandleCurrentState::new(server_domain), 25 | }) 26 | } 27 | 28 | pub async fn connection(mut self) -> Result<(), Box> { 29 | let span: Span = tracing::span!(Level::INFO, "MAIL"); 30 | let _enter: Entered<'_> = span.enter(); 31 | self.connection.write_all(INITIAL_GREETING).await?; 32 | tracing::info!("Greeted"); 33 | let mut buffer: Vec = vec![0; 65536]; 34 | let db = Arc::new(DatabaseClient::connect().await?); 35 | loop { 36 | match timeout(TIMEOUT, self.connection.read(&mut buffer)).await { 37 | Ok(Ok(0)) => { 38 | tracing::error!("Unexpected End of Stream without any data."); 39 | break; 40 | } 41 | Ok(Ok(bytes)) => { 42 | let message: &str = match std::str::from_utf8(&buffer[0..bytes]) { 43 | Ok(a) => a, 44 | Err(e) => { 45 | tracing::error!("Broken pipe, closing stream: {}", e); 46 | return Err(Box::new(e)); 47 | } 48 | }; 49 | match self.state_handler.process_smtp_command(message, &db).await { 50 | Ok(response) => { 51 | if response != b"" { 52 | self.connection.write_all(response).await?; 53 | } 54 | if response == CLOSING_CONNECTION { 55 | tracing::warn!("Closing connection!"); 56 | break; 57 | } 58 | } 59 | Err(e) => { 60 | self.connection 61 | .write_all(e.format_response().as_bytes()) 62 | .await?; 63 | tracing::error!("Unexpected End of Stream, closing connection"); 64 | if e.code.as_code() >= SmtpErrorCode::SyntaxError.into() { 65 | break; 66 | } 67 | } 68 | }; 69 | } 70 | Ok(Err(_)) => { 71 | tracing::error!("Broken pipe, couldn't read stream"); 72 | break; 73 | } 74 | Err(_) => { 75 | tracing::error!("Timeout Error: No data for 30 seconds. Closing!"); 76 | break; 77 | } 78 | } 79 | } 80 | Ok(()) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/smtp.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | database::DatabaseClient, 3 | errors::{SmtpErrorCode, SmtpResponseError}, 4 | is_valid_email, 5 | types::{CurrentStates, Email, SMTPResult}, 6 | AUTH_OK, CLOSING_CONNECTION, DATA_READY_PROMPT, MAX_EMAIL_SIZE, MAX_RECIPIENT_COUNT, 7 | SUCCESS_RESPONSE, 8 | }; 9 | use std::{str::SplitWhitespace, sync::Arc}; 10 | use tracing::{error, info}; 11 | 12 | pub struct HandleCurrentState { 13 | current_state: CurrentStates, 14 | greeting_message: String, 15 | max_email_size: usize, 16 | } 17 | 18 | impl HandleCurrentState { 19 | pub fn new(server_domain: impl AsRef) -> Self { 20 | let server_domain: &str = server_domain.as_ref(); 21 | let greeting_message: String = format!( 22 | "250-{server_domain} greets {server_domain}\n\ 23 | 250-SIZE {}\n\ 24 | 250 8BITMIME\n", 25 | MAX_EMAIL_SIZE 26 | ); 27 | 28 | tracing::info!("Greeting message: {}", greeting_message); 29 | 30 | Self { 31 | current_state: CurrentStates::Initial, 32 | max_email_size: MAX_EMAIL_SIZE, 33 | greeting_message, 34 | } 35 | } 36 | 37 | pub async fn process_smtp_command<'a>( 38 | &mut self, 39 | client_message: &str, 40 | db: &Arc, 41 | ) -> SMTPResult<'a, &[u8]> { 42 | let message: &str = client_message.trim(); 43 | if message.is_empty() { 44 | return Err(SmtpResponseError::new(&SmtpErrorCode::SyntaxError)); 45 | } 46 | let mut message_parts: SplitWhitespace<'_> = message.split_whitespace(); 47 | let command: String = message_parts 48 | .next() 49 | .ok_or_else(|| SmtpResponseError::new(&SmtpErrorCode::SyntaxError))? 50 | .to_lowercase(); 51 | 52 | let previous_state: CurrentStates = 53 | std::mem::replace(&mut self.current_state, CurrentStates::Initial); 54 | match (command.as_str(), previous_state) { 55 | ("ehlo", CurrentStates::Initial) => { 56 | self.current_state = CurrentStates::Greeted; 57 | tracing::trace!("RECIEVED: ehlo"); 58 | Ok(self.greeting_message.as_bytes()) 59 | } 60 | ("helo", CurrentStates::Initial) => { 61 | tracing::trace!("RECIEVED: helo"); 62 | self.current_state = CurrentStates::Greeted; 63 | Ok(SUCCESS_RESPONSE) 64 | } 65 | ("noop", _) | ("help", _) | ("info", _) | ("vrfy", _) | ("expn", _) => { 66 | tracing::warn!("RECIEVED: Unhandled Command"); 67 | Ok(SUCCESS_RESPONSE) 68 | } 69 | ("rset", _) => { 70 | tracing::warn!("RECIEVED: RESET"); 71 | self.current_state = CurrentStates::Initial; 72 | Ok(SUCCESS_RESPONSE) 73 | } 74 | ("auth", _) => { 75 | tracing::trace!("RECIEVED: auth"); 76 | Ok(AUTH_OK) 77 | } 78 | ("mail", CurrentStates::Greeted) => { 79 | let sender: &str = message_parts 80 | .next() 81 | .and_then(|s: &str| s.strip_prefix("FROM:")) 82 | .ok_or_else(|| SmtpResponseError::new(&SmtpErrorCode::InvalidParameters))?; 83 | 84 | if !is_valid_email(sender) { 85 | tracing::error!("ERROR: Invalid email: {}", sender); 86 | return Err(SmtpResponseError::new(&SmtpErrorCode::MailboxUnavailable)); 87 | } 88 | 89 | tracing::trace!("RECIEVED: MAIL FROM: {}", sender); 90 | self.current_state = CurrentStates::AwaitingRecipient(Email { 91 | sender: sender.to_string(), 92 | ..Default::default() 93 | }); 94 | 95 | Ok(SUCCESS_RESPONSE) 96 | } 97 | ("rcpt", CurrentStates::AwaitingRecipient(mut email)) => { 98 | if email.recipients.len() >= MAX_RECIPIENT_COUNT { 99 | tracing::error!( 100 | "ERROR: Max number of recipients reached, got: {}", 101 | email.recipients.len() 102 | ); 103 | return Err(SmtpResponseError::new( 104 | &SmtpErrorCode::InsufficientSystemStorage, 105 | )); 106 | } 107 | let receiver: &str = message_parts 108 | .next() 109 | .and_then(|s: &str| s.strip_prefix("TO:")) 110 | .ok_or_else(|| SmtpResponseError::new(&SmtpErrorCode::InvalidParameters))?; 111 | 112 | if !is_valid_email(receiver) { 113 | tracing::error!("ERROR: Invalid email: {}", receiver); 114 | return Err(SmtpResponseError::new(&SmtpErrorCode::MailboxUnavailable)); 115 | } 116 | 117 | email.recipients.push(receiver.to_string()); 118 | tracing::trace!("RECIEVED: RCPT TO: {}", receiver); 119 | self.current_state = CurrentStates::AwaitingRecipient(email); 120 | Ok(SUCCESS_RESPONSE) 121 | } 122 | ("data", CurrentStates::AwaitingRecipient(email)) => { 123 | if email.recipients.is_empty() { 124 | tracing::error!("ERROR: Recieved DATA with no recipients"); 125 | return Err(SmtpResponseError::new(&SmtpErrorCode::TransactionFailed)); 126 | } 127 | self.current_state = CurrentStates::AwaitingData(email); 128 | Ok(DATA_READY_PROMPT) 129 | } 130 | ("quit", state) => match state { 131 | CurrentStates::Initial | CurrentStates::Greeted => { 132 | tracing::warn!("Unexpected QUIT, Closing !!"); 133 | Ok(CLOSING_CONNECTION) 134 | } 135 | CurrentStates::AwaitingRecipient(email) => { 136 | tracing::warn!("Unexpected QUIT, Closing !!"); 137 | match db.add_mail(email.clone()).await.is_err() { 138 | true => error!("Unable to add mail in database"), 139 | false => info!("Mail successfully added to database"), 140 | }; 141 | Ok(CLOSING_CONNECTION) 142 | } 143 | CurrentStates::AwaitingData(email) => { 144 | tracing::trace!("RECIEVED: Closing Data Stream"); 145 | match db.add_mail(email.clone()).await.is_err() { 146 | true => error!("Unable to add mail in database"), 147 | false => info!("Mail successfully added to database"), 148 | }; 149 | self.current_state = CurrentStates::DataReceived(email); 150 | Ok(CLOSING_CONNECTION) 151 | } 152 | CurrentStates::DataReceived(email) => { 153 | tracing::warn!("Unexpected QUIT, Closing !!"); 154 | match db.add_mail(email.clone()).await.is_err() { 155 | true => error!("Unable to add mail in database"), 156 | false => info!("Mail successfully added to database"), 157 | }; 158 | Ok(CLOSING_CONNECTION) 159 | } 160 | }, 161 | (_, CurrentStates::AwaitingData(mut email)) => { 162 | email.size += client_message.len(); 163 | if email.size > self.max_email_size { 164 | tracing::error!("ERROR: Message size of 10MB exceeded. Closing!"); 165 | return Err(SmtpResponseError::new( 166 | &SmtpErrorCode::MessageSizeExceedsLimit, 167 | )); 168 | } 169 | email.content.push_str(client_message); 170 | let response: &[u8] = if email.content.ends_with("\n.\n") 171 | || email.content.ends_with("\r\n.\r\n") 172 | { 173 | self.current_state = CurrentStates::DataReceived(std::mem::take(&mut email)); 174 | SUCCESS_RESPONSE 175 | } else { 176 | self.current_state = CurrentStates::AwaitingData(std::mem::take(&mut email)); 177 | b"" 178 | }; 179 | Ok(response) 180 | } 181 | _ => { 182 | tracing::error!("ERROR: Unrecognized Command"); 183 | Err(SmtpResponseError::new(&SmtpErrorCode::CommandUnrecognized)) 184 | } 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::SmtpResponseError; 2 | 3 | #[derive(Default, Clone)] 4 | pub struct Email { 5 | #[allow(dead_code)] 6 | pub sender: String, 7 | pub recipients: Vec, 8 | pub content: String, 9 | pub size: usize, 10 | } 11 | 12 | pub enum CurrentStates { 13 | Initial, 14 | Greeted, 15 | AwaitingRecipient(Email), 16 | AwaitingData(Email), 17 | #[allow(dead_code)] 18 | DataReceived(Email), 19 | } 20 | 21 | pub type SMTPResult<'a, T> = Result>; 22 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /ui/app/actions/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { parseEmailContent } from "@/hooks/parseEmail"; 4 | import { pool } from "@/lib/db"; 5 | 6 | export async function searchEmails(recipientQuery: string) { 7 | try { 8 | const result = await pool.query( 9 | `SELECT date, sender, recipients, data 10 | FROM mail 11 | WHERE recipients = $1`, 12 | [`<${recipientQuery}>`] 13 | ); 14 | const output = []; 15 | for (const i of result.rows) { 16 | const parsedMail = await parseEmailContent(i.data); 17 | output.push({ 18 | sender: i.sender, 19 | date: i.date, 20 | recipients: i.recipients, 21 | data: parsedMail, 22 | }); 23 | } 24 | return output; 25 | } catch (error) { 26 | console.error("Database error:", error); 27 | throw new Error("Failed to search emails"); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ui/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeProvider } from "@/contexts/ThemeContext"; 2 | import "@/styles/globals.css"; 3 | import { SpeedInsights } from "@vercel/speed-insights/next"; 4 | import { Analytics } from "@vercel/analytics/react"; 5 | 6 | export default function RootLayout({ 7 | children, 8 | }: { 9 | children: React.ReactNode; 10 | }) { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 |
{children}
18 |
19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /ui/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import { useRouter } from "next/navigation"; 5 | import ThemeToggle from "@/components/ThemeToggle"; 6 | import AnimatedBackground from "@/components/AnimatedBackground"; 7 | import InteractiveTitle from "@/components/InteractiveTitle"; 8 | 9 | export default function Home() { 10 | const [searchTerm, setSearchTerm] = useState(""); 11 | const router = useRouter(); 12 | 13 | const handleSearch = () => { 14 | if (searchTerm.trim()) { 15 | router.push(`/search?q=${encodeURIComponent(searchTerm.toLowerCase())}`); 16 | } 17 | }; 18 | 19 | const handleKeyPress = (e: React.KeyboardEvent) => { 20 | if (e.key === "Enter") { 21 | handleSearch(); 22 | } 23 | }; 24 | 25 | return ( 26 |
27 | 28 |
29 |
30 | 31 | 32 |
33 |
34 |
35 | setSearchTerm(e.target.value)} 39 | onKeyPress={handleKeyPress} 40 | placeholder="Get your username" 41 | className="neutro-input flex-grow text-xl sm:text-2xl w-full" 42 | /> 43 | 49 |
50 |
51 |
52 |

53 | Temp Mail Service 54 |

55 |

56 | 1) Your mails are public. Don't use it for important mails. Use 57 | it to subscribe to all unwanted services. 58 |

59 |

60 | 2) Your mails will be cleared from the database to prevent junk 61 | after 7 days. Please save all the data that you might need later! 62 |

63 |
64 |
65 |
66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /ui/app/search/SearchResults.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import ThemeToggle from "../../components/ThemeToggle"; 5 | import { Suspense, useEffect, useState } from "react"; 6 | import { searchEmails } from "@/app/actions/actions"; 7 | import { useSearchParams, useRouter } from "next/navigation"; 8 | import { SemiParserEmail } from "@/hooks/parseEmail"; 9 | 10 | export interface Email { 11 | date: string; 12 | sender: string; 13 | recipients: string; 14 | data: SemiParserEmail; 15 | } 16 | 17 | function SearchResultsContent() { 18 | const searchParams = useSearchParams(); 19 | const router = useRouter(); 20 | const query = searchParams.get("q") || ""; 21 | const selectedEmailId = searchParams.get("email"); 22 | 23 | useEffect(() => { 24 | if (!query) { 25 | router.push("/"); 26 | } 27 | }, [query, router]); 28 | 29 | const [emails, setEmails] = useState([]); 30 | const [loading, setLoading] = useState(true); 31 | 32 | useEffect(() => { 33 | async function fetchEmails() { 34 | if (query) { 35 | try { 36 | const result = await searchEmails(`${query}@flux.shubh.sh`); 37 | setEmails(result); 38 | } catch (error) { 39 | console.error("Failed to fetch emails:", error); 40 | } finally { 41 | setLoading(false); 42 | } 43 | } 44 | } 45 | fetchEmails(); 46 | }, [query]); 47 | 48 | if (loading) { 49 | return
Loading...
; 50 | } 51 | 52 | const selectedEmail = selectedEmailId 53 | ? emails[parseInt(selectedEmailId)] 54 | : null; 55 | 56 | return ( 57 |
58 | {selectedEmail ? ( 59 | <> 60 | 64 | BACK 65 | 66 |
67 |

68 | {selectedEmail.data.subject} 69 |

70 |

71 | {new Date(selectedEmail.date).toLocaleDateString()} 72 |

73 |

74 | From: {selectedEmail.data.from} 75 |

76 |

77 | To: {selectedEmail.recipients} 78 |

79 |
80 |
84 |
85 |
86 | 87 | ) : ( 88 | <> 89 |

90 | Mails for "{query}@flux.shubh.sh" 91 |

92 |
93 | {emails.map((email, index) => ( 94 | 95 |
96 |

97 | {email.data.subject || "(No Subject)"} 98 |

99 |

100 | {new Date(email.date).toLocaleDateString()} 101 |

102 |

103 | {email.data.text} 104 |

105 |
106 | 107 | ))} 108 |
109 | {emails.length === 0 && ( 110 |

111 | No mails found. Try sending a mail to
112 | 113 | '{query}@flux.shubh.sh' 114 | {" "} 115 |
116 | and Try Again. 117 |

118 | )} 119 | 120 | )} 121 |
122 | ); 123 | } 124 | 125 | export default function SearchResults() { 126 | return ( 127 |
128 |
129 | 133 | Flux Mail 134 | 135 | 136 |
137 | Loading...
} 139 | > 140 | 141 | 142 | 143 | ); 144 | } 145 | -------------------------------------------------------------------------------- /ui/app/search/page.tsx: -------------------------------------------------------------------------------- 1 | import SearchResults from "./SearchResults"; 2 | 3 | export default function SearchPage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /ui/components/AnimatedBackground.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useRef } from "react"; 4 | import { useTheme } from "@/contexts/ThemeContext"; 5 | 6 | const AnimatedBackground = () => { 7 | const canvasRef = useRef(null); 8 | const { theme } = useTheme(); 9 | 10 | useEffect(() => { 11 | const canvas = canvasRef.current; 12 | if (!canvas) return; 13 | 14 | const ctx = canvas.getContext("2d"); 15 | if (!ctx) return; 16 | 17 | canvas.width = window.innerWidth; 18 | canvas.height = window.innerHeight; 19 | 20 | const particles: Particle[] = []; 21 | const particleCount = 100; 22 | 23 | class Particle { 24 | x: number; 25 | y: number; 26 | size: number; 27 | speedX: number; 28 | speedY: number; 29 | 30 | constructor() { 31 | this.x = Math.random() * canvas!.width; 32 | this.y = Math.random() * canvas!.height; 33 | this.size = Math.random() * 5 + 1; 34 | this.speedX = Math.random() * 3 - 1.5; 35 | this.speedY = Math.random() * 3 - 1.5; 36 | } 37 | 38 | update() { 39 | this.x += this.speedX; 40 | this.y += this.speedY; 41 | 42 | if (this.x > canvas!.width) this.x = 0; 43 | else if (this.x < 0) this.x = canvas!.width; 44 | 45 | if (this.y > canvas!.height) this.y = 0; 46 | else if (this.y < 0) this.y = canvas!.height; 47 | } 48 | 49 | draw() { 50 | if (!ctx) return; 51 | ctx.fillStyle = 52 | theme === "light" ? "rgba(0, 0, 0, 0.1)" : "rgba(255, 255, 255, 0.1)"; 53 | ctx.beginPath(); 54 | ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); 55 | ctx.closePath(); 56 | ctx.fill(); 57 | } 58 | } 59 | 60 | const init = () => { 61 | for (let i = 0; i < particleCount; i++) { 62 | particles.push(new Particle()); 63 | } 64 | }; 65 | 66 | const animate = () => { 67 | if (!ctx) return; 68 | ctx.clearRect(0, 0, canvas.width, canvas.height); 69 | for (const particle of particles) { 70 | particle.update(); 71 | particle.draw(); 72 | } 73 | requestAnimationFrame(animate); 74 | }; 75 | 76 | init(); 77 | animate(); 78 | 79 | const handleResize = () => { 80 | canvas.width = window.innerWidth; 81 | canvas.height = window.innerHeight; 82 | }; 83 | 84 | window.addEventListener("resize", handleResize); 85 | 86 | return () => { 87 | window.removeEventListener("resize", handleResize); 88 | }; 89 | }, [theme]); 90 | 91 | return ( 92 | 96 | ); 97 | }; 98 | 99 | export default AnimatedBackground; 100 | -------------------------------------------------------------------------------- /ui/components/DesignElement.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface DesignElementProps { 4 | className?: string; 5 | } 6 | 7 | const DesignElement: React.FC = ({ className }) => { 8 | return ( 9 |
10 | 17 | 24 | 29 | 30 |
31 | ); 32 | }; 33 | 34 | export default DesignElement; 35 | -------------------------------------------------------------------------------- /ui/components/InteractiveTitle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | 5 | const InteractiveTitle = () => { 6 | const [isHovered, setIsHovered] = useState(false); 7 | 8 | return ( 9 |

setIsHovered(true)} 12 | onMouseLeave={() => setIsHovered(false)} 13 | style={{ 14 | textShadow: isHovered ? "4px 4px 0px var(--accent)" : "none", 15 | transform: isHovered ? "translateY(-4px)" : "none", 16 | }} 17 | > 18 | FLUX MAIL 19 |

20 | ); 21 | }; 22 | 23 | export default InteractiveTitle; 24 | -------------------------------------------------------------------------------- /ui/components/ThemeToggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useTheme } from "@/contexts/ThemeContext"; 4 | 5 | export default function ThemeToggle() { 6 | const { theme, toggleTheme } = useTheme(); 7 | 8 | return ( 9 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /ui/contexts/ThemeContext.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { createContext, useContext, useState, useEffect } from "react"; 4 | 5 | type Theme = "light" | "dark"; 6 | 7 | type ThemeContextType = { 8 | theme: Theme; 9 | toggleTheme: () => void; 10 | }; 11 | 12 | const ThemeContext = createContext(undefined); 13 | 14 | export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ 15 | children, 16 | }) => { 17 | const [theme, setTheme] = useState("light"); 18 | 19 | useEffect(() => { 20 | const savedTheme = localStorage.getItem("theme") as Theme | null; 21 | if (savedTheme) { 22 | setTheme(savedTheme); 23 | } 24 | }, []); 25 | 26 | useEffect(() => { 27 | localStorage.setItem("theme", theme); 28 | document.body.classList.toggle("dark", theme === "dark"); 29 | }, [theme]); 30 | 31 | const toggleTheme = () => { 32 | setTheme((prevTheme) => (prevTheme === "light" ? "dark" : "light")); 33 | }; 34 | 35 | return ( 36 | 37 | {children} 38 | 39 | ); 40 | }; 41 | 42 | export const useTheme = () => { 43 | const context = useContext(ThemeContext); 44 | if (context === undefined) { 45 | throw new Error("useTheme must be used within a ThemeProvider"); 46 | } 47 | return context; 48 | }; 49 | -------------------------------------------------------------------------------- /ui/env.sample: -------------------------------------------------------------------------------- 1 | DATABASE_URL=YOUR_DATABASE_URL -------------------------------------------------------------------------------- /ui/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /ui/hooks/parseEmail.ts: -------------------------------------------------------------------------------- 1 | import { Attachment, simpleParser } from "mailparser"; 2 | 3 | export interface SemiParserEmail { 4 | subject: string; 5 | from: string; 6 | text: string; 7 | html: string; 8 | text_as_html: string; 9 | attachments: Attachment[]; 10 | date: Date; 11 | } 12 | 13 | export async function parseEmailContent( 14 | rawData: string 15 | ): Promise { 16 | try { 17 | const parsed = await simpleParser(rawData); 18 | let a = { 19 | subject: parsed.subject || "", 20 | from: parsed.from?.text || "", 21 | text: parsed.text || "", 22 | html: parsed.html || "", 23 | text_as_html: parsed.textAsHtml || "", 24 | attachments: parsed.attachments || [], 25 | date: parsed.date || new Date(), 26 | }; 27 | return a; 28 | } catch (error) { 29 | console.error("Error parsing email:", error); 30 | throw error; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ui/lib/db.ts: -------------------------------------------------------------------------------- 1 | import { Pool } from "pg"; 2 | 3 | export const pool = new Pool({ 4 | connectionString: process.env.DATABASE_URL, 5 | }); 6 | -------------------------------------------------------------------------------- /ui/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@types/mailparser": "^3.4.5", 13 | "@vercel/analytics": "^1.4.1", 14 | "@vercel/speed-insights": "^1.1.0", 15 | "mailparser": "^3.7.2", 16 | "next": "15.1.2", 17 | "pg": "^8.13.1", 18 | "react": "^19.0.0", 19 | "react-dom": "^19.0.0" 20 | }, 21 | "devDependencies": { 22 | "@eslint/eslintrc": "^3", 23 | "@types/node": "^20", 24 | "@types/pg": "^8.11.10", 25 | "@types/react": "^19", 26 | "@types/react-dom": "^19", 27 | "eslint": "^9", 28 | "eslint-config-next": "15.1.2", 29 | "postcss": "^8", 30 | "tailwindcss": "^3.4.1", 31 | "typescript": "^5" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ui/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /ui/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --background: #f0f0f0; 7 | --foreground: #000000; 8 | --accent: #ff6b6b; 9 | --secondary: #4ecdc4; 10 | } 11 | 12 | .dark { 13 | --background: #1a1a1a; 14 | --foreground: #ffffff; 15 | --accent: #ff6b6b; 16 | --secondary: #4ecdc4; 17 | } 18 | 19 | body { 20 | background-color: var(--background); 21 | color: var(--foreground); 22 | font-family: "Courier New", Courier, monospace; 23 | } 24 | 25 | .neutro-box { 26 | border: 4px solid var(--foreground); 27 | box-shadow: 8px 8px 0 var(--foreground); 28 | transition: all 0.3s ease; 29 | } 30 | 31 | .neutro-box:hover { 32 | transform: translate(-4px, -4px); 33 | box-shadow: 12px 12px 0 var(--foreground); 34 | } 35 | 36 | .neutro-button { 37 | border: 4px solid var(--foreground); 38 | background-color: var(--accent); 39 | color: var(--background); 40 | font-weight: bold; 41 | padding: 0.5rem 1rem; 42 | transition: all 0.2s ease-in-out; 43 | white-space: nowrap; 44 | } 45 | 46 | .neutro-button:hover { 47 | background-color: var(--foreground); 48 | color: var(--accent); 49 | transform: translate(-4px, -4px); 50 | box-shadow: 8px 8px 0 var(--foreground); 51 | } 52 | 53 | .neutro-input { 54 | border: 4px solid var(--foreground); 55 | background-color: var(--background); 56 | color: var(--foreground); 57 | padding: 0.5rem 1rem; 58 | transition: all 0.2s ease-in-out; 59 | width: 100%; 60 | } 61 | 62 | .neutro-input:focus { 63 | outline: none; 64 | box-shadow: 4px 4px 0 var(--foreground); 65 | transform: translate(-2px, -2px); 66 | } 67 | 68 | @keyframes float { 69 | 0%, 70 | 100% { 71 | transform: translateY(0); 72 | } 73 | 50% { 74 | transform: translateY(-10px); 75 | } 76 | } 77 | 78 | .float-animation { 79 | animation: float 5s ease-in-out infinite; 80 | } 81 | 82 | @keyframes pulse { 83 | 0%, 84 | 100% { 85 | transform: scale(1); 86 | } 87 | 50% { 88 | transform: scale(1.05); 89 | } 90 | } 91 | 92 | .pulse-animation { 93 | animation: pulse 2s ease-in-out infinite; 94 | } 95 | 96 | @media (max-width: 640px) { 97 | .neutro-box { 98 | box-shadow: 6px 6px 0 var(--foreground); 99 | } 100 | 101 | .neutro-box:hover { 102 | box-shadow: 8px 8px 0 var(--foreground); 103 | } 104 | 105 | .neutro-button:hover { 106 | box-shadow: 6px 6px 0 var(--foreground); 107 | } 108 | } 109 | 110 | .prose { 111 | max-width: none; 112 | } 113 | 114 | .prose a { 115 | color: var(--accent); 116 | text-decoration: underline; 117 | } 118 | 119 | .prose img { 120 | max-width: 100%; 121 | height: auto; 122 | } 123 | -------------------------------------------------------------------------------- /ui/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | export default { 4 | content: [ 5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | colors: { 12 | background: "var(--background)", 13 | foreground: "var(--foreground)", 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | } satisfies Config; 19 | -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | --------------------------------------------------------------------------------