email-cli 0.6.6

Agent-friendly email CLI for Resend
use anyhow::Result;
use rusqlite::params;
use std::path::{Path, PathBuf};
use uuid::Uuid;

use crate::app::App;
use crate::cli::{
    DraftCreateArgs, DraftDeleteArgs, DraftEditArgs, DraftListArgs, DraftSendArgs, DraftShowArgs,
};
use crate::helpers::{
    ensure_reply_account_matches, normalize_email, normalize_emails,
    remove_draft_attachment_snapshot, reply_headers_for_message, snapshot_draft_attachments,
    to_json,
};
use crate::models::ResolvedCompose;
use crate::output::print_success_or;

impl App {
    pub fn draft_create(&self, args: DraftCreateArgs) -> Result<()> {
        let compose = self.resolve_compose(args.compose)?;
        let id = Uuid::new_v4().to_string();
        if let Some(message_id) = args.reply_to {
            let target = self.get_message(message_id)?;
            ensure_reply_account_matches(&target, &compose.account)?;
        }
        let attachment_paths = snapshot_draft_attachments(
            self.db_path.parent().unwrap_or(Path::new(".")),
            &id,
            &compose.attachments,
        )?;
        self.conn.execute(
            "
            INSERT INTO drafts (
                id, account_email, to_json, cc_json, bcc_json, subject,
                text_body, html_body, reply_to_message_id, attachment_paths_json
            ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)
            ",
            params![
                id,
                compose.account.email,
                to_json(&compose.to)?,
                to_json(&compose.cc)?,
                to_json(&compose.bcc)?,
                compose.subject,
                compose.text,
                compose.html,
                args.reply_to,
                to_json(&attachment_paths)?,
            ],
        )?;
        let draft = self.get_draft(&id)?;

        print_success_or(self.format, &draft, |draft| {
            println!("saved draft {}", draft.id);
        });

        Ok(())
    }

    pub fn draft_list(&self, args: DraftListArgs) -> Result<()> {
        let drafts = if let Some(account) = args.account {
            let account = normalize_email(&account);
            self.list_drafts_for_account(&account)?
        } else {
            self.list_all_drafts()?
        };

        print_success_or(self.format, &drafts, |drafts| {
            for draft in drafts {
                println!("{} {} {}", draft.id, draft.account_email, draft.subject);
            }
        });

        Ok(())
    }

    pub fn draft_show(&self, args: DraftShowArgs) -> Result<()> {
        let draft = self.get_draft(&args.id)?;

        print_success_or(self.format, &draft, |draft| {
            println!("draft {}", draft.id);
            println!("account: {}", draft.account_email);
            println!("to: {}", draft.to.join(", "));
            println!("subject: {}", draft.subject);
            if let Some(text) = &draft.text_body {
                println!();
                println!("{}", text);
            }
        });

        Ok(())
    }

    pub fn draft_send(&self, args: DraftSendArgs) -> Result<()> {
        let draft = self.get_draft(&args.id)?;
        let account = self.get_account(&draft.account_email)?;
        let reply_context = if let Some(message_id) = draft.reply_to_message_id {
            let target = self.get_message(message_id)?;
            ensure_reply_account_matches(&target, &account)?;
            Some((target.id, reply_headers_for_message(&target)))
        } else {
            None
        };
        let compose = ResolvedCompose {
            account,
            to: draft.to.clone(),
            cc: draft.cc.clone(),
            bcc: draft.bcc.clone(),
            subject: draft.subject.clone(),
            text: draft.text_body.clone(),
            html: draft.html_body.clone(),
            attachments: draft
                .attachment_paths
                .iter()
                .map(PathBuf::from)
                .collect::<Vec<_>>(),
        };
        let message = self.send_compose(compose, reply_context)?;
        self.conn
            .execute("DELETE FROM drafts WHERE id = ?1", params![draft.id])?;
        remove_draft_attachment_snapshot(
            self.db_path.parent().unwrap_or(Path::new(".")),
            &draft.id,
        )?;

        print_success_or(self.format, &message, |message| {
            println!("sent draft as message {}", message.id);
        });

        Ok(())
    }

    pub fn draft_edit(&self, args: DraftEditArgs) -> Result<()> {
        let draft = self.get_draft(&args.id)?;

        let subject = args.subject.unwrap_or(draft.subject);
        let text_body = args.text.or(draft.text_body);
        let html_body = args.html.or(draft.html_body);
        let to = args.to.map(|v| normalize_emails(&v)).unwrap_or(draft.to);
        let cc = args.cc.map(|v| normalize_emails(&v)).unwrap_or(draft.cc);
        let bcc = args.bcc.map(|v| normalize_emails(&v)).unwrap_or(draft.bcc);

        self.conn.execute(
            "UPDATE drafts SET subject = ?1, text_body = ?2, html_body = ?3,
             to_json = ?4, cc_json = ?5, bcc_json = ?6, updated_at = CURRENT_TIMESTAMP
             WHERE id = ?7",
            params![
                subject,
                text_body,
                html_body,
                to_json(&to)?,
                to_json(&cc)?,
                to_json(&bcc)?,
                args.id,
            ],
        )?;

        print_success_or(
            self.format,
            &serde_json::json!({"id": args.id, "updated": true}),
            |_| {
                println!("updated draft {}", args.id);
            },
        );
        Ok(())
    }

    pub fn draft_delete(&self, args: DraftDeleteArgs) -> Result<()> {
        let count = self
            .conn
            .execute("DELETE FROM drafts WHERE id = ?1", params![args.id])?;
        if count == 0 {
            anyhow::bail!("draft {} not found", args.id);
        }
        remove_draft_attachment_snapshot(
            self.db_path.parent().unwrap_or(Path::new(".")),
            &args.id,
        )?;
        print_success_or(
            self.format,
            &serde_json::json!({"id": args.id, "deleted": true}),
            |_| {
                println!("deleted draft {}", args.id);
            },
        );
        Ok(())
    }
}