mail4agent_messenger_shell/cli/
send.rs1use crate::{
15 load_env_file, send_sock_path, send_via_socket, MachineClient, SendReply, SendRequest,
16 MAX_SEND_BYTES, STORE_ROOT_ENV,
17};
18use std::io::Read;
19use std::path::PathBuf;
20use std::time::Duration;
21
22const USAGE: &str =
23 "usage: m4a-send --as <your nick> --to <nick> <text...> (text '-' or none: read stdin)";
24
25fn fail(code: i32, message: &str) -> ! {
26 eprintln!("m4a-send: {message}");
27 std::process::exit(code);
28}
29
30pub fn run(args: Vec<String>) {
31 let mut as_nick = None;
32 let mut to = None;
33 let mut words: Vec<String> = Vec::new();
34 let mut args = args.clone().into_iter();
35 while let Some(arg) = args.next() {
36 match arg.as_str() {
37 "--as" => as_nick = args.next(),
38 "--to" => to = args.next(),
39 "-h" | "--help" => {
40 println!("{USAGE}");
41 return;
42 }
43 "--" => words.extend(args.by_ref()),
44 _ => words.push(arg),
45 }
46 }
47 let as_nick = as_nick.or_else(|| std::env::var_os(SIGNER_HANDLE_ENV).map(|_| String::new()));
48 let (Some(as_nick), Some(to)) = (as_nick, to) else {
49 fail(2, USAGE);
50 };
51 let text = if words.is_empty() || words == ["-"] {
52 let mut text = String::new();
53 if std::io::stdin().read_to_string(&mut text).is_err() {
54 fail(2, "stdin is not text");
55 }
56 text.trim_end_matches('\n').to_string()
57 } else {
58 words.join(" ")
59 };
60 if text.trim().is_empty() {
61 fail(2, "text is empty");
62 }
63 #[cfg(unix)]
68 if let (Some(handle), Some(sock)) = (std::env::var_os(SIGNER_HANDLE_ENV), std::env::var_os(SIGNER_SOCK_ENV)) {
69 match send_via_signer(std::path::Path::new(&sock), &handle.to_string_lossy(), &to, &text) {
70 Ok(from) => {
71 println!("sent via signer as {from}");
72 return;
73 }
74 Err(e) => fail(1, &format!("signer proxy: {e}")),
75 }
76 }
77 if text.len() > MAX_SEND_BYTES {
78 fail(2, &format!("text is longer than {MAX_SEND_BYTES} bytes"));
79 }
80 load_env_file();
81 let get = |key: &str| std::env::var(key).ok().filter(|value| !value.is_empty());
82 let Some(store_root) = get(STORE_ROOT_ENV).map(PathBuf::from) else {
83 fail(1, "M4A_STORE_ROOT is not set (environment or env file)");
84 };
85 let request = SendRequest {
86 as_nick: as_nick.clone(),
87 to: to.clone(),
88 text: text.clone(),
89 cap: crate::send::session_cap_from_env(),
90 };
91 let sock = send_sock_path(get, &store_root);
92 let reply = match send_via_socket(&sock, &request, Duration::from_secs(180)) {
93 Ok(reply) => reply,
94 Err(_) => {
95 let mut client = match MachineClient::from_env_for(&as_nick) {
97 Ok(client) => client,
98 Err(err) => fail(
99 1,
100 &format!("no running client and opening {as_nick} failed: {err}"),
101 ),
102 };
103 client.send_blocking(&as_nick, &to, &text, Duration::from_secs(120))
104 }
105 };
106 print_reply(&reply);
107}
108
109fn print_reply(reply: &SendReply) {
110 if reply.ok {
111 println!(
112 "sent room={} event={}",
113 reply.room.as_deref().unwrap_or(""),
114 reply.event_id.as_deref().unwrap_or("")
115 );
116 } else {
117 fail(1, reply.error.as_deref().unwrap_or("send failed"));
118 }
119}
120
121pub const SIGNER_HANDLE_ENV: &str = m4a_keys::env::SESSION_HANDLE;
122pub const SIGNER_SOCK_ENV: &str = m4a_keys::env::SIGNER_SOCK;
123
124#[cfg(unix)]
127pub fn send_via_signer(sock: &std::path::Path, handle: &str, to: &str, text: &str) -> Result<String, String> {
128 use std::io::{BufRead, Write};
129 let subject: String = text.lines().next().unwrap_or("").chars().take(80).collect();
130 let letter = serde_json::json!({ "to": to, "subject": subject, "body": text });
131 let payload: String = serde_json::to_vec(&letter).map_err(|e| e.to_string())?.iter().map(|b| format!("{b:02x}")).collect();
132 let mut req = serde_json::to_vec(&serde_json::json!({ "handle": handle, "op": "send", "payload_hex": payload })).map_err(|e| e.to_string())?;
133 req.push(b'\n');
134 let mut s = std::os::unix::net::UnixStream::connect(sock).map_err(|e| e.kind().to_string())?;
135 s.set_read_timeout(Some(Duration::from_secs(30))).ok();
136 s.write_all(&req).map_err(|e| e.kind().to_string())?;
137 let mut line = String::new();
138 std::io::BufReader::new(s).read_line(&mut line).map_err(|e| e.kind().to_string())?;
139 let v: serde_json::Value = serde_json::from_str(&line).map_err(|_| "bad answer".to_string())?;
140 match v["status"].as_str() {
141 Some("sent") => Ok(v["result"]["from"].as_str().unwrap_or("").to_owned()),
142 _ => Err(v["reason"].as_str().unwrap_or("refused").to_owned()),
143 }
144}