mxr_compose/
attachments.rs1use crate::frontmatter::ComposeError;
2use std::path::{Path, PathBuf};
3
4pub fn resolve_attachments(paths: &[String]) -> Result<Vec<ResolvedAttachment>, ComposeError> {
7 paths.iter().map(|p| resolve_one_str(p)).collect()
8}
9
10pub fn resolve_attachment_paths(
11 paths: &[PathBuf],
12) -> Result<Vec<ResolvedAttachment>, ComposeError> {
13 paths
14 .iter()
15 .map(PathBuf::as_path)
16 .map(resolve_one_path)
17 .collect()
18}
19
20#[derive(Debug)]
21pub struct ResolvedAttachment {
22 pub path: PathBuf,
23 pub filename: String,
24 pub mime_type: String,
25}
26
27fn resolve_one_str(path_str: &str) -> Result<ResolvedAttachment, ComposeError> {
28 let expanded = expand_tilde(path_str);
29 let path = PathBuf::from(&expanded);
30 resolve_one_path(&path).map_err(|err| match err {
31 ComposeError::AttachmentNotFound(_) => {
32 ComposeError::AttachmentNotFound(path_str.to_string())
33 }
34 other => other,
35 })
36}
37
38fn resolve_one_path(path: &Path) -> Result<ResolvedAttachment, ComposeError> {
39 let path = path.to_path_buf();
40
41 if !path.exists() {
42 return Err(ComposeError::AttachmentNotFound(path.display().to_string()));
43 }
44
45 let filename = path
46 .file_name()
47 .map(|n| n.to_string_lossy().to_string())
48 .unwrap_or_else(|| "attachment".to_string());
49
50 let mime_type = match path.extension().and_then(|e| e.to_str()) {
51 Some("pdf") => "application/pdf",
52 Some("png") => "image/png",
53 Some("jpg" | "jpeg") => "image/jpeg",
54 Some("gif") => "image/gif",
55 Some("txt") => "text/plain",
56 Some("csv") => "text/csv",
57 Some("html" | "htm") => "text/html",
58 Some("zip") => "application/zip",
59 Some("doc") => "application/msword",
60 Some("docx") => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
61 Some("xls") => "application/vnd.ms-excel",
62 Some("xlsx") => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
63 _ => "application/octet-stream",
64 }
65 .to_string();
66
67 Ok(ResolvedAttachment {
68 path,
69 filename,
70 mime_type,
71 })
72}
73
74fn expand_tilde(path: &str) -> String {
75 if path.starts_with("~/") {
76 if let Some(home) = dirs::home_dir() {
77 return format!("{}{}", home.display(), &path[1..]);
78 }
79 }
80 path.to_string()
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86
87 #[test]
88 fn attachment_not_found_error() {
89 let result = resolve_one_str("/nonexistent/file.pdf");
90 assert!(result.is_err());
91 match result.unwrap_err() {
92 ComposeError::AttachmentNotFound(path) => {
93 assert_eq!(path, "/nonexistent/file.pdf");
94 }
95 e => panic!("Expected AttachmentNotFound, got: {e}"),
96 }
97 }
98
99 #[test]
100 fn attachment_found_with_correct_mime() {
101 let tmp = tempfile::NamedTempFile::with_suffix(".pdf").unwrap();
102 let result = resolve_one_str(tmp.path().to_str().unwrap()).unwrap();
103 assert_eq!(result.mime_type, "application/pdf");
104 }
105
106 #[test]
107 fn tilde_expansion() {
108 let expanded = expand_tilde("~/Documents/test.txt");
109 assert!(!expanded.starts_with('~'));
110 assert!(expanded.contains("Documents/test.txt"));
111 }
112
113 #[test]
114 fn no_tilde_passthrough() {
115 let path = "/absolute/path/file.txt";
116 assert_eq!(expand_tilde(path), path);
117 }
118}