oxedyne/daimond/src/wasm/mail.rs
6.7 KiB, 1 run
created by r2519314175:1510, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! The Mail panel's edge — a thin binding to the JS driver `window.DaimondMail`. |
| 2 | //! |
| 3 | //! WHAT THIS SIDE OWNS AND WHAT THE PANEL OWNS. The mailbox is the panel's: which accounts |
| 4 | //! are configured, which folder is on screen, the Maildir layout on disk and the drafts a |
| 5 | //! person is about to send all live in `www/js/mail.js`, and asking it for them is asking the |
| 6 | //! one thing that knows. What Rust adds is the READING of a message and the BUILDING of a |
| 7 | //! draft: [`crate::wasm::mail`] hands raw bytes to [`oxedyne_fe2o3_mail::message`] to parse, |
| 8 | //! and hands its built bytes back to the panel to file. So the parser and the draft builder |
| 9 | //! are exercised in one tested place -- the fe2o3 crate -- rather than written a second time |
| 10 | //! here in a second language. |
| 11 | //! |
| 12 | //! THERE IS NO SEND ON THIS EDGE, AND THERE IS NONE ON THE PANEL A MODEL CAN REACH. A draft |
| 13 | //! is written to `mail/<address>/drafts/<id>.eml`, which is exactly where the human panel |
| 14 | //! reads the drafts a person reviews and sends; the one path to the wire is `sendDraft`, and |
| 15 | //! it runs only when a person presses Send (see the header of `www/js/mail.js`). Nothing |
| 16 | //! here, and nothing the model calls, reaches it. |
| 17 | |
| 18 | use crate::llm::extract_json_string; |
| 19 | use crate::wasm::js_str; |
| 20 | |
| 21 | use oxedyne_fe2o3_core::prelude::*; |
| 22 | use oxedyne_fe2o3_text::base64; |
| 23 | |
| 24 | use wasm_bindgen::prelude::wasm_bindgen; |
| 25 | use wasm_bindgen::{JsCast, JsValue}; |
| 26 | use wasm_bindgen_futures::JsFuture; |
| 27 | |
| 28 | |
| 29 | #[wasm_bindgen] |
| 30 | extern "C" { |
| 31 | |
| 32 | /// The driver object `www/js/mail.js` installs at `window.DaimondMail`. |
| 33 | #[wasm_bindgen(js_name = DaimondMail)] |
| 34 | type Panel; |
| 35 | |
| 36 | /// List the mailboxes, their folders and a folder's recent messages, as model-ready text. |
| 37 | #[wasm_bindgen(method, js_name = toolList)] |
| 38 | fn tool_list(this: &Panel, req: &str) -> js_sys::Promise; |
| 39 | |
| 40 | /// Find messages in a folder whose sender or subject matches, as model-ready text. |
| 41 | #[wasm_bindgen(method, js_name = toolSearch)] |
| 42 | fn tool_search(this: &Panel, req: &str) -> js_sys::Promise; |
| 43 | |
| 44 | /// Hand back one message's raw bytes, base64 in a JSON envelope, for Rust to parse. |
| 45 | #[wasm_bindgen(method, js_name = toolReadRaw)] |
| 46 | fn tool_read_raw(this: &Panel, req: &str) -> js_sys::Promise; |
| 47 | |
| 48 | /// File an already-built draft into the drafts folder the user's Send button reads. |
| 49 | #[wasm_bindgen(method, js_name = putDraftRaw)] |
| 50 | fn put_draft_raw(this: &Panel, req: &str) -> js_sys::Promise; |
| 51 | |
| 52 | /// The address of a mailbox to draft from -- the named one, or the selected one -- or the |
| 53 | /// empty string when no mailbox is configured. |
| 54 | #[wasm_bindgen(method, js_name = toolSender)] |
| 55 | fn tool_sender(this: &Panel, req: &str) -> js_sys::Promise; |
| 56 | } |
| 57 | |
| 58 | |
| 59 | /// What came back from asking the panel for one message's bytes. |
| 60 | /// |
| 61 | /// A refusal is an ANSWER and not an error, exactly as it is for the Social panel: no such |
| 62 | /// mailbox, no such message, nothing synced -- each is something the model can act on and |
| 63 | /// must read in its own language, so it comes back as a sentence to relay rather than as an |
| 64 | /// `Err` the dispatcher would dress in a failure line. |
| 65 | #[derive(Clone, Debug)] |
| 66 | pub enum Read { |
| 67 | Bytes(Vec<u8>), |
| 68 | Refused(String), |
| 69 | } |
| 70 | |
| 71 | /// Reach the driver object on `window`, or refuse in the model's language. |
| 72 | fn panel() -> Outcome<Panel> { |
| 73 | let win = res!(web_sys::window() |
| 74 | .ok_or_else(|| err!("Reaching the Mail panel needs a browser window."; System, Missing))); |
| 75 | let obj = res!(js_sys::Reflect::get(&win, &JsValue::from_str("DaimondMail")) |
| 76 | .map_err(|e| err!("Reading window.DaimondMail failed: {}.", js_str(&e); System, Missing))); |
| 77 | if obj.is_undefined() || obj.is_null() { |
| 78 | return Err(err!( |
| 79 | "Daimond's Mail panel is not loaded in this page. Tell the user what you wanted to \ |
| 80 | read or draft there, and carry on without it."; System, Missing)); |
| 81 | } |
| 82 | Ok(obj.unchecked_into::<Panel>()) |
| 83 | } |
| 84 | |
| 85 | /// The `message` of a rejected JS `Error`, verbatim. A refusal from the driver is written |
| 86 | /// for the model to read, so nothing here rewords it. |
| 87 | fn refusal(e: &JsValue) -> String { |
| 88 | match js_sys::Reflect::get(e, &JsValue::from_str("message")) { |
| 89 | Ok(m) => m.as_string().unwrap_or_else(|| js_str(e)), |
| 90 | Err(_) => js_str(e), |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | /// Settle a driver promise that answers with a string. |
| 95 | async fn text(p: js_sys::Promise) -> Outcome<String> { |
| 96 | let v = match JsFuture::from(p).await { |
| 97 | Ok(v) => v, |
| 98 | Err(e) => return Err(err!("{}", refusal(&e); IO, Invalid)), |
| 99 | }; |
| 100 | match v.as_string() { |
| 101 | Some(s) => Ok(s), |
| 102 | None => Err(err!( |
| 103 | "The Mail panel answered with something that cannot be read, so what it did is \ |
| 104 | unknown. Do not tell the user it worked."; Invalid, Data)), |
| 105 | } |
| 106 | } |
| 107 | |
| 108 | /// List the mailboxes and a folder's recent messages. |
| 109 | /// |
| 110 | /// # Arguments |
| 111 | /// * `req` - The raw `mail_list` arguments, passed straight to the panel. |
| 112 | pub async fn list(req: &str) -> Outcome<String> { |
| 113 | let p = res!(panel()); |
| 114 | text(p.tool_list(req)).await |
| 115 | } |
| 116 | |
| 117 | /// Find messages in a folder by sender or subject. |
| 118 | /// |
| 119 | /// # Arguments |
| 120 | /// * `req` - The raw `mail_search` arguments, passed straight to the panel. |
| 121 | pub async fn search(req: &str) -> Outcome<String> { |
| 122 | let p = res!(panel()); |
| 123 | text(p.tool_search(req)).await |
| 124 | } |
| 125 | |
| 126 | /// Fetch one message's bytes for the caller to parse, or a refusal to relay. |
| 127 | /// |
| 128 | /// # Arguments |
| 129 | /// * `req` - The raw `mail_read` arguments: which mailbox, folder and uid, or a path. |
| 130 | pub async fn read_raw(req: &str) -> Outcome<Read> { |
| 131 | let p = res!(panel()); |
| 132 | let json = res!(text(p.tool_read_raw(req)).await); |
| 133 | if let Some(no) = extract_json_string(&json, "error") { |
| 134 | if !no.trim().is_empty() { |
| 135 | return Ok(Read::Refused(no)); |
| 136 | } |
| 137 | } |
| 138 | match extract_json_string(&json, "raw_b64") { |
| 139 | Some(b) if !b.trim().is_empty() => { |
| 140 | let cleaned: String = b.chars().filter(|c| !c.is_whitespace()).collect(); |
| 141 | Ok(Read::Bytes(res!(base64::decode(&cleaned)))) |
| 142 | }, |
| 143 | _ => Err(err!( |
| 144 | "The Mail panel returned no message bytes, so there is nothing to read. Do not \ |
| 145 | tell the user what it said."; Invalid, Data)), |
| 146 | } |
| 147 | } |
| 148 | |
| 149 | /// File a built draft into the mailbox's drafts folder, for a person to review and send. |
| 150 | /// |
| 151 | /// # Arguments |
| 152 | /// * `req` - A JSON object of `address` (which mailbox) and `raw_b64` (the built bytes). |
| 153 | pub async fn put_draft(req: &str) -> Outcome<String> { |
| 154 | let p = res!(panel()); |
| 155 | text(p.put_draft_raw(req)).await |
| 156 | } |
| 157 | |
| 158 | /// The address a draft should be sent from when the model named none: the selected mailbox, |
| 159 | /// or empty when there is none. |
| 160 | /// |
| 161 | /// # Arguments |
| 162 | /// * `req` - A JSON object; an `address` in it names a mailbox to confirm, else the panel's |
| 163 | /// selected one is returned. |
| 164 | pub async fn sender(req: &str) -> Outcome<String> { |
| 165 | let p = res!(panel()); |
| 166 | text(p.tool_sender(req)).await |
| 167 | } |