From 6dec8f2832df531b6815a517038376b660d347af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Wed, 20 May 2026 00:27:04 +0200 Subject: [PATCH] tests: clean unused and fix deny --- .github/workflows/audit.yml | 9 + .github/workflows/fastmail-tests.yml | 25 -- .github/workflows/releases.yml | 3 +- .github/workflows/stalwart-tests.yml | 18 - Cargo.toml | 3 +- ROADMAP.md | 108 ----- deny.toml | 7 + flake.lock | 26 +- flake.nix | 4 +- shell.nix | 3 +- test.eml | 10 - test.mml | 5 - tests/common/imap.rs | 338 --------------- tests/common/jmap.rs | 613 --------------------------- tests/common/shared.rs | 403 ------------------ tests/common/smtp.rs | 38 -- tests/fastmail-imap.rs | 42 -- tests/fastmail-jmap.rs | 27 -- tests/fastmail-smtp.rs | 28 -- tests/stalwart-imap.rs | 25 -- tests/stalwart-jmap.rs | 25 -- tests/stalwart-smtp.rs | 23 - tests/stalwart.sh | 20 - 23 files changed, 36 insertions(+), 1767 deletions(-) create mode 100644 .github/workflows/audit.yml delete mode 100644 .github/workflows/fastmail-tests.yml delete mode 100644 .github/workflows/stalwart-tests.yml delete mode 100644 ROADMAP.md create mode 100644 deny.toml delete mode 100644 test.eml delete mode 100644 test.mml delete mode 100644 tests/common/imap.rs delete mode 100644 tests/common/jmap.rs delete mode 100644 tests/common/shared.rs delete mode 100644 tests/common/smtp.rs delete mode 100644 tests/fastmail-imap.rs delete mode 100644 tests/fastmail-jmap.rs delete mode 100644 tests/fastmail-smtp.rs delete mode 100644 tests/stalwart-imap.rs delete mode 100644 tests/stalwart-jmap.rs delete mode 100644 tests/stalwart-smtp.rs delete mode 100755 tests/stalwart.sh diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml new file mode 100644 index 00000000..d03c8254 --- /dev/null +++ b/.github/workflows/audit.yml @@ -0,0 +1,9 @@ +name: Audit + +on: + push: + +jobs: + audit: + uses: pimalaya/nix/.github/workflows/audit.yml@master + secrets: inherit diff --git a/.github/workflows/fastmail-tests.yml b/.github/workflows/fastmail-tests.yml deleted file mode 100644 index 7f650831..00000000 --- a/.github/workflows/fastmail-tests.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Fastmail tests - -on: - push: - branches: - - v2 - -jobs: - fastmail-tests: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - run: cargo test --no-default-features --features jmap,rustls-ring --test fastmail-jmap -- --ignored - env: - FASTMAIL_EMAIL: ${{ secrets.FASTMAIL_EMAIL }} - FASTMAIL_API_TOKEN: ${{ secrets.FASTMAIL_API_TOKEN }} - - run: cargo test --no-default-features --features imap,rustls-ring --test fastmail-imap -- --ignored - env: - FASTMAIL_EMAIL: ${{ secrets.FASTMAIL_EMAIL }} - FASTMAIL_APP_PASSWORD: ${{ secrets.FASTMAIL_APP_PASSWORD }} - - run: cargo test --no-default-features --features smtp,rustls-ring --test fastmail-smtp -- --ignored - env: - FASTMAIL_EMAIL: ${{ secrets.FASTMAIL_EMAIL }} - FASTMAIL_APP_PASSWORD: ${{ secrets.FASTMAIL_APP_PASSWORD }} diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index fa809e9e..d8870f69 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -6,7 +6,6 @@ on: - v* branches: - master - - v2 jobs: release: @@ -14,4 +13,4 @@ jobs: secrets: inherit with: project: himalaya - nixpkgs: staging-next + nixpkgs: nixos-25.11 diff --git a/.github/workflows/stalwart-tests.yml b/.github/workflows/stalwart-tests.yml deleted file mode 100644 index 0b88ac5e..00000000 --- a/.github/workflows/stalwart-tests.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Stalwart tests - -on: - push: - branches: - - v2 - -jobs: - stalwart-tests: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - run: ./tests/stalwart.sh - - run: cargo test --no-default-features --features jmap,rustls-ring --test stalwart-jmap -- --ignored - - run: cargo test --no-default-features --features imap,rustls-ring --test stalwart-imap -- --ignored - - run: cargo test --no-default-features --features smtp,rustls-ring --test stalwart-smtp -- --ignored - - run: docker stop himalaya-tests diff --git a/Cargo.toml b/Cargo.toml index 8be4ef34..5145656a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,8 @@ name = "himalaya" description = "CLI to manage emails" version = "2.0.0-alpha.1" authors = ["soywod "] -edition = "2021" +rust-version = "1.87" +edition = "2024" license = "MIT" categories = ["command-line-utilities", "email"] keywords = ["cli", "email", "imap", "maildir", "smtp"] diff --git a/ROADMAP.md b/ROADMAP.md deleted file mode 100644 index 75163e43..00000000 --- a/ROADMAP.md +++ /dev/null @@ -1,108 +0,0 @@ -# Himalaya CLI v2 — roadmap - -Living document. Items are grouped by readiness and ordered roughly by -easiest-first within each group. Decisions land inline as `Decided:` -notes; open questions stay in section E until resolved. - -## A. Shared API — already adequate - -| Command | Status | -|---|---| -| `mailboxes list` | done | -| `envelopes list` | done (pagination: `--page` / `--page-size`, default size 25) | -| `flags add/set/delete` | done | -| `messages get` | done (`--raw` writes original RFC 5322 to stdout; `--json` emits the parsed struct) | -| `messages copy` | done (`… --from --to `) | -| `messages move` | done (`… --from --to `) | -| `messages add` | done (raw RFC 5322 in; `--mailbox ` + optional `--flag` / `--file`; IMAP APPEND, JMAP Blob/upload + Email/import, Maildir tmp-then-rename) | -| `messages compose` | done (CLI args incl. `--reply` / `--forward` with `--posting-style top\|bottom`, `--quote-headline`, `--signature`/`--signature-file`, `--send`) | -| `messages send` | done (raw RFC 5322 in; SMTP + JMAP) | -| `attachments list` | done (filename / mime / size / inline; `--include-inline` to surface inline parts) | -| `attachments download` | done (`--dir `; defaults to account/global `downloads-dir`, then platform downloads dir, then temp) | - -**Shared-command rule**: cross-backend commands always treat the IMAP -id as a UID. There is **no** `--seq` flag on the shared API — that -flag is reserved for the protocol-specific `imap` subcommands. - -## B. Shared API — follow-ups (none currently) - -`messages compose` no longer needs body quoting (shipped via -`--posting-style` + `--quote-headline` + `--signature`). `--save-to` -remains intentionally out of scope: the canonical pipeline is -`messages compose | messages add --mailbox Drafts --flag draft`, -which composes well precisely because both halves are shared -commands. MIME-part extraction stays delegated to `mml`. - - -## C. Binary surface — to add back later - -### C1. Sendmail / command backend - -- Generic "exec a command, write the message to its stdin" backend. - Replaces both v1's `sendmail` variant and any future `mailcmd` - integrations. -- Config: `command.send = "/usr/sbin/sendmail -t"` (or similar). -- Wires only into `messages send`. - -### C2. Notmuch backend - -- Local index over Maildir. Adds `notmuch envelopes search` (real - query support — Notmuch's reason to exist) and reuses the maildir - arms for everything else. - -### C3. `accounts` subcommand + wizard - -- **Decided (E6)**: top-level verb is `accounts configure`. -- `accounts list` (read TOML, dump JSON). -- `accounts configure []` → wizard. Depends on the new - `pimalaya/inquire` lib (crossterm-based, extracted from old - `pimalaya-tui`) — himalaya hosts the `accounts configure` entry - point, the lib provides the prompts. -- Possibly `accounts doctor` later (connectivity check). - -### C4. Keyring support (opt-in) - -- Cargo feature `keyring`, off by default. When on, secret values in - TOML can be `{ keyring = "service:account" }`. -- Decoupled from auth itself — works for IMAP password, SMTP password, - JMAP bearer, etc. - -## D. Out of scope (delegated) - -| Concern | Lives in | -|---|---| -| OAuth2 token acquisition / refresh | `pimalaya/ortie` (CLI) | -| MML composition, signing, encryption | `pimalaya/mml` (CLI) | -| HTML rendering | downstream tool / TUI | -| Search query DSL across backends | per-protocol commands only | -| Cross-backend sync | future `pimalaya/sirup` (or similar) | - -Dropped permanently from v2 binary: `template *`, `attachment download` -(use `mml` for MIME part extraction), `messages delete` (use a flag or -move-to-trash), cross-backend copy, mailto handler. - -## E. Resolved design decisions - -1. **Pagination default size** — 25, matching v1. (E1) -2. **Compose shape** — single `messages compose` command with - mutually-exclusive `--reply ` / `--forward ` flags. (E2) -3. **Compose vs send** — `compose --send` is supported. No interactive - editor → no reason to force a `compose | send` pipe. (E3) -4. **Copy/move arg shape** — `… --from --to `, - with `--from` defaulting to `Inbox` and `--to` mandatory. (E4) -5. **`messages get` mode flags** — only `--raw`. JSON output is the - global `--json` flag's job. No `--attachment-dir`, no `--part`. (E5) -6. **Wizard placement** — `accounts configure`. (E6) -7. **Backend dispatch** — none. The shared command runs unless the - user explicitly types the protocol verb (`himalaya imap …`, - `himalaya jmap …`, etc.), in which case the protocol-specific CLI - takes over. (E7) - -## F. Suggested ordering - -1. **B1** compose body-quoting + `--save-to` — finish what's missing. -2. **C1** command/sendmail backend — drops in once the SMTP arm is - stable. -3. **C3 + C4** account wizard + keyring — both gated on the inquire - lib being extracted. -4. **C2** notmuch — last, depends on having maildir arms solid. diff --git a/deny.toml b/deny.toml new file mode 100644 index 00000000..a7a2ba4f --- /dev/null +++ b/deny.toml @@ -0,0 +1,7 @@ +[sources] +allow-git = ["https://github.com/soywod/domain", "https://github.com/pimalaya/io-discovery", "https://github.com/pimalaya/io-email", "https://github.com/pimalaya/io-http", "https://github.com/pimalaya/io-imap", "https://github.com/pimalaya/io-jmap", "https://github.com/pimalaya/io-maildir", "https://github.com/pimalaya/io-smtp", "https://github.com/pimalaya/cli", "https://github.com/pimalaya/config", "https://github.com/pimalaya/stream"] +unknown-git = "deny" +unknown-registry = "deny" + +[licenses] +allow = ["0BSD", "Apache-2.0", "Apache-2.0 WITH LLVM-exception", "BSD-3-Clause", "CDLA-Permissive-2.0", "ISC", "MIT", "MPL-2.0", "Unicode-3.0", "Zlib"] diff --git a/flake.lock b/flake.lock index 3cda8e43..55ecf1c4 100644 --- a/flake.lock +++ b/flake.lock @@ -8,11 +8,11 @@ "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1772348640, - "narHash": "sha256-caiKs7O4khFydpKyg8O8/nmvw/NfN4fn/4spageGoig=", + "lastModified": 1777624102, + "narHash": "sha256-thSyElkje577x/kAbP72nHlfiFc1a+tCudskLPHXe9s=", "owner": "nix-community", "repo": "fenix", - "rev": "47c5355eaba0b08836e720d5d545c8ea1e1783db", + "rev": "4d81601e0b73f20d81d066754ad0e7d1e7f75a06", "type": "github" }, "original": { @@ -24,16 +24,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1738537163, - "narHash": "sha256-vRjFUwVd06mqXC0wSP5ZNrQD5F5SkFvUqmYSWxWqoMg=", + "lastModified": 1777673416, + "narHash": "sha256-5c2POKPOjU40Kh0MirOdScBLG0bu9TAuPYAtPRNZMBs=", "owner": "nixos", "repo": "nixpkgs", - "rev": "736314654d0ede857a249cb06ae7afaea8bcd185", + "rev": "26ef669cffa904b6f6832ab57b77892a37c1a671", "type": "github" }, "original": { "owner": "nixos", - "ref": "staging-next", + "ref": "nixos-25.11", "repo": "nixpkgs", "type": "github" } @@ -41,11 +41,11 @@ "pimalaya": { "flake": false, "locked": { - "lastModified": 1770914484, - "narHash": "sha256-kPxdpg98/EVQKLkUdiFEYWWWI1uTLWfIewyCmb4ZfpE=", + "lastModified": 1777930959, + "narHash": "sha256-0U5rd30a74ToN9+Cqv/wR/PN9pXXxsi+0XuUeA6Vs/8=", "owner": "pimalaya", "repo": "nix", - "rev": "23454cd59e2a32199a67f36f3b13cd5145b5ffb3", + "rev": "129a1d7a378b053160adb4f5952abe8e7050cb4b", "type": "github" }, "original": { @@ -64,11 +64,11 @@ "rust-analyzer-src": { "flake": false, "locked": { - "lastModified": 1772310333, - "narHash": "sha256-njFwHnxYcfQINwSa+XWhenv8s8PMg/j5ID0HpIa49xM=", + "lastModified": 1777583169, + "narHash": "sha256-dVJ4+wrRKc8oIgp3rLOFSq1obt/sCKlXy3h47qof/w0=", "owner": "rust-lang", "repo": "rust-analyzer", - "rev": "a96b6a9b887008bae01839543f9ca8e1f67f4ebe", + "rev": "aa64e4828a2bbba44463c1229a81c748d3cce583", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 404b0998..88394027 100644 --- a/flake.nix +++ b/flake.nix @@ -1,9 +1,9 @@ { - description = "CLI to manage emails"; + description = "Client library and CLI to discover PIM-related services, written in Rust"; inputs = { nixpkgs = { - url = "github:nixos/nixpkgs/staging-next"; + url = "github:nixos/nixpkgs/nixos-25.11"; }; fenix = { url = "github:nix-community/fenix/monthly"; diff --git a/shell.nix b/shell.nix index 1b44f292..296f65b2 100644 --- a/shell.nix +++ b/shell.nix @@ -7,7 +7,7 @@ }: let - inherit (pkgs) openssl pkg-config; + inherit (pkgs) cargo-deny openssl pkg-config; shell = pimalaya.mkShell { inherit @@ -29,6 +29,7 @@ shell.overrideAttrs (prev: { ]; buildInputs = (prev.buildInputs or [ ]) ++ [ + cargo-deny openssl ]; }) diff --git a/test.eml b/test.eml deleted file mode 100644 index 764bc166..00000000 --- a/test.eml +++ /dev/null @@ -1,10 +0,0 @@ -MIME-Version: 1.0 -From: -To: -Subject: Test -Message-ID: <189af1cfd6621170.2d8f0b477933eb9f.5c085132da6091e3@soywod> -Date: Sun, 8 Mar 2026 18:41:18 +0000 -Content-Type: text/plain; charset="utf-8" -Content-Transfer-Encoding: 7bit - -Hello! diff --git a/test.mml b/test.mml deleted file mode 100644 index 8f1658a1..00000000 --- a/test.mml +++ /dev/null @@ -1,5 +0,0 @@ -From: clement.douin@posteo.net -To: pimalaya.org@gmail.com -Subject: Test - -Hello! diff --git a/tests/common/imap.rs b/tests/common/imap.rs deleted file mode 100644 index 0fe94b7d..00000000 --- a/tests/common/imap.rs +++ /dev/null @@ -1,338 +0,0 @@ -use std::{ - path::Path, - time::{SystemTime, UNIX_EPOCH}, -}; - -use assert_cmd::Command; -use serde_json::Value; - -/// Resources to clean up after the test, even on failure. -struct Cleanup<'a> { - config: &'a Path, - /// Primary test mailbox name — deleted on drop. - mbox_name: Option, - /// Secondary test mailbox name (copy/move destination) — deleted on drop. - mbox_name_2: Option, -} - -impl Drop for Cleanup<'_> { - fn drop(&mut self) { - if let Some(name) = &self.mbox_name { - let _ = imap(self.config) - .args(["mailboxes", "delete", name]) - .output(); - } - - if let Some(name) = &self.mbox_name_2 { - let _ = imap(self.config) - .args(["mailboxes", "delete", name]) - .output(); - } - } -} - -/// Builds a `himalaya imap` command with the given config path. -fn imap(config: &Path) -> Command { - let mut cmd = Command::cargo_bin("himalaya").unwrap(); - cmd.args(["-c", config.to_str().unwrap(), "imap"]); - cmd -} - -/// Builds a `himalaya --json imap` command (JSON output mode). -fn imap_json(config: &Path) -> Command { - let mut cmd = Command::cargo_bin("himalaya").unwrap(); - cmd.args(["--json", "-c", config.to_str().unwrap(), "imap"]); - cmd -} - -/// Shared IMAP integration test suite. -/// -/// Exercises every command in a single ordered flow. Pass a path to a -/// valid TOML config file with a default IMAP account configured. -pub fn run(config: &Path, email: impl ToString) { - let email = email.to_string(); - - let ts = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis(); - - let mbox_name = format!("himalaya-test-{ts}"); - let mbox_name_renamed = format!("himalaya-test-{ts}-renamed"); - let mbox_name_2 = format!("himalaya-test-{ts}-copy"); - - let mut cleanup = Cleanup { - config, - mbox_name: None, - mbox_name_2: None, - }; - - // ── 1. LIST mailboxes ───────────────────────────────────────────────── - - // baseline list — must succeed and return output (INBOX always exists) - imap(config).args(["mailboxes", "list"]).assert().success(); - - // ── 2. CREATE mailbox ───────────────────────────────────────────────── - - imap(config) - .args(["mailboxes", "create", &mbox_name]) - .assert() - .success(); - - cleanup.mbox_name = Some(mbox_name_renamed.clone()); - - // create the copy/move destination mailbox - imap(config) - .args(["mailboxes", "create", &mbox_name_2]) - .assert() - .success(); - - cleanup.mbox_name_2 = Some(mbox_name_2.clone()); - - // ── 3. RENAME mailbox ───────────────────────────────────────────────── - - imap(config) - .args(["mailboxes", "rename", &mbox_name, &mbox_name_renamed]) - .assert() - .success(); - - // verify the renamed mailbox appears in list - let stdout = imap(config) - .args(["mailboxes", "list", "--all"]) - .assert() - .success() - .get_output() - .stdout - .clone(); - - let list_output = String::from_utf8(stdout).unwrap(); - - assert!( - list_output.contains(&mbox_name_renamed), - "renamed mailbox should appear in list" - ); - - // ── 4. STATUS ───────────────────────────────────────────────────────── - - imap(config) - .args(["mailboxes", "status", &mbox_name_renamed]) - .assert() - .success(); - - // ── 5. APPEND a test message ────────────────────────────────────────── - - let eml = [ - &format!("From: Himalaya Test <{email}>"), - &format!("To: Himalaya Test <{email}>"), - "Subject: Himalaya IMAP integration test", - "Date: Thu, 01 Jan 2026 00:00:00 +0000", - "MIME-Version: 1.0", - "Content-Type: text/plain; charset=utf-8", - "", - "This is a test email for himalaya IMAP integration tests.", - ] - .join("\r\n"); - - imap(config) - .args(["messages", "save", &mbox_name_renamed]) - .write_stdin(eml.as_bytes()) - .assert() - .success(); - - // ── 6. LIST envelopes ───────────────────────────────────────────────── - - let stdout = imap_json(config) - .args(["envelopes", "list", "--mailbox", &mbox_name_renamed]) - .assert() - .success() - .get_output() - .stdout - .clone(); - - let envelopes: Vec = serde_json::from_slice::(&stdout) - .unwrap_or_else(|e| { - panic!( - "failed to parse envelope list output: {e}\nstdout: {}", - String::from_utf8_lossy(&stdout) - ) - }) - .get("envelopes") - .and_then(|v| serde_json::from_value(v.clone()).ok()) - .unwrap_or_else(|| { - panic!( - "missing `envelopes` key in output: {}", - String::from_utf8_lossy(&stdout) - ) - }); - - assert_eq!( - envelopes.len(), - 1, - "expected exactly one envelope after save" - ); - - let uid = envelopes[0]["uid"] - .as_u64() - .expect("envelope should have numeric uid"); - - assert!(uid > 0, "uid must be non-zero"); - - let uid_str = uid.to_string(); - - // ── 7. GET the message ──────────────────────────────────────────────── - - imap(config) - .args(["messages", "get", "--mailbox", &mbox_name_renamed, &uid_str]) - .assert() - .success(); - - // ── 8. SEARCH ───────────────────────────────────────────────────────── - - let stdout = imap_json(config) - .args([ - "envelopes", - "search", - "--mailbox", - &mbox_name_renamed, - "subject:Himalaya", - ]) - .assert() - .success() - .get_output() - .stdout - .clone(); - - let results: Vec = serde_json::from_slice::(&stdout) - .unwrap_or_else(|e| { - panic!( - "failed to parse search output: {e}\nstdout: {}", - String::from_utf8_lossy(&stdout) - ) - }) - .get("ids") - .and_then(|v| serde_json::from_value(v.clone()).ok()) - .unwrap_or_else(|| { - panic!( - "missing `ids` key in search output: {}", - String::from_utf8_lossy(&stdout) - ) - }); - - assert!( - !results.is_empty(), - "search should find the appended message" - ); - - // ── 9. FLAGS: add \\Seen, then remove it ────────────────────────────── - - imap(config) - .args([ - "flags", - "add", - "--mailbox", - &mbox_name_renamed, - &uid_str, - "--flag", - "\\Seen", - ]) - .assert() - .success(); - - imap(config) - .args([ - "flags", - "remove", - "--mailbox", - &mbox_name_renamed, - &uid_str, - "--flag", - "\\Seen", - ]) - .assert() - .success(); - - // ── 10. COPY message to the second mailbox ──────────────────────────── - - imap(config) - .args([ - "messages", - "copy", - "--mailbox", - &mbox_name_renamed, - &uid_str, - &mbox_name_2, - ]) - .assert() - .success(); - - // verify copy landed in destination - let stdout = imap_json(config) - .args(["envelopes", "list", "--mailbox", &mbox_name_2]) - .assert() - .success() - .get_output() - .stdout - .clone(); - - let dest_envelopes: Vec = serde_json::from_slice::(&stdout) - .unwrap_or_else(|e| { - panic!( - "failed to parse destination envelope list: {e}\nstdout: {}", - String::from_utf8_lossy(&stdout) - ) - }) - .get("envelopes") - .and_then(|v| serde_json::from_value(v.clone()).ok()) - .unwrap_or_else(|| { - panic!( - "missing `envelopes` key in destination output: {}", - String::from_utf8_lossy(&stdout) - ) - }); - - assert_eq!( - dest_envelopes.len(), - 1, - "expected one message in copy destination" - ); - - let dest_uid = dest_envelopes[0]["uid"] - .as_u64() - .expect("destination envelope should have numeric uid"); - - let dest_uid_str = dest_uid.to_string(); - - // ── 11. MOVE message from second mailbox back to primary ────────────── - - imap(config) - .args([ - "messages", - "move", - "--mailbox", - &mbox_name_2, - &dest_uid_str, - &mbox_name_renamed, - ]) - .assert() - .success(); - - // ── 12. DELETE both test mailboxes ──────────────────────────────────── - - // delete the primary test mailbox - imap(config) - .args(["mailboxes", "delete", &mbox_name_renamed]) - .assert() - .success(); - - cleanup.mbox_name = None; - - // delete the secondary test mailbox - imap(config) - .args(["mailboxes", "delete", &mbox_name_2]) - .assert() - .success(); - - cleanup.mbox_name_2 = None; - - // cleanup via Drop (no-ops since we cleared both names above) -} diff --git a/tests/common/jmap.rs b/tests/common/jmap.rs deleted file mode 100644 index a13d6ac1..00000000 --- a/tests/common/jmap.rs +++ /dev/null @@ -1,613 +0,0 @@ -use std::{ - env, - path::Path, - time::{SystemTime, UNIX_EPOCH}, -}; - -use assert_cmd::Command; -use io_jmap::rfc8621::{ - email::Email, email_submission::EmailSubmission, identity::Identity, mailbox::Mailbox, - thread::Thread, vacation_response::VacationResponse, -}; -use serde::de::DeserializeOwned; -use serde_json::Value; - -/// Resources to clean up after the test, even on failure. -struct Cleanup<'a> { - config: &'a Path, - /// Test mailbox ID — destroyed with --purge (removes all emails inside). - mbox_id: Option, - /// Identity created during the test. - identity_id: Option, -} - -impl Drop for Cleanup<'_> { - fn drop(&mut self) { - if let Some(id) = &self.identity_id { - let _ = jmap(self.config).args(["identity", "delete", id]).output(); - } - - if let Some(id) = &self.mbox_id { - let _ = jmap(self.config) - .args(["mailboxes", "destroy", "--purge", id]) - .output(); - } - } -} - -/// Builds a `himalaya jmap` command with the given config path. -fn jmap(config: &Path) -> Command { - let mut cmd = Command::cargo_bin("himalaya").unwrap(); - cmd.args(["-c", config.to_str().unwrap(), "jmap"]); - cmd -} - -/// Builds a `himalaya --json jmap` command (JSON output mode). -fn jmap_json(config: &Path) -> Command { - let mut cmd = Command::cargo_bin("himalaya").unwrap(); - cmd.args(["--json", "-c", config.to_str().unwrap(), "jmap"]); - cmd -} - -/// Runs a JSON-mode command, asserts success, and deserializes stdout into T. -fn parse_output(config: &Path, args: &[&str]) -> T { - let stdout = jmap_json(config) - .args(args) - .assert() - .success() - .get_output() - .stdout - .clone(); - - serde_json::from_slice(&stdout).unwrap_or_else(|e| { - panic!( - "failed to parse output for {:?}: {e}\nstdout: {}", - args, - String::from_utf8_lossy(&stdout) - ) - }) -} - -/// Runs a JSON-mode command, asserts success, extracts `key` from the wrapper -/// object, and deserializes the value into `Vec`. -fn parse_list(config: &Path, args: &[&str], key: &str) -> Vec { - let stdout = jmap_json(config) - .args(args) - .assert() - .success() - .get_output() - .stdout - .clone(); - - let value: Value = serde_json::from_slice(&stdout).unwrap_or_else(|e| { - panic!( - "failed to parse output for {:?}: {e}\nstdout: {}", - args, - String::from_utf8_lossy(&stdout) - ) - }); - - serde_json::from_value( - value - .get(key) - .cloned() - .unwrap_or_else(|| panic!("missing `{key}` key in output for {args:?}: {value}")), - ) - .unwrap_or_else(|e| { - panic!("failed to deserialize `{key}` from output for {args:?}: {e}\nvalue: {value}") - }) -} - -/// Shared JMAP integration test suite. -/// -/// Exercises every command in a single ordered flow. Pass a path to a -/// valid TOML config file with a default JMAP account configured. -pub fn run(config: &Path, email: impl ToString) { - let email = email.to_string(); - - let ts = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis(); - - let mbox_name = format!("himalaya-test-{ts}"); - - let mut cleanup = Cleanup { - config, - mbox_id: None, - identity_id: None, - }; - - // ── 1. MAILBOXES ────────────────────────────────────────────────────── - - // baseline list — must return at least one mailbox (e.g. INBOX) - let mboxes: Vec = parse_list(config, &["mailboxes", "query"], "mailboxes"); - - assert!( - !mboxes.is_empty(), - "mailboxes query should return at least one mailbox" - ); - - // create test mailbox (subscribed so it shows up in the default query) - jmap(config) - .args(["mailboxes", "create", &mbox_name, "--subscribe"]) - .assert() - .success(); - - // query by name — verify name matches - let mboxes: Vec = parse_list( - config, - &["mailboxes", "query", "--name", &mbox_name], - "mailboxes", - ); - - assert_eq!( - mboxes[0].name.as_deref(), - Some(mbox_name.as_str()), - "created mailbox name mismatch" - ); - - let mbox_id = mboxes[0].id.clone().expect("mailbox id"); - cleanup.mbox_id = Some(mbox_id.clone()); - - // get by id — verify id and name - let got: Vec = parse_list(config, &["mailboxes", "get", &mbox_id], "mailboxes"); - - assert_eq!( - got[0].id.as_deref(), - Some(mbox_id.as_str()), - "get: id mismatch" - ); - - assert_eq!( - got[0].name.as_deref(), - Some(mbox_name.as_str()), - "get: name mismatch" - ); - - // update: rename - let mbox_name_2 = format!("{mbox_name}-renamed"); - - jmap(config) - .args(["mailboxes", "update", &mbox_id, "--name", &mbox_name_2]) - .assert() - .success(); - - // get by id again — verify the rename took effect - let got: Vec = parse_list(config, &["mailboxes", "get", &mbox_id], "mailboxes"); - - assert_eq!( - got[0].name.as_deref(), - Some(mbox_name_2.as_str()), - "mailbox rename not reflected in get" - ); - - // ── 2. EMAILS ───────────────────────────────────────────────────────── - - let eml = [ - &format!("From: Himalaya Test <{email}>"), - &format!("To: Himalaya Test <{email}>"), - "Subject: Himalaya integration test", - "Date: Thu, 01 Jan 2026 00:00:00 +0000", - "MIME-Version: 1.0", - "Content-Type: text/plain; charset=utf-8", - "", - "This is a test email for himalaya integration tests.", - ] - .join("\r\n"); - - // import from stdin - jmap(config) - .args(["emails", "import", "--mailbox-id", &mbox_id]) - .write_stdin(eml.as_bytes()) - .assert() - .success(); - - // query — verify exactly one email landed in the mailbox - let emails: Vec = parse_list( - config, - &["emails", "query", "--mailbox", &mbox_id], - "emails", - ); - assert_eq!(emails.len(), 1, "expected exactly one email after import"); - - let email_id = emails[0].id.clone().expect("email id"); - let thread_id = emails[0].thread_id.clone().expect("thread id"); - - // get by id — verify the returned row matches the imported email - let got: Vec = parse_list(config, &["emails", "get", &email_id], "emails"); - - assert_eq!( - got[0].id.as_deref(), - Some(email_id.as_str()), - "emails get: id mismatch" - ); - - // read: plain text — verify headers + body are present - let stdout = jmap(config) - .args(["emails", "read", &email_id]) - .assert() - .success() - .get_output() - .stdout - .clone(); - - let text = String::from_utf8(stdout).unwrap(); - - assert!( - text.contains("Himalaya integration test"), - "read: subject missing" - ); - - assert!(text.contains("This is a test email"), "read: body missing"); - - // read: html (no html part in fixture — command still succeeds) - jmap(config) - .args(["emails", "read", "--html", &email_id]) - .assert() - .success(); - - // update: add $seen — then verify via query with --has-keyword - jmap(config) - .args(["emails", "update", &email_id, "--add-keyword", "$seen"]) - .assert() - .success(); - - let seen: Vec = parse_list( - config, - &[ - "emails", - "query", - "--mailbox", - &mbox_id, - "--has-keyword", - "$seen", - ], - "emails", - ); - - assert!( - seen.iter() - .any(|e| e.id.as_deref() == Some(email_id.as_str())), - "email should have $seen keyword after update" - ); - - // update: add $flagged - jmap(config) - .args(["emails", "update", &email_id, "--add-keyword", "$flagged"]) - .assert() - .success(); - - // update: remove $flagged — then verify it is gone - jmap(config) - .args([ - "emails", - "update", - &email_id, - "--remove-keyword", - "$flagged", - ]) - .assert() - .success(); - - let flagged: Vec = parse_list( - config, - &[ - "emails", - "query", - "--mailbox", - &mbox_id, - "--has-keyword", - "$flagged", - ], - "emails", - ); - - assert!( - !flagged - .iter() - .any(|e| e.id.as_deref() == Some(email_id.as_str())), - "email should not have $flagged keyword after remove" - ); - - // export: raw RFC 5322 to stdout — verify original headers are present - let stdout = jmap(config) - .args(["emails", "export", &email_id]) - .assert() - .success() - .get_output() - .stdout - .clone(); - - let raw = String::from_utf8(stdout).unwrap(); - - assert!( - raw.contains("Subject: Himalaya integration test"), - "export: subject missing" - ); - - assert!( - raw.contains("From: Himalaya Test"), - "export: From header missing" - ); - - // import --upload-only: upload blob and get its id - let stdout = jmap(config) - .args(["emails", "import", "--upload-only"]) - .write_stdin(eml) - .assert() - .success() - .get_output() - .stdout - .clone(); - - let blob_id = String::from_utf8(stdout).unwrap().trim().to_owned(); - - assert!(!blob_id.is_empty(), "upload-only must return a blob id"); - - // parse the uploaded blob — verify subject is present in output - let stdout = jmap(config) - .args(["emails", "parse", &blob_id]) - .assert() - .success() - .get_output() - .stdout - .clone(); - - let body = String::from_utf8(stdout).unwrap(); - - assert!( - body.contains("This is a test email"), - "parse: body missing from output" - ); - - // ── 3. THREADS ──────────────────────────────────────────────────────── - - // get thread — verify it references the imported email - let threads: Vec = parse_list(config, &["threads", "get", &thread_id], "threads"); - - assert_eq!(threads[0].id, thread_id, "thread: id mismatch"); - - assert!( - threads[0].email_ids.contains(&email_id), - "thread should reference the imported email id" - ); - - // ── 4. IDENTITY ─────────────────────────────────────────────────────── - - // create - jmap(config) - .args([ - "identity", - "create", - "Test", - &email, - "--text-signature", - "Sent by himalaya integration tests", - ]) - .assert() - .success(); - - // list — find by name and verify signature field - let identities: Vec = parse_list(config, &["identity", "get"], "identities"); - let identity = identities - .iter() - .find(|i| i.name == "Test") - .expect("created identity not found in list"); - - assert_eq!( - identity.text_signature.as_deref(), - Some("Sent by himalaya integration tests"), - "identity textSignature mismatch after create" - ); - - let identity_id = identity.id.clone(); - let identity_email = identity.email.clone(); - cleanup.identity_id = Some(identity_id.clone()); - - // update: rename - jmap(config) - .args(["identity", "update", &identity_id, "--name", "Test Updated"]) - .assert() - .success(); - - // list — verify rename - let identities: Vec = parse_list(config, &["identity", "get"], "identities"); - - assert!( - identities.iter().any(|i| i.name == "Test Updated"), - "updated identity name not found in list" - ); - - // ── 5. SUBMISSION ───────────────────────────────────────────────────── - - // import a draft addressed to the account itself - let draft = format!( - "From: {identity_email}\r\n\ - To: {identity_email}\r\n\ - Subject: Himalaya submission test\r\n\ - Date: Thu, 01 Jan 2026 00:00:00 +0000\r\n\ - MIME-Version: 1.0\r\n\ - Content-Type: text/plain; charset=utf-8\r\n\ - \r\n\ - Submission test by himalaya integration tests.\r\n" - ); - - jmap(config) - .args([ - "emails", - "import", - "--mailbox-id", - &mbox_id, - "--keyword", - "$draft", - ]) - .write_stdin(draft.as_bytes()) - .assert() - .success(); - - // query to get draft id — verify it is flagged $draft - let emails: Vec = parse_list( - config, - &[ - "emails", - "query", - "--mailbox", - &mbox_id, - "--has-keyword", - "$draft", - ], - "emails", - ); - - assert!(!emails.is_empty(), "draft email not found after import"); - - let draft_id = emails[0].id.clone().expect("draft id"); - - // create submission (send) — JSON mode returns the created submission(s) - let created: Vec = parse_list( - config, - &[ - "submission", - "create", - &draft_id, - "--identity-id", - &identity_id, - ], - "submissions", - ); - - assert!( - !created.is_empty(), - "expected at least one created submission in response" - ); - - let sub_id = created[0].id.clone().expect("submission id"); - - // get the submission by ID — EmailSubmission objects are short-lived on - // some servers (e.g. Fastmail) and may already be gone by the time we - // query; accept both found and not-found outcomes. - let got: Vec = - parse_list(config, &["submission", "get", &sub_id], "submissions"); - - if !got.is_empty() { - assert_eq!( - got[0].id.as_deref(), - Some(sub_id.as_str()), - "submission get: id mismatch" - ); - } - - // ── 6. COPY (optional) ──────────────────────────────────────────────── - - // Requires JMAP_FROM_ACCOUNT_ID env var (the server-side JMAP accountId, - // e.g. "u1d764051" for FastMail). Set it to enable this step. - if let Ok(from_account) = env::var("JMAP_FROM_ACCOUNT_ID") { - let before: Vec = parse_list( - config, - &["emails", "query", "--mailbox", &mbox_id], - "emails", - ); - let count_before = before.len(); - - jmap(config) - .args([ - "emails", - "copy", - &email_id, - "--from-account", - &from_account, - "--mailbox-id", - &mbox_id, - ]) - .assert() - .success(); - - let after: Vec = parse_list( - config, - &["emails", "query", "--mailbox", &mbox_id], - "emails", - ); - - assert!( - after.len() > count_before, - "email copy should increase mailbox count" - ); - } - - // ── 7. VACATION ─────────────────────────────────────────────────────── - - // Check whether the server supports vacation response. Servers that do - // not advertise the vacationresponse capability return a non-zero exit - // code; in that case we skip the vacation assertions entirely. - let vacation_supported = jmap_json(config) - .args(["vacation", "get"]) - .output() - .expect("failed to run vacation get") - .status - .success(); - - if vacation_supported { - // enable vacation response - jmap(config) - .args([ - "vacation", - "set", - "--enable", - "--subject", - "Away (himalaya test)", - "--text-body", - "I am away for himalaya integration testing.", - ]) - .assert() - .success(); - - // verify enabled and subject - let vacation: VacationResponse = parse_output(config, &["vacation", "get"]); - - assert!( - vacation.is_enabled, - "vacation should be enabled after set --enable" - ); - - assert_eq!( - vacation.subject.as_deref(), - Some("Away (himalaya test)"), - "vacation subject mismatch" - ); - - // disable vacation response - jmap(config) - .args(["vacation", "set", "--disable"]) - .assert() - .success(); - - // verify disabled - let vacation: VacationResponse = parse_output(config, &["vacation", "get"]); - - assert!( - !vacation.is_enabled, - "vacation should be disabled after set --disable" - ); - } - - // ── 8. RAW QUERY ────────────────────────────────────────────────────── - - // raw Mailbox/get — shape is dynamic, use Value; verify response is a non-empty array - let raw: Value = parse_output( - config, - &["query", r#"[["Mailbox/get", {"ids": null}, "c0"]]"#], - ); - - let responses = raw - .get("method_responses") - .and_then(|v| v.as_array()) - .expect("method_responses should be an array in raw query output"); - - assert!( - !responses.is_empty(), - "raw query response should be a non-empty array" - ); - - // cleanup via Drop (identity delete + mailbox destroy --purge) -} diff --git a/tests/common/shared.rs b/tests/common/shared.rs deleted file mode 100644 index 1576cca4..00000000 --- a/tests/common/shared.rs +++ /dev/null @@ -1,403 +0,0 @@ -use std::{ - fs, - path::Path, - time::{SystemTime, UNIX_EPOCH}, -}; - -use assert_cmd::Command; -use serde_json::Value; -use tempfile::TempDir; - -/// Cleanup resources allocated by the shared API test flow. The shared -/// API has no `mailboxes create/delete` of its own, so scaffolding goes -/// through the protocol-specific `imap mailboxes` commands. The shared -/// surface under test is everything else (`mailboxes list`, -/// `envelopes`, `messages`, `flags`, `attachments`). -struct Cleanup<'a> { - config: &'a Path, - mbox_name: Option, - mbox_name_2: Option, -} - -impl Drop for Cleanup<'_> { - fn drop(&mut self) { - if let Some(name) = &self.mbox_name { - let _ = imap_scaffold(self.config) - .args(["mailboxes", "delete", name]) - .output(); - } - - if let Some(name) = &self.mbox_name_2 { - let _ = imap_scaffold(self.config) - .args(["mailboxes", "delete", name]) - .output(); - } - } -} - -/// `himalaya ` — invokes a shared subcommand. No `imap` / -/// `jmap` / `maildir` prefix; dispatch is driven by `--backend auto` -/// (configured account has IMAP only, so the shared command resolves -/// to the IMAP backend). -fn shared(config: &Path) -> Command { - let mut cmd = Command::cargo_bin("himalaya").unwrap(); - cmd.args(["-c", config.to_str().unwrap()]); - cmd -} - -/// Same as `shared` but with `--json` so we can parse output. -fn shared_json(config: &Path) -> Command { - let mut cmd = Command::cargo_bin("himalaya").unwrap(); - cmd.args(["--json", "-c", config.to_str().unwrap()]); - cmd -} - -/// Protocol-specific `himalaya imap …` used only for scaffolding -/// (create/delete mailboxes). The shared API has no equivalents. -fn imap_scaffold(config: &Path) -> Command { - let mut cmd = Command::cargo_bin("himalaya").unwrap(); - cmd.args(["-c", config.to_str().unwrap(), "imap"]); - cmd -} - -/// Shared API integration test suite. -/// -/// Exercises every command in the shared surface in a single ordered -/// flow against a backend that already works (here: Fastmail over -/// IMAP). The shared API has no mailbox create/delete, so the test -/// scaffolds setup/teardown via `imap mailboxes …` and exercises -/// every other shared subcommand on top of those mailboxes. -/// -/// `messages send` is intentionally skipped: it needs SMTP or JMAP -/// and is covered by the dedicated `fastmail-smtp` / `fastmail-jmap` -/// test suites. -pub fn run(config: &Path, email: impl ToString) { - let email = email.to_string(); - - let ts = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis(); - - let mbox_name = format!("himalaya-shared-{ts}"); - let mbox_name_2 = format!("himalaya-shared-{ts}-copy"); - - let mut cleanup = Cleanup { - config, - mbox_name: None, - mbox_name_2: None, - }; - - // ── scaffold: two test mailboxes via the IMAP protocol-specific - // commands; the shared API has no create/delete. - imap_scaffold(config) - .args(["mailboxes", "create", &mbox_name]) - .assert() - .success(); - cleanup.mbox_name = Some(mbox_name.clone()); - - imap_scaffold(config) - .args(["mailboxes", "create", &mbox_name_2]) - .assert() - .success(); - cleanup.mbox_name_2 = Some(mbox_name_2.clone()); - - // ── 1. mailboxes list ──────────────────────────────────────────── - - shared(config) - .args(["mailboxes", "list"]) - .assert() - .success(); - - // `--counts` issues an extra STATUS per mailbox on IMAP (slow on - // big accounts, but the test account is small). - shared(config) - .args(["mailboxes", "list", "--counts"]) - .assert() - .success(); - - // ── 2. messages add (with attachment) ──────────────────────────── - - let eml = build_eml_with_attachment(&email); - - shared(config) - .args([ - "messages", - "add", - "--mailbox", - &mbox_name, - "--flag", - "draft", - ]) - .write_stdin(eml.as_bytes()) - .assert() - .success(); - - // ── 3. envelopes list (plain, JSON for UID extraction) ─────────── - - let stdout = shared_json(config) - .args(["envelopes", "list", &mbox_name]) - .assert() - .success() - .get_output() - .stdout - .clone(); - - let envelopes: Vec = serde_json::from_slice::(&stdout) - .unwrap_or_else(|e| { - panic!( - "failed to parse envelope list output: {e}\nstdout: {}", - String::from_utf8_lossy(&stdout) - ) - }) - .get("envelopes") - .and_then(|v| serde_json::from_value(v.clone()).ok()) - .unwrap_or_else(|| { - panic!( - "missing `envelopes` key in output: {}", - String::from_utf8_lossy(&stdout) - ) - }); - - assert_eq!( - envelopes.len(), - 1, - "expected exactly one envelope after `messages add`" - ); - - let uid = envelopes[0]["id"] - .as_str() - .map(|s| s.to_owned()) - .or_else(|| envelopes[0]["uid"].as_u64().map(|n| n.to_string())) - .expect("envelope should expose an id"); - - // ── 4. envelopes list flag variants ────────────────────────────── - - shared(config) - .args(["envelopes", "list", &mbox_name, "--has-attachment"]) - .assert() - .success(); - - shared(config) - .args(["envelopes", "list", &mbox_name, "--recipient"]) - .assert() - .success(); - - shared(config) - .args([ - "envelopes", - "list", - &mbox_name, - "--page", - "1", - "--page-size", - "10", - ]) - .assert() - .success(); - - // ── 5. messages get (default + --raw) ──────────────────────────── - - shared(config) - .args(["messages", "get", "--mailbox", &mbox_name, &uid]) - .assert() - .success(); - - let raw_stdout = shared(config) - .args(["messages", "get", "--mailbox", &mbox_name, &uid, "--raw"]) - .assert() - .success() - .get_output() - .stdout - .clone(); - let raw_body = String::from_utf8_lossy(&raw_stdout); - assert!( - raw_body.contains("Subject:"), - "`messages get --raw` should emit RFC 5322 bytes incl. Subject header, got: {raw_body}" - ); - - // ── 6. messages compose (stdout, no --send) ────────────────────── - - let compose_stdout = shared(config) - .args([ - "messages", - "compose", - "--from", - &email, - "--to", - &email, - "--subject", - "Himalaya shared compose", - "--body", - "compose body", - ]) - .assert() - .success() - .get_output() - .stdout - .clone(); - let compose_body = String::from_utf8_lossy(&compose_stdout); - assert!( - compose_body.contains("Subject: Himalaya shared compose"), - "`messages compose` stdout should contain the assembled headers, got: {compose_body}" - ); - - // ── 7. flags add / set / remove ────────────────────────────────── - - shared(config) - .args(["flags", "add", &mbox_name, &uid, "--flag", "seen"]) - .assert() - .success(); - - shared(config) - .args([ - "flags", "set", &mbox_name, &uid, "--flag", "seen", "--flag", "flagged", - ]) - .assert() - .success(); - - shared(config) - .args(["flags", "remove", &mbox_name, &uid, "--flag", "flagged"]) - .assert() - .success(); - - // ── 8. attachments list (default + --inline) ───────────────────── - - let att_stdout = shared_json(config) - .args(["attachments", "list", &mbox_name, &uid]) - .assert() - .success() - .get_output() - .stdout - .clone(); - let att_body = String::from_utf8_lossy(&att_stdout); - assert!( - att_body.contains("hello.txt"), - "`attachments list` should surface the test attachment filename, got: {att_body}" - ); - - shared(config) - .args(["attachments", "list", &mbox_name, &uid, "--inline"]) - .assert() - .success(); - - // ── 9. attachments download ────────────────────────────────────── - - let dl_dir = TempDir::new().unwrap(); - shared(config) - .args([ - "attachments", - "download", - &mbox_name, - &uid, - "--dir", - dl_dir.path().to_str().unwrap(), - ]) - .assert() - .success(); - let downloaded = dl_dir.path().join("hello.txt"); - assert!( - downloaded.exists(), - "expected attachment to land at {}, dir listing: {:?}", - downloaded.display(), - fs::read_dir(dl_dir.path()) - .unwrap() - .map(|e| e.unwrap().path()) - .collect::>() - ); - - // ── 10. messages copy ──────────────────────────────────────────── - - shared(config) - .args([ - "messages", - "copy", - "--from", - &mbox_name, - "--to", - &mbox_name_2, - &uid, - ]) - .assert() - .success(); - - let stdout = shared_json(config) - .args(["envelopes", "list", &mbox_name_2]) - .assert() - .success() - .get_output() - .stdout - .clone(); - let dest_envelopes: Vec = serde_json::from_slice::(&stdout) - .unwrap_or_else(|e| { - panic!( - "failed to parse destination envelope list: {e}\nstdout: {}", - String::from_utf8_lossy(&stdout) - ) - }) - .get("envelopes") - .and_then(|v| serde_json::from_value(v.clone()).ok()) - .unwrap_or_else(|| { - panic!( - "missing `envelopes` key in destination output: {}", - String::from_utf8_lossy(&stdout) - ) - }); - assert_eq!( - dest_envelopes.len(), - 1, - "expected one message in copy destination" - ); - let dest_uid = dest_envelopes[0]["id"] - .as_str() - .map(|s| s.to_owned()) - .or_else(|| dest_envelopes[0]["uid"].as_u64().map(|n| n.to_string())) - .expect("destination envelope should expose an id"); - - // ── 11. messages move ──────────────────────────────────────────── - - shared(config) - .args([ - "messages", - "move", - "--from", - &mbox_name_2, - "--to", - &mbox_name, - &dest_uid, - ]) - .assert() - .success(); - - // cleanup via Drop - let _ = cleanup; -} - -/// Build a small multipart/mixed RFC 5322 message with a plain-text -/// body and a single named text attachment (`hello.txt`). Used to -/// exercise `attachments list` and `attachments download`. -fn build_eml_with_attachment(email: &str) -> String { - let boundary = "HIMALAYA-SHARED-BOUNDARY"; - [ - &format!("From: Himalaya Shared <{email}>"), - &format!("To: Himalaya Shared <{email}>"), - "Subject: Himalaya shared API integration test", - "Date: Thu, 01 Jan 2026 00:00:00 +0000", - "MIME-Version: 1.0", - &format!(r#"Content-Type: multipart/mixed; boundary="{boundary}""#), - "", - &format!("--{boundary}"), - "Content-Type: text/plain; charset=utf-8", - "", - "This is the body for the shared API integration test.", - "", - &format!("--{boundary}"), - r#"Content-Type: text/plain; charset=utf-8; name="hello.txt""#, - r#"Content-Disposition: attachment; filename="hello.txt""#, - "", - "Attachment contents.", - "", - &format!("--{boundary}--"), - ] - .join("\r\n") -} diff --git a/tests/common/smtp.rs b/tests/common/smtp.rs deleted file mode 100644 index db8fc8a5..00000000 --- a/tests/common/smtp.rs +++ /dev/null @@ -1,38 +0,0 @@ -use std::path::Path; - -use assert_cmd::Command; - -/// Builds a `himalaya smtp` command with the given config path. -fn smtp(config: &Path) -> Command { - let mut cmd = Command::cargo_bin("himalaya").unwrap(); - cmd.args(["-c", config.to_str().unwrap(), "smtp"]); - cmd -} - -/// Shared SMTP integration test suite. -/// -/// Exercises the send command. Pass a path to a valid TOML config file -/// with a default SMTP account configured. -pub fn run(config: &Path, email: impl ToString) { - let email = email.to_string(); - - let eml = [ - &format!("From: Himalaya Test <{email}>"), - &format!("To: Himalaya Test <{email}>"), - "Subject: Himalaya SMTP integration test", - "Date: Thu, 01 Jan 2026 00:00:00 +0000", - "MIME-Version: 1.0", - "Content-Type: text/plain; charset=utf-8", - "", - "This is a test email for himalaya SMTP integration tests.", - ] - .join("\r\n"); - - // ── SEND message ────────────────────────────────────────────────────── - - smtp(config) - .args(["messages", "send"]) - .write_stdin(eml.as_bytes()) - .assert() - .success(); -} diff --git a/tests/fastmail-imap.rs b/tests/fastmail-imap.rs deleted file mode 100644 index abd1dfb7..00000000 --- a/tests/fastmail-imap.rs +++ /dev/null @@ -1,42 +0,0 @@ -#![cfg(feature = "imap")] - -#[path = "common/imap.rs"] -mod imap; -#[path = "common/shared.rs"] -mod shared; - -use std::{env, io::Write}; - -use tempfile::NamedTempFile; - -fn write_imap_config() -> (NamedTempFile, String) { - let email = env::var("FASTMAIL_EMAIL").expect("FASTMAIL_EMAIL not set"); - let app_password = env::var("FASTMAIL_APP_PASSWORD").expect("FASTMAIL_APP_PASSWORD not set"); - - let mut config = NamedTempFile::new().unwrap(); - let config_tpl = format!( - r#"[accounts.fastmail] -default = true -imap.url = "imaps://imap.fastmail.com" -imap.sasl.plain.authcid = "{email}" -imap.sasl.plain.passwd.raw = "{app_password}""# - ); - - config.write_all(config_tpl.as_bytes()).unwrap(); - - (config, email) -} - -#[test] -#[ignore = "requires FASTMAIL_{EMAIL,APP_PASSWORD} env vars and --ignored"] -fn fastmail_imap() { - let (config, email) = write_imap_config(); - imap::run(config.path(), email); -} - -#[test] -#[ignore = "requires FASTMAIL_{EMAIL,APP_PASSWORD} env vars and --ignored"] -fn fastmail_shared_imap() { - let (config, email) = write_imap_config(); - shared::run(config.path(), email); -} diff --git a/tests/fastmail-jmap.rs b/tests/fastmail-jmap.rs deleted file mode 100644 index 9f770a34..00000000 --- a/tests/fastmail-jmap.rs +++ /dev/null @@ -1,27 +0,0 @@ -#![cfg(feature = "jmap")] - -#[path = "common/jmap.rs"] -mod jmap; - -use std::{env, io::Write}; - -use tempfile::NamedTempFile; - -#[test] -#[ignore = "requires FASTMAIL_{EMAIL,API_TOKEN} env vars and --ignored"] -fn fastmail_jmap() { - let email = env::var("FASTMAIL_EMAIL").expect("FASTMAIL_EMAIL env var"); - let token = env::var("FASTMAIL_API_TOKEN").expect("FASTMAIL_API_TOKEN env var"); - - let mut config = NamedTempFile::new().unwrap(); - let config_tpl = format!( - r#"[accounts.fastmail] -default = true -jmap.server = "https://api.fastmail.com/jmap/session" -jmap.auth.bearer.token.raw = "{token}""# - ); - - config.write(&config_tpl.into_bytes()).unwrap(); - - jmap::run(config.path(), email); -} diff --git a/tests/fastmail-smtp.rs b/tests/fastmail-smtp.rs deleted file mode 100644 index d45096a6..00000000 --- a/tests/fastmail-smtp.rs +++ /dev/null @@ -1,28 +0,0 @@ -#![cfg(feature = "smtp")] - -#[path = "common/smtp.rs"] -mod smtp; - -use std::{env, io::Write}; - -use tempfile::NamedTempFile; - -#[test] -#[ignore = "requires FASTMAIL_{EMAIL,APP_PASSWORD} env vars and --ignored"] -fn fastmail_smtp() { - let email = env::var("FASTMAIL_EMAIL").expect("FASTMAIL_EMAIL not set"); - let app_password = env::var("FASTMAIL_APP_PASSWORD").expect("FASTMAIL_APP_PASSWORD not set"); - - let mut config = NamedTempFile::new().unwrap(); - let config_tpl = format!( - r#"[accounts.fastmail] -default = true -smtp.url = "smtps://smtp.fastmail.com" -smtp.sasl.plain.authcid = "{email}" -smtp.sasl.plain.passwd.raw = "{app_password}""# - ); - - config.write_all(config_tpl.as_bytes()).unwrap(); - - smtp::run(config.path(), email); -} diff --git a/tests/stalwart-imap.rs b/tests/stalwart-imap.rs deleted file mode 100644 index fb38673a..00000000 --- a/tests/stalwart-imap.rs +++ /dev/null @@ -1,25 +0,0 @@ -#![cfg(feature = "imap")] - -#[path = "common/imap.rs"] -mod imap; - -use std::io::Write; - -use tempfile::NamedTempFile; - -#[test] -#[ignore = "requires a running Stalwart instance and --ignored"] -fn stalwart_imap() { - let mut config = NamedTempFile::new().unwrap(); - let config_tpl = format!( - r#"[accounts.stalwart] -default = true -imap.url = "imap://localhost" -imap.sasl.plain.authcid = "test" -imap.sasl.plain.passwd.raw = "test""# - ); - - config.write_all(config_tpl.as_bytes()).unwrap(); - - imap::run(config.path(), "test@pimalaya.org"); -} diff --git a/tests/stalwart-jmap.rs b/tests/stalwart-jmap.rs deleted file mode 100644 index e4a5c9e6..00000000 --- a/tests/stalwart-jmap.rs +++ /dev/null @@ -1,25 +0,0 @@ -#![cfg(feature = "jmap")] - -#[path = "common/jmap.rs"] -mod jmap; - -use std::io::Write; - -use tempfile::NamedTempFile; - -#[test] -#[ignore = "requires a running Stalwart instance and --ignored"] -fn stalwart_jmap() { - let mut config = NamedTempFile::new().unwrap(); - let config_tpl = format!( - r#"[accounts.stalwart] -default = true -jmap.server = "http://localhost:8080/jmap/session" -jmap.auth.basic.username = "test" -jmap.auth.basic.password.raw = "test""# - ); - - config.write(&config_tpl.into_bytes()).unwrap(); - - jmap::run(config.path(), "test@pimalaya.org"); -} diff --git a/tests/stalwart-smtp.rs b/tests/stalwart-smtp.rs deleted file mode 100644 index 2c271639..00000000 --- a/tests/stalwart-smtp.rs +++ /dev/null @@ -1,23 +0,0 @@ -#![cfg(feature = "smtp")] - -#[path = "common/smtp.rs"] -mod smtp; - -use std::io::Write; - -use tempfile::NamedTempFile; - -#[test] -#[ignore = "requires a running Stalwart instance and --ignored"] -fn stalwart_smtp() { - let mut config = NamedTempFile::new().unwrap(); - let config_tpl = r#"[accounts.stalwart] -default = true -smtp.url = "smtp://localhost" -smtp.sasl.plain.authcid = "test" -smtp.sasl.plain.passwd.raw = "test""#; - - config.write_all(config_tpl.as_bytes()).unwrap(); - - smtp::run(config.path(), "test@pimalaya.org"); -} diff --git a/tests/stalwart.sh b/tests/stalwart.sh deleted file mode 100755 index bd6b136c..00000000 --- a/tests/stalwart.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash - -set -eu - -docker run -d --name himalaya-tests --rm -p 8080:8080 -p 25:25 -p 143:143 stalwartlabs/stalwart:v0.15.5-alpine - -sleep 1 -admin_password=$(docker logs himalaya-tests 2>&1 | grep -oP "(?<=with password ')[^']+") - -curl -X POST \ - -u "admin:${admin_password}" \ - -H 'Content-Type: application/json' \ - -d '{"type":"domain","name":"pimalaya.org"}' \ - http://localhost:8080/api/principal - -curl -X POST \ - -u "admin:${admin_password}" \ - -H 'Content-Type: application/json' \ - -d '{"type":"individual","name":"test","emails":["test@pimalaya.org"],"secrets":["test"],"roles":["user"]}' \ - http://localhost:8080/api/principal