swadhinbiswas 📧 fix: mail module cleanup - remove dead code, simplify error handling · 7/25/2026
e4f7f83
121 lines · 3.7 KB
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
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::types::PyDict;

use justapi_core::mail::smtp::SmtpSender as CoreSmtpSender;
use justapi_core::mail::SmtpConfig;

use crate::message_builder;

/// SMTP mail sender with optional template rendering.
///
/// Usage:
///
/// ```python
/// from justapi import Mailer
/// mailer = Mailer(host="smtp.example.com", port=587, username="user",
///                 password="pass", default_from="[email protected]")
/// mailer.send(to="[email protected]", subject="Hello", body="World")
/// ```
#[pyclass(name = "Mailer")]
pub struct PyMailer {
    sender: CoreSmtpSender,
}

#[pymethods]
impl PyMailer {
    #[new]
    #[pyo3(signature = (host, port=587, username=None, password=None, use_tls=true, default_from=None, default_from_name=None))]
    fn py_new(
        host: String,
        port: u16,
        username: Option<String>,
        password: Option<String>,
        use_tls: bool,
        default_from: Option<String>,
        default_from_name: Option<String>,
    ) -> Self {
        let from = default_from.unwrap_or_else(|| format!("noreply@{}.invalid", host));
        let mut config = SmtpConfig::new(host, port, from);
        if let (Some(user), Some(pass)) = (&username, &password) {
            config = config.credentials(user.clone(), pass.clone());
        }
        if !use_tls {
            config = config.no_tls();
        }
        config.default_from_name = default_from_name;
        let sender = CoreSmtpSender::new(config);
        Self { sender }
    }

    /// Send an email synchronously. Blocks the current thread.
    #[pyo3(signature = (to, subject, body=None, html=None, cc=None, bcc=None, reply_to=None, attachments=None))]
    fn send(
        &self,
        to: String,
        subject: String,
        body: Option<String>,
        html: Option<String>,
        cc: Option<Vec<String>>,
        bcc: Option<Vec<String>>,
        reply_to: Option<String>,
        attachments: Option<Vec<Py<PyDict>>>,
        py: Python<'_>,
    ) -> PyResult<()> {
        let msg = message_builder::build_message(
            to,
            subject,
            body,
            html,
            cc,
            bcc,
            reply_to,
            attachments,
            py,
        )?;
        let sender = self.sender.clone();
        let rt = tokio::runtime::Runtime::new()
            .map_err(|e| PyValueError::new_err(format!("Failed to create runtime: {e}")))?;
        rt.block_on(async {
            sender
                .send(&msg)
                .await
                .map_err(|e| PyValueError::new_err(format!("Failed to send email: {e}")))
        })
    }

    /// Send an email asynchronously. Returns a coroutine.
    #[pyo3(signature = (to, subject, body=None, html=None, cc=None, bcc=None, reply_to=None, attachments=None))]
    fn send_async<'py>(
        &'py self,
        to: String,
        subject: String,
        body: Option<String>,
        html: Option<String>,
        cc: Option<Vec<String>>,
        bcc: Option<Vec<String>>,
        reply_to: Option<String>,
        attachments: Option<Vec<Py<PyDict>>>,
        py: Python<'py>,
    ) -> PyResult<Bound<'py, PyAny>> {
        let msg = message_builder::build_message(
            to,
            subject,
            body,
            html,
            cc,
            bcc,
            reply_to,
            attachments,
            py,
        )?;
        let sender = self.sender.clone();
        let fut = async move {
            sender.send(&msg).await.map_err(|e| anyhow::anyhow!("Failed to send email: {e}"))
        };
        pyo3_async_runtimes::tokio::future_into_py(py, async move {
            fut.await.map_err(|e| PyValueError::new_err(e.to_string()))
        })
    }
}