lettre/message/
attachment.rs

1use crate::message::{
2    header::{self, ContentType},
3    IntoBody, SinglePart,
4};
5
6/// `SinglePart` builder for attachments
7///
8/// Allows building attachment parts easily.
9#[derive(Clone)]
10pub struct Attachment {
11    disposition: Disposition,
12}
13
14#[derive(Clone)]
15enum Disposition {
16    /// File name
17    Attached(String),
18    /// Content id
19    Inline {
20        content_id: String,
21        name: Option<String>,
22    },
23}
24
25impl Attachment {
26    /// Create a new attachment
27    ///
28    /// This attachment will be displayed as a normal attachment,
29    /// with the chosen `filename` appearing as the file name.
30    ///
31    /// ```rust
32    /// # use std::error::Error;
33    /// use std::fs;
34    ///
35    /// use lettre::message::{header::ContentType, Attachment};
36    ///
37    /// # fn main() -> Result<(), Box<dyn Error>> {
38    /// let filename = String::from("invoice.pdf");
39    /// # if false {
40    /// let filebody = fs::read("invoice.pdf")?;
41    /// # }
42    /// # let filebody = fs::read("docs/lettre.png")?;
43    /// let content_type = ContentType::parse("application/pdf").unwrap();
44    /// let attachment = Attachment::new(filename).body(filebody, content_type);
45    ///
46    /// // The document `attachment` will show up as a normal attachment.
47    /// # Ok(())
48    /// # }
49    /// ```
50    pub fn new(filename: String) -> Self {
51        Attachment {
52            disposition: Disposition::Attached(filename),
53        }
54    }
55
56    /// Create a new inline attachment
57    ///
58    /// This attachment should be displayed inline into the message
59    /// body:
60    ///
61    /// ```html
62    /// <img src="cid:123">
63    /// ```
64    ///
65    ///
66    /// ```rust
67    /// # use std::error::Error;
68    /// use std::fs;
69    ///
70    /// use lettre::message::{header::ContentType, Attachment};
71    ///
72    /// # fn main() -> Result<(), Box<dyn Error>> {
73    /// let content_id = String::from("123");
74    /// # if false {
75    /// let filebody = fs::read("image.jpg")?;
76    /// # }
77    /// # let filebody = fs::read("docs/lettre.png")?;
78    /// let content_type = ContentType::parse("image/jpeg").unwrap();
79    /// let attachment = Attachment::new_inline(content_id).body(filebody, content_type);
80    ///
81    /// // The image `attachment` will display inline into the email.
82    /// # Ok(())
83    /// # }
84    /// ```
85    pub fn new_inline(content_id: String) -> Self {
86        Attachment {
87            disposition: Disposition::Inline {
88                content_id,
89                name: None,
90            },
91        }
92    }
93
94    /// Create a new inline attachment giving it a name
95    ///
96    /// This attachment should be displayed inline into the message
97    /// body:
98    ///
99    /// ```html
100    /// <img src="cid:123">
101    /// ```
102    ///
103    ///
104    /// ```rust
105    /// # use std::error::Error;
106    /// use std::fs;
107    ///
108    /// use lettre::message::{header::ContentType, Attachment};
109    ///
110    /// # fn main() -> Result<(), Box<dyn Error>> {
111    /// let content_id = String::from("123");
112    /// let file_name = String::from("image.jpg");
113    /// # if false {
114    /// let filebody = fs::read(&file_name)?;
115    /// # }
116    /// # let filebody = fs::read("docs/lettre.png")?;
117    /// let content_type = ContentType::parse("image/jpeg").unwrap();
118    /// let attachment =
119    ///     Attachment::new_inline_with_name(content_id, file_name).body(filebody, content_type);
120    ///
121    /// // The image `attachment` will display inline into the email.
122    /// # Ok(())
123    /// # }
124    /// ```
125    pub fn new_inline_with_name(content_id: String, name: String) -> Self {
126        Attachment {
127            disposition: Disposition::Inline {
128                content_id,
129                name: Some(name),
130            },
131        }
132    }
133
134    /// Build the attachment into a [`SinglePart`] which can then be used to build the rest of the email
135    ///
136    /// Look at the [Complex MIME body example](crate::message#complex-mime-body)
137    /// to see how [`SinglePart`] can be put into the email.
138    pub fn body<T: IntoBody>(self, content: T, content_type: ContentType) -> SinglePart {
139        let mut builder = SinglePart::builder();
140        builder = match self.disposition {
141            Disposition::Attached(filename) => {
142                builder.header(header::ContentDisposition::attachment(&filename))
143            }
144            Disposition::Inline {
145                content_id,
146                name: None,
147            } => builder
148                .header(header::ContentId::from(format!("<{content_id}>")))
149                .header(header::ContentDisposition::inline()),
150            Disposition::Inline {
151                content_id,
152                name: Some(name),
153            } => builder
154                .header(header::ContentId::from(format!("<{content_id}>")))
155                .header(header::ContentDisposition::inline_with_name(&name)),
156        };
157        builder = builder.header(content_type);
158        builder.body(content)
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use crate::message::header::ContentType;
165
166    #[test]
167    fn attachment() {
168        let part = super::Attachment::new(String::from("test.txt")).body(
169            String::from("Hello world!"),
170            ContentType::parse("text/plain").unwrap(),
171        );
172        assert_eq!(
173            &String::from_utf8_lossy(&part.formatted()),
174            concat!(
175                "Content-Disposition: attachment; filename=\"test.txt\"\r\n",
176                "Content-Type: text/plain\r\n",
177                "Content-Transfer-Encoding: 7bit\r\n\r\n",
178                "Hello world!\r\n",
179            )
180        );
181    }
182
183    #[test]
184    fn attachment_inline() {
185        let part = super::Attachment::new_inline(String::from("id")).body(
186            String::from("Hello world!"),
187            ContentType::parse("text/plain").unwrap(),
188        );
189        assert_eq!(
190            &String::from_utf8_lossy(&part.formatted()),
191            concat!(
192                "Content-ID: <id>\r\n",
193                "Content-Disposition: inline\r\n",
194                "Content-Type: text/plain\r\n",
195                "Content-Transfer-Encoding: 7bit\r\n\r\n",
196                "Hello world!\r\n"
197            )
198        );
199    }
200
201    #[test]
202    fn attachment_inline_with_name() {
203        let id = String::from("id");
204        let name = String::from("test");
205        let part = super::Attachment::new_inline_with_name(id, name).body(
206            String::from("Hello world!"),
207            ContentType::parse("text/plain").unwrap(),
208        );
209        assert_eq!(
210            &String::from_utf8_lossy(&part.formatted()),
211            concat!(
212                "Content-ID: <id>\r\n",
213                "Content-Disposition: inline; filename=\"test\"\r\n",
214                "Content-Type: text/plain\r\n",
215                "Content-Transfer-Encoding: 7bit\r\n\r\n",
216                "Hello world!\r\n"
217            )
218        );
219    }
220}