tests: clean unused and fix deny

This commit is contained in:
Clément DOUIN
2026-05-20 00:27:04 +02:00
parent 58d13804e5
commit 6dec8f2832
23 changed files with 36 additions and 1767 deletions
+9
View File
@@ -0,0 +1,9 @@
name: Audit
on:
push:
jobs:
audit:
uses: pimalaya/nix/.github/workflows/audit.yml@master
secrets: inherit
-25
View File
@@ -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 }}
+1 -2
View File
@@ -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
-18
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+7
View File
@@ -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
View File
@@ -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": {
+2 -2
View File
@@ -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";
+2 -1
View File
@@ -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
];
})
-10
View File
@@ -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!
-5
View File
@@ -1,5 +0,0 @@
From: clement.douin@posteo.net
To: pimalaya.org@gmail.com
Subject: Test
Hello!
-338
View File
@@ -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)
}
-613
View File
@@ -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)
}
-403
View File
@@ -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")
}
-38
View File
@@ -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();
}
-42
View File
@@ -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);
}
-27
View File
@@ -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);
}
-28
View File
@@ -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);
}
-25
View File
@@ -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");
}
-25
View File
@@ -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");
}
-23
View File
@@ -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");
}
-20
View File
@@ -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