1#![allow(non_snake_case)]
4
5use super::*;
6
7pub fn mail_now_unix() -> u64 {
8 std::time::SystemTime::now()
9 .duration_since(std::time::UNIX_EPOCH)
10 .map(|d| d.as_secs())
11 .unwrap_or(0)
12}
13
14fn agent_type_from_token(t: &str) -> crate::domains::AgentType {
15 use crate::domains::AgentType::*;
16 match t {
17 "org" | "organization" => Organization,
18 "ai" | "agent" => AiAgent,
19 "service" | "humanitarian" => HumanitarianService,
20 "content" => ContentProvider,
21 "group" => Group,
22 _ => NaturalPerson,
23 }
24}
25
26pub fn list_mail_domains() -> Result<serde_json::Value, String> {
28 serde_json::to_value(crate::domains::list_domains()).map_err(|e| e.to_string())
29}
30
31pub fn add_mail_domain(
33 name: String,
34 agent_type: String,
35 front_door_did: String,
36 label: String,
37 parent: Option<String>,
38) -> Result<serde_json::Value, String> {
39 let owner = crate::domains::DomainOwner::Personal {
40 did: front_door_did.clone(),
41 };
42 let d = crate::domains::make_domain(
43 &name,
44 agent_type_from_token(&agent_type),
45 owner,
46 &front_door_did,
47 &label,
48 parent,
49 mail_now_unix(),
50 )?;
51 crate::domains::upsert_domain(d)?;
52 list_mail_domains()
53}
54
55pub fn purpose_inbox_presets() -> Result<serde_json::Value, String> {
57 serde_json::to_value(crate::domains::purpose_presets()).map_err(|e| e.to_string())
58}
59
60pub fn list_mail_addresses(domain: Option<String>) -> Result<serde_json::Value, String> {
62 serde_json::to_value(crate::domains::list_addresses(domain.as_deref()))
63 .map_err(|e| e.to_string())
64}
65
66pub fn mint_purpose_inbox(
68 domain: String,
69 local: String,
70 rules_json: String,
71) -> Result<serde_json::Value, String> {
72 let rules: crate::domains::MailRules = if rules_json.trim().is_empty() {
73 crate::domains::MailRules::default()
74 } else {
75 serde_json::from_str(&rules_json).map_err(|e| format!("bad rules json: {e}"))?
76 };
77 let a = crate::domains::make_purpose_address(&domain, &local, rules, mail_now_unix())?;
78 crate::domains::upsert_address(a)?;
79 list_mail_addresses(Some(domain))
80}
81
82pub fn onboard_mail_domain(domain: String) -> Result<serde_json::Value, String> {
85 let minted = crate::domains::onboard_purpose_inboxes(&domain)?;
86 let domain_key = domain.trim().to_lowercase();
87 let addresses = crate::domains::list_addresses(Some(&domain_key));
88 #[cfg(not(target_arch = "wasm32"))]
89 let receiver = match crate::mail_inbound::start_receiver("") {
90 Ok(v) => v,
91 Err(e) => serde_json::json!({
92 "started": false,
93 "error": e,
94 "hint": "Start the receiver from Talk → Mail if bind failed (port in use?).",
95 }),
96 };
97 #[cfg(target_arch = "wasm32")]
98 let receiver = serde_json::json!({ "started": false, "hint": "receiver is native-only" });
99 let mint_msg = if minted.is_empty() {
100 "Mail already onboarded for this domain.".to_string()
101 } else {
102 format!(
103 "Minted {} mailbox(es): {}.",
104 minted.len(),
105 minted.join(", ")
106 )
107 };
108 let recv_msg = if receiver
109 .get("started")
110 .and_then(|x| x.as_bool())
111 .unwrap_or(false)
112 || receiver
113 .get("already_running")
114 .and_then(|x| x.as_bool())
115 .unwrap_or(false)
116 {
117 " Local SMTP receiver running — inbox can accept mail.".to_string()
118 } else if let Some(err) = receiver.get("error").and_then(|e| e.as_str()) {
119 format!(" Receiver not started: {err}.")
120 } else {
121 String::new()
122 };
123 Ok(serde_json::json!({
124 "domain": domain_key,
125 "minted": minted,
126 "addresses": addresses,
127 "receiver": receiver,
128 "message": format!("{mint_msg}{recv_msg}"),
129 }))
130}
131
132pub fn talk_setup_status() -> Result<serde_json::Value, String> {
134 let domains = crate::domains::list_domains();
135 let addresses = crate::domains::list_addresses(None);
136 let (mail_total, mail_unread, mail_quarantine) = crate::mail_store::counts();
137 let receiver = crate::mail_inbound::receiver_status();
138 let peers = crate::social_peers::list_peers();
139 let contacts_n = crate::social_connect::list_chat_contacts().len();
140 let has_domain = !domains.is_empty();
141 let has_mailboxes = !addresses.is_empty();
142 let receiver_running = receiver
143 .get("running")
144 .and_then(|x| x.as_bool())
145 .unwrap_or(false);
146 let has_people = !peers.is_empty() || contacts_n > 0;
147 let collabs = crate::project_collab::list(None);
148 let dial = crate::social_mesh::dialability(&peers);
149 let dialable = dial.iter().filter(|d| d.dialable_now).count();
150 let reachable = dial.iter().filter(|d| d.reachable).count();
151 let ready_for_mail = has_domain && has_mailboxes && receiver_running;
152 let ready_to_collaborate = has_domain && has_people;
153 Ok(serde_json::json!({
154 "domains": domains.len(),
155 "mailboxes": addresses.len(),
156 "mail_inbox": { "total": mail_total, "unread": mail_unread, "quarantine": mail_quarantine },
157 "receiver": receiver,
158 "peers": peers.len(),
159 "contacts": contacts_n,
160 "collaborators": collabs.len(),
161 "mesh": {
162 "reachable_peers": reachable,
163 "dialable_now": dialable,
164 "reports": dial,
165 },
166 "has_domain": has_domain,
167 "has_mailboxes": has_mailboxes,
168 "receiver_running": receiver_running,
169 "has_people": has_people,
170 "ready_for_mail": ready_for_mail,
171 "ready_to_collaborate": ready_to_collaborate,
172 "next_step": if !has_domain {
173 "reception"
174 } else if !has_mailboxes {
175 "mail_onboard"
176 } else if !receiver_running {
177 "mail_receiver"
178 } else if !has_people {
179 "people"
180 } else if collabs.is_empty() {
181 "projects"
182 } else {
183 "chat_or_projects"
184 },
185 }))
186}
187
188fn ensure_connect_invites_enabled() -> Result<(), String> {
190 let mut profile = crate::user_profile::load_profile();
191 if !profile.sharing.allow_group_chat_invites {
192 profile.sharing.allow_group_chat_invites = true;
193 crate::user_profile::save_profile(&profile)?;
194 }
195 Ok(())
196}
197
198pub fn extract_invite_json_from_package(v: &serde_json::Value) -> Result<String, String> {
201 if v.get("qualia_coop_share").is_some() {
202 let invite_payload = v
203 .get("invite_json")
204 .cloned()
205 .filter(|x| !x.is_null())
206 .ok_or_else(|| {
207 "share package has no invite_json — host must enable invites and rebuild package"
208 .to_string()
209 })?;
210 if invite_payload.is_string() {
211 let s = invite_payload.as_str().unwrap_or("").trim().to_string();
212 if s.is_empty() {
213 return Err("invite_json string is empty".into());
214 }
215 return Ok(s);
216 }
217 return serde_json::to_string(&invite_payload).map_err(|e| e.to_string());
218 }
219 if v.get("signature_hex").is_some() && v.get("inviter_did").is_some() {
221 return serde_json::to_string(v).map_err(|e| e.to_string());
222 }
223 Err("not a coop share package or connect invite JSON".into())
224}
225
226pub fn coop_share_package(
229 project_id: Option<String>,
230 project_name: Option<String>,
231) -> Result<serde_json::Value, String> {
232 ensure_connect_invites_enabled()?;
233
234 let domains = crate::domains::list_domains();
235 let domain = domains.first().map(|d| d.name.clone()).unwrap_or_default();
236 let front_door = domains
237 .first()
238 .map(|d| d.front_door_did.clone())
239 .unwrap_or_default();
240 let pid = project_id.unwrap_or_default();
241 let pname = project_name.unwrap_or_default();
242 let profile = crate::user_profile::load_profile();
243
244 let invite = crate::social_connect::generate_connect_invite(None).map_err(|e| {
246 format!(
247 "could not embed connect invite: {e}. Set a display name under Talk → People and try again."
248 )
249 })?;
250 let invite_json: serde_json::Value = serde_json::from_str(&invite.invite_json)
251 .unwrap_or_else(|_| serde_json::Value::String(invite.invite_json.clone()));
252
253 #[cfg(not(target_arch = "wasm32"))]
254 let magic = {
255 let fd = if front_door.is_empty() {
256 String::new()
257 } else {
258 front_door.clone()
259 };
260 generate_magic_link(fd, "collaborator".into(), domain.clone()).ok()
262 };
263 #[cfg(target_arch = "wasm32")]
264 let magic: Option<serde_json::Value> = None;
265
266 if !pid.is_empty() {
268 let self_did = profile.public_did.clone();
269 if !self_did.is_empty() {
270 let _ = crate::project_collab::add(
271 &pid,
272 &pname,
273 &self_did,
274 &profile.display_name,
275 "steward",
276 );
277 }
278 }
279
280 let package = serde_json::json!({
281 "qualia_coop_share": "1",
282 "from_display_name": profile.display_name,
283 "domain": domain,
284 "front_door_did": front_door,
285 "project_id": pid,
286 "project_name": pname,
287 "members": crate::project_collab::list(if pid.is_empty() { None } else { Some(pid.as_str()) }),
288 "invite_code": invite.code,
289 "invite_json": invite_json,
290 "invite_expires_at": invite.expires_at,
291 "magic_link": magic,
292 "how": [
293 "1. Open Webizen (0.0.25+).",
294 "2. Talk → People → paste this entire JSON under Accept package / invite.",
295 "3. You are connected and the project is scoped on your device.",
296 "4. Talk → People → Start mesh for live peer chat.",
297 "5. Chat with #project:Name_With_Underscores for scoped work.",
298 ],
299 "note": "No private keys. Package is self-contained — one paste joins.",
300 });
301 Ok(package)
302}
303
304pub fn accept_coop_share_package(package_or_invite: String) -> Result<serde_json::Value, String> {
308 let raw = package_or_invite.trim();
309 if raw.is_empty() {
310 return Err("paste a coop share package or invite JSON".into());
311 }
312 let v: serde_json::Value = serde_json::from_str(raw).map_err(|e| format!("not JSON: {e}"))?;
313
314 let invite_str = extract_invite_json_from_package(&v).or_else(|e| {
315 if v.get("inviter_did").is_some() {
317 serde_json::to_string(&v).map_err(|e2| e2.to_string())
318 } else {
319 Err(e)
320 }
321 })?;
322 let contact = crate::social_connect::accept_connect_invite(&invite_str)?;
323
324 let project_id = v
325 .get("project_id")
326 .and_then(|x| x.as_str())
327 .unwrap_or("")
328 .to_string();
329 let project_name = v
330 .get("project_name")
331 .and_then(|x| x.as_str())
332 .unwrap_or("")
333 .to_string();
334
335 let profile = crate::user_profile::load_profile();
336 let mut admitted = Vec::new();
337 if !project_id.is_empty() {
338 let host_did = contact.did.clone();
339 let host_name = if contact.display_name.is_empty() {
340 contact.did.clone()
341 } else {
342 contact.display_name.clone()
343 };
344 if !host_did.is_empty() {
345 if let Ok(row) = crate::project_collab::add(
346 &project_id,
347 &project_name,
348 &host_did,
349 &host_name,
350 "steward",
351 ) {
352 admitted.push(row);
353 }
354 }
355 let self_did = profile.public_did.clone();
356 if self_did.is_empty() {
357 let resolved = crate::user_profile::resolve_public_did(&profile);
359 if !resolved.is_empty() {
360 if let Ok(row) = crate::project_collab::add(
361 &project_id,
362 &project_name,
363 &resolved,
364 &profile.display_name,
365 "contributor",
366 ) {
367 admitted.push(row);
368 }
369 }
370 } else if let Ok(row) = crate::project_collab::add(
371 &project_id,
372 &project_name,
373 &self_did,
374 &profile.display_name,
375 "contributor",
376 ) {
377 admitted.push(row);
378 }
379 }
380
381 let mut peer_registered = false;
383 #[cfg(not(target_arch = "wasm32"))]
384 if let Some(magic) = v.get("magic_link") {
385 let link = magic
386 .get("deep_link")
387 .or_else(|| magic.get("https_link"))
388 .and_then(|x| x.as_str())
389 .unwrap_or("");
390 if !link.is_empty() {
391 if accept_connection(link.to_string()).is_ok() {
392 peer_registered = true;
393 }
394 }
395 }
396
397 let mut group_chat: Option<serde_json::Value> = None;
399 if !project_id.is_empty() {
400 match create_project_group_chat(
401 project_id.clone(),
402 if project_name.is_empty() {
403 project_id.clone()
404 } else {
405 project_name.clone()
406 },
407 Some(vec![contact.did.clone()]),
408 ) {
409 Ok(g) => group_chat = Some(g),
410 Err(_) => {
411 }
413 }
414 }
415
416 Ok(serde_json::json!({
417 "connected": true,
418 "contact": contact,
419 "project_id": project_id,
420 "project_name": project_name,
421 "admitted": admitted,
422 "peer_registered": peer_registered,
423 "group_chat": group_chat,
424 "message": "Joined. Contact saved; project scoped; roster updated; group chat created when possible. Start mesh under People for live peer traffic.",
425 }))
426}
427
428#[cfg(test)]
429mod coop_share_tests {
430 use super::*;
431
432 #[test]
433 fn extract_invite_from_full_package() {
434 let package = serde_json::json!({
435 "qualia_coop_share": "1",
436 "project_id": "board-1",
437 "project_name": "QualiaDB Development Cooperative",
438 "invite_json": {
439 "version": 1,
440 "code": "QUALIA-TEST-TEST",
441 "inviter_name": "Host",
442 "inviter_did": "did:host",
443 "inviter_pubkey_hex": "",
444 "relay_endpoint": "",
445 "front_door_did": "did:host",
446 "profile_card": {},
447 "created_at": 1,
448 "expires_at": 9_999_999_999u64,
449 "signature_hex": ""
450 }
451 });
452 let s = extract_invite_json_from_package(&package).expect("extract");
453 assert!(s.contains("did:host"));
454 assert!(s.contains("QUALIA-TEST-TEST"));
455 }
456
457 #[test]
458 fn extract_invite_from_string_field() {
459 let package = serde_json::json!({
460 "qualia_coop_share": "1",
461 "invite_json": "{\"version\":1,\"code\":\"X\",\"inviter_did\":\"did:x\",\"signature_hex\":\"\"}"
462 });
463 let s = extract_invite_json_from_package(&package).expect("extract string");
464 assert!(s.contains("did:x"));
465 }
466
467 #[test]
468 fn extract_bare_invite_object() {
469 let invite = serde_json::json!({
470 "version": 1,
471 "inviter_did": "did:bare",
472 "signature_hex": "aa",
473 "code": "C"
474 });
475 let s = extract_invite_json_from_package(&invite).expect("bare");
476 assert!(s.contains("did:bare"));
477 }
478
479 #[test]
480 fn extract_rejects_empty_package() {
481 let package = serde_json::json!({ "qualia_coop_share": "1" });
482 assert!(extract_invite_json_from_package(&package).is_err());
483 }
484}
485
486pub fn create_project_group_chat(
489 project_id: String,
490 project_name: String,
491 extra_dids: Option<Vec<String>>,
492) -> Result<serde_json::Value, String> {
493 let mut dids: Vec<String> = crate::project_collab::list(Some(&project_id))
494 .into_iter()
495 .map(|c| c.member_did)
496 .collect();
497 if let Some(extra) = extra_dids {
498 for d in extra {
499 let d = d.trim().to_string();
500 if !d.is_empty() && !dids.iter().any(|x| x == &d) {
501 dids.push(d);
502 }
503 }
504 }
505 if dids.is_empty() {
507 for c in crate::social_connect::list_chat_contacts() {
508 if !c.did.is_empty() {
509 dids.push(c.did);
510 }
511 }
512 }
513 if dids.is_empty() {
514 return Err(
515 "No participants — admit collaborators on the project or accept invites under People first."
516 .into(),
517 );
518 }
519 let title = if project_name.trim().is_empty() {
520 format!("Project {project_id}")
521 } else {
522 format!("Project: {project_name}")
523 };
524 let session_id = create_group_chat_session(Some(title.clone()), dids.clone())?;
525 Ok(serde_json::json!({
526 "session_id": session_id,
527 "title": title,
528 "participants": dids,
529 "project_id": project_id,
530 "message": "Group chat created — open it under Talk → Chat → Conversations.",
531 }))
532}
533
534pub fn resolve_mail_delivery(to_address: String) -> Result<serde_json::Value, String> {
536 let addresses = crate::domains::list_addresses(None);
537 match crate::domains::resolve_delivery(&addresses, &to_address) {
538 crate::domains::DeliveryResolution::Deliver { address, via } => Ok(serde_json::json!({
539 "deliver": true,
540 "via": via,
541 "address": address,
542 })),
543 crate::domains::DeliveryResolution::Reject { reason } => Ok(serde_json::json!({
544 "deliver": false,
545 "rejected": reason,
546 })),
547 }
548}
549
550pub fn save_mail_transport_config(
552 smtp_json: String,
553 imap_json: String,
554) -> Result<serde_json::Value, String> {
555 let path = crate::state::app_meta_dir().join("mail_transport.json");
556 if let Some(p) = path.parent() {
557 std::fs::create_dir_all(p).map_err(|e| e.to_string())?;
558 }
559 let smtp: Option<crate::mail_transport::SmtpConfig> = if smtp_json.trim().is_empty() {
560 None
561 } else {
562 Some(serde_json::from_str(&smtp_json).map_err(|e| format!("bad smtp: {e}"))?)
563 };
564 let imap: Option<crate::mail_transport::ImapConfig> = if imap_json.trim().is_empty() {
565 None
566 } else {
567 Some(serde_json::from_str(&imap_json).map_err(|e| format!("bad imap: {e}"))?)
568 };
569 let doc = serde_json::json!({ "smtp": smtp, "imap": imap });
570 std::fs::write(
571 &path,
572 serde_json::to_string_pretty(&doc).map_err(|e| e.to_string())?,
573 )
574 .map_err(|e| e.to_string())?;
575 Ok(serde_json::json!({ "saved": true, "path": path.display().to_string() }))
576}
577
578pub fn load_mail_transport_config() -> Result<serde_json::Value, String> {
579 let path = crate::state::app_meta_dir().join("mail_transport.json");
580 match std::fs::read_to_string(path) {
581 Ok(t) => serde_json::from_str(&t).map_err(|e| e.to_string()),
582 Err(_) => Ok(serde_json::json!({ "smtp": null, "imap": null })),
583 }
584}
585
586pub fn mint_relationship_address(
588 domain: String,
589 local: String,
590 relationship_did: String,
591) -> Result<serde_json::Value, String> {
592 let a = crate::domains::make_relationship_address(
593 &domain,
594 &local,
595 &relationship_did,
596 mail_now_unix(),
597 )?;
598 crate::domains::upsert_address(a)?;
599 list_mail_addresses(Some(domain))
600}
601
602pub fn set_mail_address_enabled(
604 address: String,
605 enabled: bool,
606) -> Result<serde_json::Value, String> {
607 crate::domains::set_address_enabled(&address, enabled)?;
608 list_mail_addresses(None)
609}
610
611pub fn front_door_forms(domain: String) -> Result<serde_json::Value, String> {
614 let d = crate::domains::list_domains()
615 .into_iter()
616 .find(|d| d.name == domain)
617 .ok_or_else(|| format!("unknown domain '{domain}'"))?;
618 let rec = crate::front_door::FrontDoorRecord {
620 domain: d.name.clone(),
621 agent_type: d.agent_type.clone(),
622 front_door_did: d.front_door_did.clone(),
623 name: if d.label.is_empty() {
624 None
625 } else {
626 Some(d.label.clone())
627 },
628 webid: None,
629 services: vec![],
630 identity_pubkey_hex: None,
631 wireguard_pubkey_hex: None,
632 overlay_addr: None,
633 profile_url: None,
634 };
635 let mail_dns = crate::mail_inbound::mail_dns_forms(&d.name, None);
636 Ok(serde_json::json!({
637 "dns_name": crate::front_door::dns_record_name(&d.name),
638 "dns_txt": rec.to_dns_txt(),
639 "turtle": rec.to_turtle(),
640 "jsonld": rec.to_json_ld(),
641 "mail_dns": mail_dns,
642 }))
643}
644
645fn build_front_door_record(domain: &str) -> Result<crate::front_door::FrontDoorRecord, String> {
647 let d = crate::domains::list_domains()
648 .into_iter()
649 .find(|d| d.name == domain)
650 .ok_or_else(|| format!("unknown domain '{domain}'"))?;
651 Ok(crate::front_door::FrontDoorRecord {
652 domain: d.name.clone(),
653 agent_type: d.agent_type.clone(),
654 front_door_did: d.front_door_did.clone(),
655 name: if d.label.is_empty() {
656 None
657 } else {
658 Some(d.label.clone())
659 },
660 webid: None,
661 services: vec![],
662 identity_pubkey_hex: None,
663 wireguard_pubkey_hex: None,
664 overlay_addr: None,
665 profile_url: None,
666 })
667}
668
669#[cfg(not(target_arch = "wasm32"))]
671pub fn cf_verify_token(token: String) -> Result<serde_json::Value, String> {
672 crate::cloudflare::verify_token(&token)?;
673 Ok(serde_json::json!({ "ok": true }))
674}
675
676#[cfg(not(target_arch = "wasm32"))]
678pub fn cf_list_zones(token: String) -> Result<serde_json::Value, String> {
679 let zones = crate::cloudflare::list_zones(&token)?;
680 Ok(serde_json::json!(zones
681 .into_iter()
682 .map(|(id, name)| serde_json::json!({ "id": id, "name": name }))
683 .collect::<Vec<_>>()))
684}
685
686#[cfg(not(target_arch = "wasm32"))]
688pub fn cf_publish_front_door(
689 token: String,
690 zone_id: String,
691 domain: String,
692) -> Result<serde_json::Value, String> {
693 let rec = build_front_door_record(&domain)?;
694 let cfg = crate::cloudflare::CfConfig {
695 api_token: token,
696 zone_id,
697 };
698 let id = crate::cloudflare::publish_front_door(&cfg, &rec)?;
699 Ok(serde_json::json!({
700 "record_id": id,
701 "dns_name": crate::front_door::dns_record_name(&domain),
702 "dns_txt": rec.to_dns_txt(),
703 }))
704}
705
706#[cfg(not(target_arch = "wasm32"))]
708pub fn cf_provision_r2_bucket(
709 token: String,
710 account_id: String,
711 bucket_name: String,
712) -> Result<serde_json::Value, String> {
713 crate::cloudflare::provision_r2_bucket(&token, &account_id, &bucket_name)?;
714 Ok(serde_json::json!({ "ok": true, "bucket": bucket_name }))
715}
716
717#[cfg(not(target_arch = "wasm32"))]
719pub fn cf_provision_worker(
720 token: String,
721 account_id: String,
722 script_name: String,
723) -> Result<serde_json::Value, String> {
724 let script_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
725 .join("../../vendor/cloudflare-worker/worker.js");
726 let script_content = std::fs::read_to_string(&script_path)
727 .map_err(|e| format!("Failed to read worker.js: {e}"))?;
728
729 crate::cloudflare::provision_worker(&token, &account_id, &script_name, &script_content)?;
730 Ok(serde_json::json!({ "ok": true, "script": script_name }))
731}
732
733#[cfg(not(target_arch = "wasm32"))]
735pub fn cf_provision_tunnel(
736 token: String,
737 account_id: String,
738 tunnel_name: String,
739 tunnel_secret: String,
740) -> Result<serde_json::Value, String> {
741 let tunnel_id =
742 crate::cloudflare::provision_tunnel(&token, &account_id, &tunnel_name, &tunnel_secret)?;
743 Ok(serde_json::json!({ "ok": true, "tunnel_id": tunnel_id }))
744}
745
746#[cfg(not(target_arch = "wasm32"))]
748pub fn cf_route_tunnel_dns(
749 token: String,
750 zone_id: String,
751 record_name: String,
752 tunnel_id: String,
753) -> Result<serde_json::Value, String> {
754 let record_id =
755 crate::cloudflare::route_tunnel_dns(&token, &zone_id, &record_name, &tunnel_id)?;
756 Ok(serde_json::json!({ "ok": true, "record_id": record_id }))
757}
758
759#[cfg(not(target_arch = "wasm32"))]
761pub fn github_verify_token(token: String) -> Result<serde_json::Value, String> {
762 let login = crate::github::verify_github_token(&token).map_err(|e| e.to_string())?;
763 Ok(serde_json::json!({ "ok": true, "login": login }))
764}
765
766#[cfg(not(target_arch = "wasm32"))]
768pub fn github_deploy_static_site(
769 token: String,
770 repo_name: String,
771 files: std::collections::HashMap<String, String>,
772) -> Result<serde_json::Value, String> {
773 let full_name =
774 crate::github::create_repository(&token, &repo_name).map_err(|e| e.to_string())?;
775 crate::github::push_static_site(&token, &full_name, files).map_err(|e| e.to_string())?;
776 Ok(serde_json::json!({ "ok": true, "full_name": full_name }))
777}
778
779#[cfg(not(target_arch = "wasm32"))]
781pub fn cf_provision_pages_project(
782 token: String,
783 account_id: String,
784 project_name: String,
785 github_repo: String,
786) -> Result<serde_json::Value, String> {
787 crate::cloudflare::provision_pages_project(&token, &account_id, &project_name, &github_repo)?;
788 Ok(serde_json::json!({ "ok": true, "project": project_name }))
789}
790
791#[cfg(not(target_arch = "wasm32"))]
793pub fn start_qdp_server(domain: String, bind_addr: String) -> Result<serde_json::Value, String> {
794 let rec = build_front_door_record(&domain)?;
795 let addr = bind_addr.clone();
796 std::thread::spawn(move || {
797 let _ = crate::qdp_http::serve_blocking(rec, &addr);
798 });
799 Ok(serde_json::json!({ "serving": bind_addr, "path": crate::qdp_server::WELL_KNOWN_QDP_PATH }))
800}
801
802pub fn parse_magic_link(link: String) -> Result<serde_json::Value, String> {
804 let id = crate::magic_link::from_link(&link)?;
805 serde_json::to_value(id).map_err(|e| e.to_string())
806}
807
808#[cfg(not(target_arch = "wasm32"))]
810pub fn mail_send(smtp_json: String, mail_json: String) -> Result<serde_json::Value, String> {
811 let cfg: crate::mail_transport::SmtpConfig =
812 serde_json::from_str(&smtp_json).map_err(|e| format!("bad smtp config: {e}"))?;
813 let mail: crate::mail_transport::OutgoingMail =
814 serde_json::from_str(&mail_json).map_err(|e| format!("bad mail: {e}"))?;
815 crate::mail_transport::send(&cfg, &mail)?;
816 Ok(serde_json::json!({ "sent": true }))
817}
818
819#[cfg(not(target_arch = "wasm32"))]
822pub fn mail_fetch(imap_json: String, mailbox: String) -> Result<serde_json::Value, String> {
823 let cfg: crate::mail_transport::ImapConfig =
824 serde_json::from_str(&imap_json).map_err(|e| format!("bad imap config: {e}"))?;
825 let msgs = crate::mail_transport::fetch_unseen(&cfg, &mailbox)?;
826 let mut stored_ids = Vec::new();
827 let evaluated: Vec<serde_json::Value> = msgs
828 .into_iter()
829 .map(|m| {
830 let to = if m.to_address.contains('@') {
831 m.to_address.clone()
832 } else {
833 cfg.username.clone()
834 };
835 let r = crate::mail_inbound::accept_message(
837 &m.from_address,
838 &to,
839 &m.subject,
840 &format!("(imported via IMAP; size {} bytes)", m.size_bytes),
841 m.sender_verified,
842 m.sender_did.clone(),
843 );
844 if let Some(ref s) = r.stored {
845 stored_ids.push(s.id.clone());
846 }
847 serde_json::json!({
848 "message": m,
849 "accepted": r.accepted,
850 "rejected": r.rejected,
851 "stored": r.stored,
852 })
853 })
854 .collect();
855 Ok(serde_json::json!({
856 "messages": evaluated,
857 "stored_ids": stored_ids,
858 "stored_count": stored_ids.len(),
859 }))
860}