Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: CI
on:
pull_request:
branches: [master]
concurrency:
group: ci-${{ github.head_ref || github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
jobs:
check:
name: fmt + clippy + test
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
- run: cargo fmt --all -- --check
- run: cargo clippy --all-targets --locked -- -D warnings
- run: cargo test --locked
55 changes: 55 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Release
on:
push:
tags: ["v*"]
permissions:
contents: write # required to create the release + upload assets
env:
CARGO_TERM_COLOR: always
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- { os: ubuntu-latest, target: x86_64-unknown-linux-gnu, suffix: linux-amd64 }
- { os: macos-latest, target: aarch64-apple-darwin, suffix: macos-arm64 }
- { os: macos-latest, target: x86_64-apple-darwin, suffix: macos-amd64 }
- { os: windows-latest, target: x86_64-pc-windows-msvc, suffix: windows-amd64, ext: ".exe" }
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
- run: cargo build --release --locked --target ${{ matrix.target }}
- name: Stage artifact
shell: bash
run: |
v="${GITHUB_REF_NAME#v}"
src="target/${{ matrix.target }}/release/mail-cli${{ matrix.ext }}"
dst="mail-cli-${v}-${{ matrix.suffix }}${{ matrix.ext }}"
mkdir -p dist && cp "$src" "dist/$dst"
- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.suffix }}
path: dist/*
publish:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Verify tag matches Cargo.toml version
run: |
v="${GITHUB_REF_NAME#v}"
cargo_v=$(grep -m1 '^version' Cargo.toml | cut -d'"' -f2)
[ "$v" = "$cargo_v" ] || { echo "tag $v != Cargo.toml $cargo_v"; exit 1; }
- uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- uses: softprops/action-gh-release@v2
with:
files: dist/*
generate_release_notes: true
14 changes: 9 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,9 +290,7 @@ fn run() -> Result<()> {
html,
raw,
} => cmd_read(cli.json, &folder, selector, html, raw),
Command::Attachments { folder, selector } => {
cmd_attachments(cli.json, &folder, selector)
}
Command::Attachments { folder, selector } => cmd_attachments(cli.json, &folder, selector),
Command::Download {
folder,
selector,
Expand Down Expand Up @@ -329,7 +327,10 @@ fn resolve(
let n = idx as usize;
let uids = imap::newest_uids(session, folder, n, None)?;
let uid = uids.get(n - 1).copied().ok_or_else(|| {
anyhow!("no message #{idx} in {folder} (only {} present)", uids.len())
anyhow!(
"no message #{idx} in {folder} (only {} present)",
uids.len()
)
})?;
Ok((uid, Some(n)))
}
Expand Down Expand Up @@ -523,7 +524,10 @@ mod tests {

#[test]
fn selector_parses_index() {
assert!(matches!("3".parse::<Selector>().unwrap(), Selector::Index(3)));
assert!(matches!(
"3".parse::<Selector>().unwrap(),
Selector::Index(3)
));
}

#[test]
Expand Down
5 changes: 4 additions & 1 deletion src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,10 @@ pub fn attachments_human(uid: u32, index: Option<usize>, atts: &[Attachment]) {
println!("(no attachments)");
return;
}
println!("{:>2} {:<30} {:<28} {:>10}", "K", "FILENAME", "MIME", "SIZE");
println!(
"{:>2} {:<30} {:<28} {:>10}",
"K", "FILENAME", "MIME", "SIZE"
);
for a in atts {
println!(
"{:>2} {:<30} {:<28} {:>10}",
Expand Down
9 changes: 4 additions & 5 deletions src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ fn parse(raw: &[u8]) -> Result<Message<'_>> {

/// Join address-list headers into a display string like `Name <a@b>, c@d`.
fn addrs(list: Option<&mail_parser::Address<'_>>) -> String {
let Some(addr) = list else { return String::new() };
let Some(addr) = list else {
return String::new();
};
let mut out = Vec::new();
for a in addr.iter() {
let email = a.address().unwrap_or_default();
Expand Down Expand Up @@ -108,10 +110,7 @@ fn collect_attachments(msg: &Message<'_>) -> Vec<Attachment> {
let bytes = part.contents().to_vec();
Attachment {
index: i + 1,
filename: part
.attachment_name()
.unwrap_or("unnamed")
.to_string(),
filename: part.attachment_name().unwrap_or("unnamed").to_string(),
mime: part
.content_type()
.map(|ct| match ct.subtype() {
Expand Down
20 changes: 13 additions & 7 deletions src/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,16 @@ impl Criteria {

// Dates (converted to IMAP's dd-Mon-yyyy at this boundary).
if let Some(d) = &self.since {
terms.push(format!("SINCE {}", imap_date(d).map_err(rename("--since"))?));
terms.push(format!(
"SINCE {}",
imap_date(d).map_err(rename("--since"))?
));
}
if let Some(d) = &self.before {
terms.push(format!("BEFORE {}", imap_date(d).map_err(rename("--before"))?));
terms.push(format!(
"BEFORE {}",
imap_date(d).map_err(rename("--before"))?
));
}
if let Some(d) = &self.on {
terms.push(format!("ON {}", imap_date(d).map_err(rename("--on"))?));
Expand Down Expand Up @@ -142,7 +148,10 @@ fn imap_date(iso: &str) -> Result<String> {
if !(1..=days_in_month(year, month)).contains(&day) {
bail!("day out of range in {iso:?}");
}
Ok(format!("{day:02}-{}-{year:04}", MONTHS[(month - 1) as usize]))
Ok(format!(
"{day:02}-{}-{year:04}",
MONTHS[(month - 1) as usize]
))
}

/// Days in a given month, honoring Gregorian leap years.
Expand Down Expand Up @@ -217,10 +226,7 @@ mod tests {
body: Some(r#"say "hi"\bye"#.into()),
..Default::default()
};
assert_eq!(
c.to_imap_query().unwrap(),
r#"BODY "say \"hi\"\\bye""#
);
assert_eq!(c.to_imap_query().unwrap(), r#"BODY "say \"hi\"\\bye""#);
}

#[test]
Expand Down
15 changes: 9 additions & 6 deletions src/skill.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ const ASSETS: &[(&str, &str)] = &[
"references/reference.md",
include_str!("../skill/references/reference.md"),
),
("evals/evals.json", include_str!("../skill/evals/evals.json")),
(
"evals/evals.json",
include_str!("../skill/evals/evals.json"),
),
];

/// The default install root: `~/.agents/skills/mail-cli`.
Expand All @@ -33,10 +36,7 @@ pub fn default_dir() -> Result<PathBuf> {
let home = std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.context("cannot locate home directory: neither HOME nor USERPROFILE is set")?;
Ok(Path::new(&home)
.join(".agents")
.join("skills")
.join(NAME))
Ok(Path::new(&home).join(".agents").join("skills").join(NAME))
}

/// Write the bundled skill into `dir`, creating parent directories as needed.
Expand All @@ -52,7 +52,10 @@ pub fn install(dir: &Path, force: bool) -> Result<Vec<PathBuf>> {
dest.push(part);
}
if dest.exists() && !force {
bail!("{} already exists; pass --force to overwrite", dest.display());
bail!(
"{} already exists; pass --force to overwrite",
dest.display()
);
}
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent)
Expand Down
28 changes: 22 additions & 6 deletions tests/search_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,8 @@ fn seed(
internal_date: DateTime<FixedOffset>,
flags: &[Flag<'static>],
) {
let raw = format!(
"From: {from}\r\nTo: rcpt@example.com\r\nSubject: {subject}\r\n\r\n{body}\r\n"
);
let raw =
format!("From: {from}\r\nTo: rcpt@example.com\r\nSubject: {subject}\r\n\r\n{body}\r\n");
let mut cmd = session.append("INBOX", raw.as_bytes());
cmd.internal_date(internal_date);
for f in flags {
Expand Down Expand Up @@ -124,9 +123,23 @@ fn from_and_subject_and_flags() {
let mut s = session_for(&user);

let d = dt("2026-03-10T12:00:00+00:00");
seed(&mut s, "alice@example.com", "Invoice #7", "body one", d, &[Flag::Seen]);
seed(
&mut s,
"alice@example.com",
"Invoice #7",
"body one",
d,
&[Flag::Seen],
);
seed(&mut s, "bob@example.com", "Lunch plans", "body two", d, &[]);
seed(&mut s, "alice@example.com", "Weekly report", "body three", d, &[]);
seed(
&mut s,
"alice@example.com",
"Weekly report",
"body three",
d,
&[],
);
let alice_seen = uid_of(&mut s, "Invoice #7");
let bob_unseen = uid_of(&mut s, "Lunch plans");
let alice_unseen = uid_of(&mut s, "Weekly report");
Expand Down Expand Up @@ -244,7 +257,10 @@ fn size_bounds() {
};
let got = search_uids(&mut s, &larger);
assert!(got.contains(&big), "big message should match LARGER 2000");
assert!(!got.contains(&small), "small message should NOT match LARGER 2000");
assert!(
!got.contains(&small),
"small message should NOT match LARGER 2000"
);

let _ = s.logout();
}
Loading