Oregami
Repositories/oxedyne/daimond

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
18use crate::llm::extract_json_string;
19use crate::wasm::js_str;
20
21use oxedyne_fe2o3_core::prelude::*;
22use oxedyne_fe2o3_text::base64;
23
24use wasm_bindgen::prelude::wasm_bindgen;
25use wasm_bindgen::{JsCast, JsValue};
26use wasm_bindgen_futures::JsFuture;
27
28
29#[wasm_bindgen]
30extern "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)]
66pub 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.
72fn 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.
87fn 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.
95async 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.
112pub 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.
121pub 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.
130pub 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).
153pub 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.
164pub async fn sender(req: &str) -> Outcome<String> {
165 let p = res!(panel());
166 text(p.tool_sender(req)).await
167}