caldir_core/event/
attachment.rs1use icalendar::Property;
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub struct Attachment {
5 pub uri: String,
6 pub params: Vec<(String, String)>,
7}
8
9impl Attachment {
10 pub fn new(uri: impl Into<String>) -> Self {
11 Attachment {
12 uri: uri.into(),
13 params: Vec::new(),
14 }
15 }
16
17 pub fn filename(&self) -> Option<&str> {
21 self.params
22 .iter()
23 .find(|(k, _)| k.eq_ignore_ascii_case("FILENAME"))
24 .map(|(_, v)| v.as_str())
25 }
26
27 pub fn from_property(prop: &Property) -> Option<Self> {
31 if is_binary(prop) {
32 return None;
33 }
34
35 let params = prop
36 .params()
37 .values()
38 .map(|p| (p.key().to_string(), p.value().to_string()))
39 .collect();
40
41 Some(Attachment {
42 uri: prop.value().to_string(),
43 params,
44 })
45 }
46}
47
48fn is_binary(prop: &Property) -> bool {
49 prop.params().values().any(|p| {
50 (p.key().eq_ignore_ascii_case("VALUE") && p.value().eq_ignore_ascii_case("BINARY"))
51 || (p.key().eq_ignore_ascii_case("ENCODING")
52 && p.value().eq_ignore_ascii_case("BASE64"))
53 })
54}
55
56impl From<&Attachment> for Property {
57 fn from(value: &Attachment) -> Self {
58 let mut prop = Property::new("ATTACH", &value.uri);
59 for (k, v) in &value.params {
60 prop.add_parameter(k, v);
61 }
62 prop.done()
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 #[test]
71 fn keeps_uri_value() {
72 let prop = Property::new("ATTACH", "https://drive.google.com/file/d/abc").done();
73
74 let attachment = Attachment::from_property(&prop).unwrap();
75
76 assert_eq!(attachment.uri, "https://drive.google.com/file/d/abc");
77 assert!(attachment.params.is_empty());
78 }
79
80 #[test]
81 fn captures_params() {
82 let mut prop = Property::new("ATTACH", "https://p01.icloud.com/att/abc");
85 prop.add_parameter("MANAGED-ID", "1");
86 prop.add_parameter("FMTTYPE", "application/pdf");
87 prop.add_parameter("FILENAME", "agenda.pdf");
88
89 let attachment = Attachment::from_property(&prop.done()).unwrap();
90
91 assert!(
92 attachment
93 .params
94 .contains(&("MANAGED-ID".to_string(), "1".to_string()))
95 );
96 assert!(
97 attachment
98 .params
99 .contains(&("FILENAME".to_string(), "agenda.pdf".to_string()))
100 );
101 }
102
103 #[test]
104 fn filename_reads_param_case_insensitively() {
105 let mut prop = Property::new("ATTACH", "https://p01.icloud.com/att/abc");
106 prop.add_parameter("filename", "agenda.pdf");
107
108 let attachment = Attachment::from_property(&prop.done()).unwrap();
109
110 assert_eq!(attachment.filename(), Some("agenda.pdf"));
111 }
112
113 #[test]
114 fn filename_is_none_without_param() {
115 let attachment = Attachment::new("https://drive.google.com/file/d/abc");
116
117 assert_eq!(attachment.filename(), None);
118 }
119
120 #[test]
121 fn skips_binary_value() {
122 let mut prop = Property::new("ATTACH", "VGhlIHF1aWNr");
123 prop.add_parameter("VALUE", "BINARY");
124 prop.add_parameter("ENCODING", "BASE64");
125
126 assert!(Attachment::from_property(&prop.done()).is_none());
127 }
128
129 #[test]
130 fn skips_base64_encoding_without_value_param() {
131 let mut prop = Property::new("ATTACH", "VGhlIHF1aWNr");
132 prop.add_parameter("ENCODING", "BASE64");
133
134 assert!(Attachment::from_property(&prop.done()).is_none());
135 }
136
137 #[test]
138 fn writes_attach_property_with_params() {
139 let attachment = Attachment {
140 uri: "https://example.com/report.doc".to_string(),
141 params: vec![("FMTTYPE".to_string(), "application/msword".to_string())],
142 };
143
144 let prop = Property::from(&attachment);
145
146 assert_eq!(prop.key(), "ATTACH");
147 assert_eq!(prop.value(), "https://example.com/report.doc");
148 assert_eq!(
149 prop.params().get("FMTTYPE").map(|p| p.value()),
150 Some("application/msword")
151 );
152 }
153}