Skip to main content

rustbasic_core/
mail.rs

1/* ---------------------------------------------------------
2 * 📧 LABEL: MAIL SERVICE (src/mail.rs)
3 * Menangani pengiriman email dengan SMTP menggunakan Lettre.
4 * Menyediakan Mailer fluent builder API yang kaya fitur.
5 * --------------------------------------------------------- */
6
7use std::error::Error;
8use lettre::transport::smtp::authentication::Credentials;
9use lettre::{Message, AsyncSmtpTransport, AsyncTransport, Tokio1Executor};
10use crate::app::Config;
11
12/// Representasi attachment / lampiran email
13#[derive(Debug, Clone)]
14pub struct MailAttachment {
15    pub name: String,
16    pub body: Vec<u8>,
17    pub content_type: String,
18}
19
20/// Fluent Builder untuk menyusun dan mengirim Email
21#[derive(Debug, Clone)]
22pub struct Mailer {
23    to: Vec<String>,
24    cc: Vec<String>,
25    bcc: Vec<String>,
26    subject: Option<String>,
27    html_body: Option<String>,
28    text_body: Option<String>,
29    from_name: Option<String>,
30    from_address: Option<String>,
31    attachments: Vec<MailAttachment>,
32}
33
34impl Default for Mailer {
35    fn default() -> Self {
36        Self::new()
37    }
38}
39
40impl Mailer {
41    /// Membuat instance baru Mailer
42    pub fn new() -> Self {
43        Self {
44            to: Vec::new(),
45            cc: Vec::new(),
46            bcc: Vec::new(),
47            subject: None,
48            html_body: None,
49            text_body: None,
50            from_name: None,
51            from_address: None,
52            attachments: Vec::new(),
53        }
54    }
55
56    /// Menambahkan penerima utama (To)
57    pub fn to(mut self, to: impl Into<String>) -> Self {
58        self.to.push(to.into());
59        self
60    }
61
62    /// Menambahkan CC (Carbon Copy)
63    pub fn cc(mut self, cc: impl Into<String>) -> Self {
64        self.cc.push(cc.into());
65        self
66    }
67
68    /// Menambahkan BCC (Blind Carbon Copy)
69    pub fn bcc(mut self, bcc: impl Into<String>) -> Self {
70        self.bcc.push(bcc.into());
71        self
72    }
73
74    /// Mengatur subjek email
75    pub fn subject(mut self, subject: impl Into<String>) -> Self {
76        self.subject = Some(subject.into());
77        self
78    }
79
80    /// Mengatur konten HTML body email
81    pub fn html(mut self, body: impl Into<String>) -> Self {
82        self.html_body = Some(body.into());
83        self
84    }
85
86    /// Mengatur konten Text biasa body email
87    pub fn text(mut self, body: impl Into<String>) -> Self {
88        self.text_body = Some(body.into());
89        self
90    }
91
92    /// Mengatur kustom pengirim (From) untuk email ini
93    pub fn from(mut self, name: impl Into<String>, address: impl Into<String>) -> Self {
94        self.from_name = Some(name.into());
95        self.from_address = Some(address.into());
96        self
97    }
98
99    /// Melampirkan file berkas ke email
100    pub fn attach(mut self, name: impl Into<String>, body: Vec<u8>, content_type: impl Into<String>) -> Self {
101        self.attachments.push(MailAttachment {
102            name: name.into(),
103            body,
104            content_type: content_type.into(),
105        });
106        self
107    }
108
109    /// Mengirim email secara asinkron menggunakan SMTP relay dari Config
110    pub async fn send(self) -> Result<(), Box<dyn Error + Send + Sync>> {
111        let config = Config::load();
112
113        let from_name = self.from_name.unwrap_or(config.mail_from_name);
114        let from_address = self.from_address.unwrap_or(config.mail_from_address);
115
116        let mut builder = Message::builder()
117            .from(format!("{} <{}>", from_name, from_address).parse()?);
118
119        if self.to.is_empty() {
120            return Err("At least one recipient ('to') must be specified".into());
121        }
122
123        for recipient in self.to {
124            builder = builder.to(recipient.parse()?);
125        }
126
127        for cc_rec in self.cc {
128            builder = builder.cc(cc_rec.parse()?);
129        }
130
131        for bcc_rec in self.bcc {
132            builder = builder.bcc(bcc_rec.parse()?);
133        }
134
135        if let Some(sub) = self.subject {
136            builder = builder.subject(sub);
137        }
138
139        // Tentukan body (Text / HTML / Alternative)
140        let email = match (self.html_body, self.text_body) {
141            (Some(html), Some(text)) => {
142                let alt = lettre::message::MultiPart::alternative()
143                    .singlepart(lettre::message::SinglePart::plain(text))
144                    .singlepart(lettre::message::SinglePart::html(html));
145                if !self.attachments.is_empty() {
146                    let mut mixed = lettre::message::MultiPart::mixed().multipart(alt);
147                    for att in self.attachments {
148                        let mime = att.content_type.parse::<lettre::message::header::ContentType>()?;
149                        let single_part = lettre::message::SinglePart::builder()
150                            .header(mime)
151                            .header(lettre::message::header::ContentDisposition::attachment(&att.name))
152                            .body(att.body);
153                        mixed = mixed.singlepart(single_part);
154                    }
155                    builder.multipart(mixed)?
156                } else {
157                    builder.multipart(alt)?
158                }
159            }
160            (Some(html), None) => {
161                if !self.attachments.is_empty() {
162                    let alt = lettre::message::MultiPart::alternative()
163                        .singlepart(lettre::message::SinglePart::html(html));
164                    let mut mixed = lettre::message::MultiPart::mixed().multipart(alt);
165                    for att in self.attachments {
166                        let mime = att.content_type.parse::<lettre::message::header::ContentType>()?;
167                        let single_part = lettre::message::SinglePart::builder()
168                            .header(mime)
169                            .header(lettre::message::header::ContentDisposition::attachment(&att.name))
170                            .body(att.body);
171                        mixed = mixed.singlepart(single_part);
172                    }
173                    builder.multipart(mixed)?
174                } else {
175                    builder
176                        .header(lettre::message::header::ContentType::TEXT_HTML)
177                        .body(html)?
178                }
179            }
180            (None, Some(text)) => {
181                if !self.attachments.is_empty() {
182                    let alt = lettre::message::MultiPart::alternative()
183                        .singlepart(lettre::message::SinglePart::plain(text));
184                    let mut mixed = lettre::message::MultiPart::mixed().multipart(alt);
185                    for att in self.attachments {
186                        let mime = att.content_type.parse::<lettre::message::header::ContentType>()?;
187                        let single_part = lettre::message::SinglePart::builder()
188                            .header(mime)
189                            .header(lettre::message::header::ContentDisposition::attachment(&att.name))
190                            .body(att.body);
191                        mixed = mixed.singlepart(single_part);
192                    }
193                    builder.multipart(mixed)?
194                } else {
195                    builder
196                        .header(lettre::message::header::ContentType::TEXT_PLAIN)
197                        .body(text)?
198                }
199            }
200            (None, None) => {
201                return Err("Email body (HTML or Text) is required".into());
202            }
203        };
204
205        // Konfigurasi SMTP Transport (Async)
206        let creds = Credentials::new(config.mail_username, config.mail_password);
207
208        let mailer = AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(&config.mail_host)?
209            .port(config.mail_port)
210            .credentials(creds)
211            .build();
212
213        mailer.send(email).await?;
214
215        Ok(())
216    }
217}
218
219/// Helper kompatibilitas lama
220pub struct MailService;
221
222impl MailService {
223    /// Mengirim email secara asinkron menggunakan SMTP
224    pub async fn send_email(
225        to: &str,
226        subject: &str,
227        body: &str,
228    ) -> Result<(), Box<dyn Error + Send + Sync>> {
229        Mailer::new()
230            .to(to)
231            .subject(subject)
232            .html(body)
233            .send()
234            .await
235    }
236}