mirror of
https://github.com/pimalaya/himalaya.git
synced 2026-06-15 11:27:53 +08:00
tests: clean unused and fix deny
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
name: Audit
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
audit:
|
||||
uses: pimalaya/nix/.github/workflows/audit.yml@master
|
||||
secrets: inherit
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
+2
-1
@@ -3,7 +3,8 @@ name = "himalaya"
|
||||
description = "CLI to manage emails"
|
||||
version = "2.0.0-alpha.1"
|
||||
authors = ["soywod <pimalaya.org@posteo.net>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.87"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
categories = ["command-line-utilities", "email"]
|
||||
keywords = ["cli", "email", "imap", "maildir", "smtp"]
|
||||
|
||||
-108
@@ -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 (`<id>… --from <mailbox> --to <mailbox>`) |
|
||||
| `messages move` | done (`<id>… --from <mailbox> --to <mailbox>`) |
|
||||
| `messages add` | done (raw RFC 5322 in; `--mailbox <NAME>` + 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 <PATH>`; 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 <args> | 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 [<name>]` → 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 <id>` / `--forward <id>` 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** — `<id>… --from <mailbox> --to <mailbox>`,
|
||||
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.
|
||||
@@ -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"]
|
||||
Generated
+13
-13
@@ -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": {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
];
|
||||
})
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
MIME-Version: 1.0
|
||||
From: <clement.douin@posteo.net>
|
||||
To: <pimalaya.org@gmail.com>
|
||||
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!
|
||||
@@ -1,5 +0,0 @@
|
||||
From: clement.douin@posteo.net
|
||||
To: pimalaya.org@gmail.com
|
||||
Subject: Test
|
||||
|
||||
Hello!
|
||||
@@ -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<String>,
|
||||
/// Secondary test mailbox name (copy/move destination) — deleted on drop.
|
||||
mbox_name_2: Option<String>,
|
||||
}
|
||||
|
||||
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<Value> = serde_json::from_slice::<Value>(&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<Value> = serde_json::from_slice::<Value>(&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<Value> = serde_json::from_slice::<Value>(&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)
|
||||
}
|
||||
@@ -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<String>,
|
||||
/// Identity created during the test.
|
||||
identity_id: Option<String>,
|
||||
}
|
||||
|
||||
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<T: DeserializeOwned>(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<T>`.
|
||||
fn parse_list<T: DeserializeOwned>(config: &Path, args: &[&str], key: &str) -> Vec<T> {
|
||||
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<Mailbox> = 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<Mailbox> = 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<Mailbox> = 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<Mailbox> = 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<Email> = 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<Email> = 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<Email> = 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<Email> = 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<Thread> = 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<Identity> = 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<Identity> = 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<Email> = 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<EmailSubmission> = 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<EmailSubmission> =
|
||||
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<Email> = 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<Email> = 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)
|
||||
}
|
||||
@@ -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<String>,
|
||||
mbox_name_2: Option<String>,
|
||||
}
|
||||
|
||||
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 <SHARED>` — 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<Value> = serde_json::from_slice::<Value>(&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::<Vec<_>>()
|
||||
);
|
||||
|
||||
// ── 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<Value> = serde_json::from_slice::<Value>(&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")
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user