1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
use crate::message::{
    header::{self, ContentType},
    IntoBody, SinglePart,
};

/// `SinglePart` builder for attachments
///
/// Allows building attachment parts easily.
#[derive(Clone)]
pub struct Attachment {
    disposition: Disposition,
}

#[derive(Clone)]
enum Disposition {
    /// file name
    Attached(String),
    /// content id
    Inline(String),
}

impl Attachment {
    /// Create a new attachment
    ///
    /// This attachment will be displayed as a normal attachment,
    /// with the chosen `filename` appearing as the file name.
    ///
    /// ```rust
    /// # use std::error::Error;
    /// use std::fs;
    ///
    /// use lettre::message::{Attachment, header::ContentType};
    ///
    /// # fn main() -> Result<(), Box<dyn Error>> {
    /// let filename = String::from("invoice.pdf");
    /// # if false {
    /// let filebody = fs::read("invoice.pdf")?;
    /// # }
    /// # let filebody = fs::read("docs/lettre.png")?;
    /// let content_type = ContentType::parse("application/pdf").unwrap();
    /// let attachment = Attachment::new(filename).body(filebody, content_type);
    ///
    /// // The document `attachment` will show up as a normal attachment.
    /// # Ok(())
    /// # }
    /// ```
    pub fn new(filename: String) -> Self {
        Attachment {
            disposition: Disposition::Attached(filename),
        }
    }

    /// Create a new inline attachment
    ///
    /// This attachment should be displayed inline into the message
    /// body:
    ///
    /// ```html
    /// <img src="cid:123">
    /// ```
    ///
    ///
    /// ```rust
    /// # use std::error::Error;
    /// use std::fs;
    ///
    /// use lettre::message::{Attachment, header::ContentType};
    ///
    /// # fn main() -> Result<(), Box<dyn Error>> {
    /// let content_id = String::from("123");
    /// # if false {
    /// let filebody = fs::read("image.jpg")?;
    /// # }
    /// # let filebody = fs::read("docs/lettre.png")?;
    /// let content_type = ContentType::parse("image/jpeg").unwrap();
    /// let attachment = Attachment::new_inline(content_id).body(filebody, content_type);
    ///
    /// // The image `attachment` will display inline into the email.
    /// # Ok(())
    /// # }
    /// ```
    pub fn new_inline(content_id: String) -> Self {
        Attachment {
            disposition: Disposition::Inline(content_id),
        }
    }

    /// Build the attachment into a [`SinglePart`] which can then be used to build the rest of the email
    ///
    /// Look at the [Complex MIME body example](crate::message#complex-mime-body)
    /// to see how [`SinglePart`] can be put into the email.
    pub fn body<T: IntoBody>(self, content: T, content_type: ContentType) -> SinglePart {
        let mut builder = SinglePart::builder();
        builder = match self.disposition {
            Disposition::Attached(filename) => {
                builder.header(header::ContentDisposition::attachment(&filename))
            }
            Disposition::Inline(content_id) => builder
                .header(header::ContentId::from(format!("<{}>", content_id)))
                .header(header::ContentDisposition::inline()),
        };
        builder = builder.header(content_type);
        builder.body(content)
    }
}

#[cfg(test)]
mod tests {
    use crate::message::header::ContentType;

    #[test]
    fn attachment() {
        let part = super::Attachment::new(String::from("test.txt")).body(
            String::from("Hello world!"),
            ContentType::parse("text/plain").unwrap(),
        );
        assert_eq!(
            &String::from_utf8_lossy(&part.formatted()),
            concat!(
                "Content-Disposition: attachment; filename=\"test.txt\"\r\n",
                "Content-Type: text/plain\r\n",
                "Content-Transfer-Encoding: 7bit\r\n\r\n",
                "Hello world!\r\n",
            )
        );
    }

    #[test]
    fn attachment_inline() {
        let part = super::Attachment::new_inline(String::from("id")).body(
            String::from("Hello world!"),
            ContentType::parse("text/plain").unwrap(),
        );
        assert_eq!(
            &String::from_utf8_lossy(&part.formatted()),
            concat!(
                "Content-ID: <id>\r\n",
                "Content-Disposition: inline\r\n",
                "Content-Type: text/plain\r\n",
                "Content-Transfer-Encoding: 7bit\r\n\r\n",
                "Hello world!\r\n"
            )
        );
    }
}