swadhinbiswas 📧 fix: mail module cleanup - remove dead code, simplify error handling · 7/25/2026
e4f7f83
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()))
})
}
}