mail4agent_messenger_shell/
send.rs1use serde::{Deserialize, Serialize};
15use std::ffi::OsStr;
16use std::io::{BufRead, BufReader, Read, Write};
17use std::path::{Path, PathBuf};
18use std::time::Duration;
19
20use crate::ipc::SendStream;
21
22pub const SEND_SOCK_ENV: &str = "M4A_SEND_SOCK";
24
25pub const DEFAULT_SOCK_NAME: &str = "web-client.sock";
27
28pub const ENV_FILE_ENV: &str = "M4A_ENV_FILE";
34
35pub const MAX_SEND_BYTES: usize = 16 * 1024;
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct SendRequest {
41 #[serde(rename = "as")]
43 pub as_nick: String,
44 pub to: String,
46 pub text: String,
48 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub cap: Option<String>,
52}
53
54pub fn session_cap_from_env() -> Option<String> {
56 std::env::var(crate::session_account::SESSION_CAP_ENV).ok().filter(|c| !c.trim().is_empty())
57}
58
59#[derive(Debug, Clone, Default, Serialize, Deserialize)]
61pub struct SendReply {
62 pub ok: bool,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub room: Option<String>,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub event_id: Option<String>,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub error: Option<String>,
73}
74
75impl SendReply {
76 pub fn failed(error: impl Into<String>) -> Self {
78 Self {
79 error: Some(error.into()),
80 ..Self::default()
81 }
82 }
83}
84
85pub fn send_sock_path(get: impl FnMut(&str) -> Option<String>, store_root: &Path) -> PathBuf {
88 send_sock_path_named(get, store_root, DEFAULT_SOCK_NAME)
89}
90
91pub fn send_sock_path_named(
94 mut get: impl FnMut(&str) -> Option<String>,
95 store_root: &Path,
96 default_name: &str,
97) -> PathBuf {
98 get(SEND_SOCK_ENV)
99 .map(PathBuf::from)
100 .unwrap_or_else(|| store_root.join(default_name))
101}
102
103pub fn load_env_file() -> Option<PathBuf> {
109 load_env_file_named("web-client.env")
110}
111
112pub fn load_env_file_named(default_name: &str) -> Option<PathBuf> {
117 let path = std::env::var(ENV_FILE_ENV)
118 .ok()
119 .filter(|value| !value.is_empty())
120 .map(PathBuf::from)
121 .or_else(|| {
122 home_dir_from(
123 std::env::var_os("HOME").as_deref(),
124 std::env::var_os("USERPROFILE").as_deref(),
125 )
126 .map(|home| home.join(".config/mail4agent").join(default_name))
127 })?;
128 let text = std::fs::read_to_string(&path).ok()?;
129 for (key, value) in parse_env_lines(&text) {
130 if std::env::var_os(&key).is_none() {
131 std::env::set_var(key, value);
132 }
133 }
134 Some(path)
135}
136
137fn home_dir_from(home: Option<&OsStr>, userprofile: Option<&OsStr>) -> Option<PathBuf> {
141 let chosen = home
142 .filter(|value| !value.is_empty())
143 .or_else(|| userprofile.filter(|value| !value.is_empty()))?;
144 Some(PathBuf::from(chosen))
145}
146
147fn parse_env_lines(text: &str) -> Vec<(String, String)> {
148 text.lines()
149 .filter_map(|line| {
150 let line = line.trim();
151 if line.is_empty() || line.starts_with('#') {
152 return None;
153 }
154 let line = line.strip_prefix("export ").unwrap_or(line);
155 let (key, value) = line.split_once('=')?;
156 let key = key.trim();
157 if key.is_empty() || !key.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_') {
158 return None;
159 }
160 let value = value.trim();
161 let value = value
162 .strip_prefix('"')
163 .and_then(|v| v.strip_suffix('"'))
164 .or_else(|| value.strip_prefix('\'').and_then(|v| v.strip_suffix('\'')))
165 .unwrap_or(value);
166 Some((key.to_string(), value.to_string()))
167 })
168 .collect()
169}
170
171pub fn send_via_socket(
176 sock: &Path,
177 request: &SendRequest,
178 wait: Duration,
179) -> std::io::Result<SendReply> {
180 let mut stream = SendStream::connect(sock)?;
181 stream.set_read_timeout(Some(wait))?;
182 stream.set_write_timeout(Some(Duration::from_secs(5)))?;
183 let mut line = serde_json::to_vec(request)?;
184 line.push(b'\n');
185 stream.write_all(&line)?;
186 stream.flush()?;
187 let mut reader = BufReader::new(stream);
188 let mut answer = String::new();
189 reader.read_line(&mut answer)?;
190 if answer.trim().is_empty() {
191 return Err(std::io::Error::new(
192 std::io::ErrorKind::UnexpectedEof,
193 "client closed the socket without an answer",
194 ));
195 }
196 serde_json::from_str(answer.trim())
197 .map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))
198}
199
200pub(crate) enum Incoming {
202 Send(SendRequest),
203 Cmd(crate::CmdRequest),
204}
205
206pub(crate) fn read_incoming(stream: &mut SendStream) -> Result<Incoming, String> {
208 stream.set_nonblocking(false).map_err(|err| err.to_string())?;
209 stream.set_read_timeout(Some(Duration::from_secs(3))).map_err(|err| err.to_string())?;
210 let mut buf = Vec::new();
211 let mut limited = stream.take((MAX_SEND_BYTES * 2 + 1024) as u64);
212 let mut reader = BufReader::new(&mut limited);
213 reader.read_until(b'\n', &mut buf).map_err(|err| err.to_string())?;
214 let value: serde_json::Value =
215 serde_json::from_slice(&buf).map_err(|_| "request is not a JSON line".to_string())?;
216 if value.get("cmd").is_some() {
217 let cmd: crate::CmdRequest =
218 serde_json::from_value(value).map_err(|err| format!("bad command: {err}"))?;
219 return Ok(Incoming::Cmd(cmd));
220 }
221 let request: SendRequest =
222 serde_json::from_value(value).map_err(|_| "request is not a send JSON line".to_string())?;
223 if request.text.trim().is_empty() {
224 return Err("text is empty".to_string());
225 }
226 if request.text.len() > MAX_SEND_BYTES {
227 return Err(format!("text is longer than {MAX_SEND_BYTES} bytes"));
228 }
229 Ok(Incoming::Send(request))
230}
231
232pub fn send_cmd_via_socket(
234 sock: &Path,
235 request: &crate::CmdRequest,
236 wait: Duration,
237) -> std::io::Result<crate::CmdReply> {
238 let mut stream = SendStream::connect(sock)?;
239 stream.set_read_timeout(Some(wait))?;
240 stream.set_write_timeout(Some(Duration::from_secs(5)))?;
241 let mut line = serde_json::to_vec(request)?;
242 line.push(b'\n');
243 stream.write_all(&line)?;
244 stream.flush()?;
245 let mut reader = BufReader::new(stream);
246 let mut answer = String::new();
247 reader.read_line(&mut answer)?;
248 if answer.trim().is_empty() {
249 return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "client closed the socket without an answer"));
250 }
251 serde_json::from_str(answer.trim()).map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))
252}
253
254pub(crate) fn write_cmd_reply(stream: &mut SendStream, reply: &crate::CmdReply) {
256 if let Ok(mut line) = serde_json::to_vec(reply) {
257 line.push(b'\n');
258 let _ = stream.set_write_timeout(Some(Duration::from_secs(3)));
259 let _ = stream.write_all(&line);
260 let _ = stream.flush();
261 }
262}
263
264pub(crate) fn read_request(stream: &mut SendStream) -> Result<SendRequest, String> {
266 stream
267 .set_nonblocking(false)
268 .map_err(|err| err.to_string())?;
269 stream
270 .set_read_timeout(Some(Duration::from_secs(3)))
271 .map_err(|err| err.to_string())?;
272 let mut buf = Vec::new();
273 let mut limited = stream.take((MAX_SEND_BYTES * 2 + 1024) as u64);
274 let mut reader = BufReader::new(&mut limited);
275 reader
276 .read_until(b'\n', &mut buf)
277 .map_err(|err| err.to_string())?;
278 let request: SendRequest =
279 serde_json::from_slice(&buf).map_err(|_| "request is not a send JSON line".to_string())?;
280 if request.text.trim().is_empty() {
281 return Err("text is empty".to_string());
282 }
283 if request.text.len() > MAX_SEND_BYTES {
284 return Err(format!("text is longer than {MAX_SEND_BYTES} bytes"));
285 }
286 Ok(request)
287}
288
289pub(crate) fn write_reply(stream: &mut SendStream, reply: &SendReply) {
291 if let Ok(mut line) = serde_json::to_vec(reply) {
292 line.push(b'\n');
293 let _ = stream.set_write_timeout(Some(Duration::from_secs(3)));
294 let _ = stream.write_all(&line);
295 let _ = stream.flush();
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use std::ffi::OsStr;
302
303 use super::*;
304
305 #[test]
306 fn env_lines_skip_comments_and_strip_quotes() {
307 let parsed = parse_env_lines(
308 "# c\n\nM4A_A=1\nexport M4A_B=\"two words\"\nM4A_C='x'\nbad line\n=v\n",
309 );
310 assert_eq!(
311 parsed,
312 vec![
313 ("M4A_A".to_string(), "1".to_string()),
314 ("M4A_B".to_string(), "two words".to_string()),
315 ("M4A_C".to_string(), "x".to_string()),
316 ]
317 );
318 }
319
320 #[test]
321 fn home_dir_prefers_home_then_userprofile() {
322 assert_eq!(
323 home_dir_from(
324 Some(OsStr::new("/from-home")),
325 Some(OsStr::new("/from-profile"))
326 ),
327 Some(PathBuf::from("/from-home"))
328 );
329 assert_eq!(
330 home_dir_from(None, Some(OsStr::new("/from-profile"))),
331 Some(PathBuf::from("/from-profile"))
332 );
333 assert_eq!(
334 home_dir_from(Some(OsStr::new("")), Some(OsStr::new("/from-profile"))),
335 Some(PathBuf::from("/from-profile"))
336 );
337 assert_eq!(home_dir_from(None, None), None);
338 assert_eq!(
339 home_dir_from(Some(OsStr::new("")), Some(OsStr::new(""))),
340 None
341 );
342 }
343
344 #[test]
345 fn request_uses_as_on_the_wire() {
346 let request = SendRequest {
347 as_nick: "alice".to_string(),
348 to: "privet-mir".to_string(),
349 text: "hi".to_string(),
350 cap: None,
351 };
352 let json = serde_json::to_value(&request).expect("json");
353 assert_eq!(json["as"], "alice");
354 assert_eq!(json["to"], "privet-mir");
355 }
356}