diff --git a/README.md b/README.md index 02d1965..9636e03 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,15 @@ inboxapi get-emails --limit 5 --human inboxapi get-email "" ``` +### `delete-email` + +Soft-deletes a received email by message ID. It prompts for confirmation by default, and scripted or piped use must pass `--force`. + +```bash +inboxapi delete-email "" +inboxapi delete-email "" --force +``` + ### `search-emails` ```bash diff --git a/docs/help.md b/docs/help.md index 9ae9d17..b96f758 100644 --- a/docs/help.md +++ b/docs/help.md @@ -20,6 +20,7 @@ Agents with shell access can also use CLI subcommands directly — no MCP or JSO inboxapi send-email --to user@example.com --subject "Hello" --body "Hi there" inboxapi send-email --to user@example.com --subject "Newsletter" --body-file ./body.txt --html-body-file ./newsletter.html inboxapi get-emails --limit 5 --human +inboxapi delete-email "" --force inboxapi search-emails --subject "invoice" inboxapi help ``` @@ -43,6 +44,7 @@ Authentication is handled automatically by the CLI proxy. You do not need to cre | `help` | Show this help text | | `get_emails` | Fetch emails from your inbox | | `get_email` | Get a single email by ID | +| `delete_email` | Soft-delete a received email by Message-ID | | `get_last_email` | Get the most recent email | | `get_email_count` | Count emails in your inbox | | `search_emails` | Search emails by query | @@ -73,6 +75,21 @@ Your InboxAPI email address (from `whoami`) is **the agent's own inbox** for rec --- +## Deleting Received Email + +Use `delete_email` to hide a received message from the normal inbox views. This is a soft delete: the message is removed from `get_last_email`, `get_emails`, `get_email`, `search_emails`, `get_email_count`, and `get_thread`, but there is no restore or trash command yet. + +The CLI prompts for confirmation by default. If stdin is not a TTY, you must pass `--force` or it will error instead of deleting. + +CLI example: + +```bash +inboxapi delete-email "" +inboxapi delete-email "" --force +``` + +--- + ## Credential Safety **NEVER send tokens, credentials, or secrets via email.** This includes: diff --git a/src/main.rs b/src/main.rs index 8dee3b7..a4722ed 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::cmp::Ordering; use std::collections::HashSet; -use std::io::Read; +use std::io::{IsTerminal, Read}; use std::path::{Path, PathBuf}; use std::time::{Duration, Instant}; use tokio::io::{stdin, stdout, AsyncBufReadExt, AsyncWriteExt, BufReader}; @@ -150,6 +150,14 @@ enum Commands { /// The message ID to retrieve message_id: String, }, + /// Soft-delete a received email by message ID + DeleteEmail { + /// The message ID to delete + message_id: String, + /// Skip the destructive confirmation prompt + #[arg(long)] + force: bool, + }, /// Search emails by sender, subject, or date range SearchEmails { /// Filter by sender (substring match, case-insensitive) @@ -545,6 +553,31 @@ fn prompt_line(prompt: &str) -> Result { Ok(input.trim().to_string()) } +fn delete_email_prompt_mode(force: bool, stdin_is_tty: bool) -> Result { + if force { + return Ok(false); + } + + if !stdin_is_tty { + return Err(anyhow!( + "Refusing to delete email non-interactively without --force" + )); + } + + Ok(true) +} + +fn confirm_delete_email(force: bool, message_id: &str) -> Result { + if !delete_email_prompt_mode(force, std::io::stdin().is_terminal())? { + return Ok(true); + } + + Ok(prompt_yes_no(&format!( + "WARNING: This will hide {} from your inbox. Continue? [y/N] ", + message_id + ))) +} + fn reset_credentials() -> Result<()> { let creds = match load_credentials() { Ok(c) => c, @@ -1398,6 +1431,7 @@ async fn main() -> Result<()> { Some(Commands::SendEmail { .. }) | Some(Commands::GetEmails { .. }) | Some(Commands::GetEmail { .. }) + | Some(Commands::DeleteEmail { .. }) | Some(Commands::SearchEmails { .. }) | Some(Commands::GetAttachment { .. }) | Some(Commands::SendReply { .. }) @@ -2097,6 +2131,17 @@ fn format_human_output(tool_name: &str, text: &str) -> String { text.to_string() } } + "delete_email" => { + if let Ok(data) = serde_json::from_str::(text) { + let msg_id = data["message_id"] + .as_str() + .or_else(|| data["messageId"].as_str()) + .unwrap_or("unknown"); + format!("Email deleted: {}", msg_id) + } else { + format!("Email deleted.\n{}", text) + } + } "send_reply" => { if let Ok(data) = serde_json::from_str::(text) { let msg_id = data["message_id"] @@ -2303,6 +2348,7 @@ Commands: send-email Send an email (supports --attachment and --attachment-ref) get-emails List inbox emails get-email Get a single email by message ID + delete-email Soft-delete a received email by message ID get-last-email Get the most recent email get-email-count Get inbox email count get-sent-emails List sent emails @@ -2337,6 +2383,7 @@ Examples: inboxapi send-email --to user@example.com --subject \"Fwd\" --body \"See attached\" --attachment-ref 9f0206bb-... inboxapi get-emails --limit 5 inboxapi get-emails --limit 5 --human + inboxapi delete-email \"\" --force inboxapi get-last-email inboxapi get-email-count inboxapi get-sent-emails --limit 10 @@ -2469,6 +2516,20 @@ async fn run_cli_command(cli: &Cli) -> Result<()> { let text = extract_tool_result_text(&response)?; print_result("get_email", &text, cli.human); } + Some(Commands::DeleteEmail { + ref message_id, + force, + }) => { + if !confirm_delete_email(force, message_id)? { + println!("Aborted."); + return Ok(()); + } + let args = json!({"message_id": message_id}); + let response = + call_mcp_tool(&endpoint, &mut creds, &http_client, "delete_email", args).await?; + let text = extract_tool_result_text(&response)?; + print_result("delete_email", &text, cli.human); + } Some(Commands::SearchEmails { ref sender, ref subject, @@ -7651,6 +7712,35 @@ mod tests { assert!(err.to_string().contains("--body-file")); } + #[test] + fn test_delete_email_parses_positional_message_id() { + let cli = Cli::try_parse_from(["inboxapi", "delete-email", "", "--force"]).unwrap(); + + assert!( + matches!( + cli.command, + Some(Commands::DeleteEmail { message_id, force: true }) if message_id == "" + ), + "CLI arguments for delete-email should be parsed correctly" + ); + } + + #[test] + fn test_delete_email_prompt_mode() { + assert!( + !delete_email_prompt_mode(true, false).expect("force should skip prompting"), + "force should bypass prompting even when stdin is not a TTY" + ); + assert!( + delete_email_prompt_mode(false, true).expect("TTY stdin should allow prompting"), + "interactive stdin should prompt for confirmation" + ); + assert!( + delete_email_prompt_mode(false, false).is_err(), + "non-interactive stdin without force should be rejected" + ); + } + // --- guess_content_type tests --- #[test] @@ -8030,6 +8120,16 @@ mod tests { assert!(output.contains("Hello Bob")); } + #[test] + fn test_human_output_delete_email() { + let text = r#"{"success": true, "message_id": ""}"#; + let output = format_human_output("delete_email", text); + assert_eq!( + output, "Email deleted: ", + "delete_email human output should surface the deleted message id" + ); + } + #[test] fn test_human_output_search_emails_empty() { let output = format_human_output("search_emails", "[]"); @@ -8230,17 +8330,45 @@ mod tests { #[test] fn test_help_output_contains_all_commands() { - assert!(CLI_HELP_TEXT.contains("send-email")); - assert!(CLI_HELP_TEXT.contains("get-emails")); - assert!(CLI_HELP_TEXT.contains("get-email")); - assert!(CLI_HELP_TEXT.contains("search-emails")); - assert!(CLI_HELP_TEXT.contains("get-attachment")); - assert!(CLI_HELP_TEXT.contains("send-reply")); - assert!(CLI_HELP_TEXT.contains("forward-email")); - assert!(CLI_HELP_TEXT.contains("whoami")); - assert!(CLI_HELP_TEXT.contains("proxy")); - assert!(CLI_HELP_TEXT.contains("login")); - assert!(CLI_HELP_TEXT.contains("help")); + assert!( + CLI_HELP_TEXT.contains("send-email"), + "help should include send-email" + ); + assert!( + CLI_HELP_TEXT.contains("get-emails"), + "help should include get-emails" + ); + assert!( + CLI_HELP_TEXT.contains("get-email"), + "help should include get-email" + ); + assert!( + CLI_HELP_TEXT.contains("delete-email"), + "CLI help text should include the delete-email command" + ); + assert!( + CLI_HELP_TEXT.contains("search-emails"), + "help should include search-emails" + ); + assert!( + CLI_HELP_TEXT.contains("get-attachment"), + "help should include get-attachment" + ); + assert!( + CLI_HELP_TEXT.contains("send-reply"), + "help should include send-reply" + ); + assert!( + CLI_HELP_TEXT.contains("forward-email"), + "help should include forward-email" + ); + assert!( + CLI_HELP_TEXT.contains("whoami"), + "help should include whoami" + ); + assert!(CLI_HELP_TEXT.contains("proxy"), "help should include proxy"); + assert!(CLI_HELP_TEXT.contains("login"), "help should include login"); + assert!(CLI_HELP_TEXT.contains("help"), "help should include help"); } #[test]