/// ProtonMail REST API client (Step 3). /// /// Wraps the endpoints tuimail needs: /// - list messages in the inbox /// - fetch a single message (with encrypted body) /// - delete messages /// - fetch a recipient's public keys (for encryption when sending) use reqwest::Client; use serde::{Deserialize, Serialize}; use crate::auth::Session; const API_BASE: &str = "https://mail.proton.me/api"; /// ProtonMail label IDs for well-known mailboxes. pub const LABEL_INBOX: &str = "0"; // ── API response envelopes ──────────────────────────────────────────────────── /// Every ProtonMail API response wraps the payload in `{ "Code": 1000, ... }`. /// We only check Code in places where we want a custom error message; elsewhere /// `error_for_status()` on the HTTP layer is sufficient. #[derive(Deserialize)] struct Envelope { #[serde(rename = "Code")] code: u32, #[serde(flatten)] data: T, } // ── Message list ───────────────────────────────────────────────────────────── /// Lightweight message metadata returned by `GET /mail/v4/messages`. #[derive(Debug, Clone, Deserialize)] pub struct MessageMeta { #[serde(rename = "ID")] pub id: String, #[serde(rename = "Subject")] pub subject: String, /// Sender display name + address. #[serde(rename = "Sender")] pub sender: EmailAddress, /// Unix timestamp of the message. #[serde(rename = "Time")] pub time: i64, /// 0 = unread, 1 = read. #[serde(rename = "Unread")] pub unread: u32, /// Message size in bytes. #[serde(rename = "Size")] pub size: u64, } #[derive(Debug, Clone, Deserialize)] pub struct EmailAddress { #[serde(rename = "Name")] pub name: String, #[serde(rename = "Address")] pub address: String, } #[derive(Deserialize)] struct MessageListPayload { #[serde(rename = "Messages")] messages: Vec, #[serde(rename = "Total")] total: u32, } // ── Full message ────────────────────────────────────────────────────────────── /// Full message with (still-encrypted) body. #[derive(Debug, Clone, Deserialize)] pub struct Message { #[serde(rename = "ID")] pub id: String, #[serde(rename = "Subject")] pub subject: String, #[serde(rename = "Sender")] pub sender: EmailAddress, #[serde(rename = "ToList")] pub to_list: Vec, #[serde(rename = "CCList")] pub cc_list: Vec, #[serde(rename = "Time")] pub time: i64, /// PGP-encrypted MIME body (decrypt in Step 4). #[serde(rename = "Body")] pub body: String, /// MIME type of the decrypted body ("text/html" or "text/plain"). #[serde(rename = "MIMEType")] pub mime_type: String, /// The session key is encrypted with the recipient's private key. #[serde(rename = "Header")] pub header: String, } #[derive(Deserialize)] struct MessagePayload { #[serde(rename = "Message")] message: Message, } // ── Delete ──────────────────────────────────────────────────────────────────── #[derive(Serialize)] struct DeleteRequest<'a> { #[serde(rename = "IDs")] ids: &'a [String], } // ── Public keys ─────────────────────────────────────────────────────────────── /// One armored public key for a recipient. #[derive(Debug, Clone, Deserialize)] pub struct RecipientKey { /// PGP armored public key. #[serde(rename = "PublicKey")] pub public_key: String, /// Flags bitmap: bit 0 = can encrypt, bit 1 = can verify. #[serde(rename = "Flags")] pub flags: u32, } #[derive(Deserialize)] struct KeysPayload { #[serde(rename = "Keys")] keys: Vec, } // ── API client ──────────────────────────────────────────────────────────────── /// Stateless API client. Pass a freshly-validated `Session` to each call. pub struct ApiClient<'a> { client: &'a Client, session: &'a Session, } impl<'a> ApiClient<'a> { pub fn new(client: &'a Client, session: &'a Session) -> Self { Self { client, session } } fn auth_headers(&self) -> [(&'static str, String); 2] { [ ("x-pm-uid", self.session.uid.clone()), ("Authorization", format!("Bearer {}", self.session.access_token)), ] } /// List messages in a label (use `LABEL_INBOX`). /// /// Returns `(messages, total)` where `total` is the full count (for paging). /// `page` is zero-based; `page_size` is typically 50. pub async fn list_messages( &self, label_id: &str, page: u32, page_size: u32, ) -> Result<(Vec, u32), String> { let [h0, h1] = self.auth_headers(); let env: Envelope = self .client .get(format!("{}/mail/v4/messages", API_BASE)) .header(h0.0, h0.1) .header(h1.0, h1.1) .query(&[ ("LabelID", label_id), ("Page", &page.to_string()), ("PageSize", &page_size.to_string()), ("Sort", "Time"), ("Desc", "1"), ]) .send() .await .map_err(|e| e.to_string())? .error_for_status() .map_err(|e| e.to_string())? .json() .await .map_err(|e| e.to_string())?; if env.code != 1000 { return Err(format!("list_messages: API code {}", env.code)); } Ok((env.data.messages, env.data.total)) } /// Fetch a single message with its (encrypted) body. pub async fn get_message(&self, id: &str) -> Result { let [h0, h1] = self.auth_headers(); let env: Envelope = self .client .get(format!("{}/mail/v4/messages/{}", API_BASE, id)) .header(h0.0, h0.1) .header(h1.0, h1.1) .send() .await .map_err(|e| e.to_string())? .error_for_status() .map_err(|e| e.to_string())? .json() .await .map_err(|e| e.to_string())?; if env.code != 1000 { return Err(format!("get_message: API code {}", env.code)); } Ok(env.data.message) } /// Move messages to Trash (ProtonMail's delete is a soft-delete to Trash). pub async fn delete_messages(&self, ids: &[String]) -> Result<(), String> { let [h0, h1] = self.auth_headers(); self.client .put(format!("{}/mail/v4/messages/delete", API_BASE)) .header(h0.0, h0.1) .header(h1.0, h1.1) .json(&DeleteRequest { ids }) .send() .await .map_err(|e| e.to_string())? .error_for_status() .map_err(|e| e.to_string())?; Ok(()) } /// Fetch the public keys for a recipient email address. /// Used when composing outbound mail (Step 7). pub async fn get_public_keys(&self, email: &str) -> Result, String> { let [h0, h1] = self.auth_headers(); let env: Envelope = self .client .get(format!("{}/core/v4/keys", API_BASE)) .header(h0.0, h0.1) .header(h1.0, h1.1) .query(&[("Email", email)]) .send() .await .map_err(|e| e.to_string())? .error_for_status() .map_err(|e| e.to_string())? .json() .await .map_err(|e| e.to_string())?; if env.code != 1000 { return Err(format!("get_public_keys: API code {}", env.code)); } Ok(env.data.keys) } }