1 use std
::time
::Duration
;
3 use lettre
::message
::header
::{HeaderName, HeaderValue}
;
4 use lettre
::message
::{Mailbox, MultiPart, SinglePart}
;
5 use lettre
::transport
::smtp
::client
::{Tls, TlsParameters}
;
6 use lettre
::{message::header::ContentType, Message, SmtpTransport, Transport}
;
7 use serde
::{Deserialize, Serialize}
;
9 use proxmox_schema
::api_types
::COMMENT_SCHEMA
;
10 use proxmox_schema
::{api, Updater}
;
12 use crate::context
::context
;
13 use crate::endpoints
::common
::mail
;
14 use crate::renderer
::TemplateType
;
15 use crate::schema
::{EMAIL_SCHEMA, ENTITY_NAME_SCHEMA, USER_SCHEMA}
;
16 use crate::{renderer, Content, Endpoint, Error, Notification, Origin}
;
18 pub(crate) const SMTP_TYPENAME
: &str = "smtp";
20 const SMTP_PORT
: u16 = 25;
21 const SMTP_SUBMISSION_STARTTLS_PORT
: u16 = 587;
22 const SMTP_SUBMISSION_TLS_PORT
: u16 = 465;
23 const SMTP_TIMEOUT
: u16 = 5;
26 #[derive(Debug, Serialize, Deserialize, Default, Clone, Copy)]
27 #[serde(rename_all = "kebab-case")]
28 /// Connection security
30 /// No encryption (insecure), plain SMTP
32 /// Upgrade to TLS after connecting
33 #[serde(rename = "starttls")]
35 /// Use TLS-secured connection
43 schema
: ENTITY_NAME_SCHEMA
,
61 schema
: COMMENT_SCHEMA
,
65 #[derive(Debug, Serialize, Deserialize, Updater, Default)]
66 #[serde(rename_all = "kebab-case")]
67 /// Config for Sendmail notification endpoints
68 pub struct SmtpConfig
{
69 /// Name of the endpoint
72 /// Host name or IP of the SMTP relay
74 /// Port to use when connecting to the SMTP relay
75 #[serde(skip_serializing_if = "Option::is_none")]
76 pub port
: Option
<u16>,
77 #[serde(skip_serializing_if = "Option::is_none")]
78 pub mode
: Option
<SmtpMode
>,
79 /// Username for authentication
80 #[serde(skip_serializing_if = "Option::is_none")]
81 pub username
: Option
<String
>,
83 #[serde(default, skip_serializing_if = "Vec::is_empty")]
84 #[updater(serde(skip_serializing_if = "Option::is_none"))]
85 pub mailto
: Vec
<String
>,
87 #[serde(default, skip_serializing_if = "Vec::is_empty")]
88 #[updater(serde(skip_serializing_if = "Option::is_none"))]
89 pub mailto_user
: Vec
<String
>,
90 /// `From` address for the mail
91 pub from_address
: String
,
92 /// Author of the mail
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub author
: Option
<String
>,
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub comment
: Option
<String
>,
98 /// Disable this target.
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub disable
: Option
<bool
>,
101 /// Origin of this config entry.
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub origin
: Option
<Origin
>,
108 #[derive(Serialize, Deserialize)]
109 #[serde(rename_all = "kebab-case")]
110 pub enum DeleteableSmtpProperty
{
119 /// Delete `mailto-user`
121 /// Delete `password`
125 /// Delete `username`
130 #[derive(Serialize, Deserialize, Clone, Updater, Debug)]
131 #[serde(rename_all = "kebab-case")]
132 /// Private configuration for SMTP notification endpoints.
133 /// This config will be saved to a separate configuration file with stricter
134 /// permissions (root:root 0600)
135 pub struct SmtpPrivateConfig
{
136 /// Name of the endpoint
139 /// Authentication token
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub password
: Option
<String
>,
144 /// A sendmail notification endpoint.
145 pub struct SmtpEndpoint
{
146 pub config
: SmtpConfig
,
147 pub private_config
: SmtpPrivateConfig
,
150 impl Endpoint
for SmtpEndpoint
{
151 fn send(&self, notification
: &Notification
) -> Result
<(), Error
> {
152 let tls_parameters
= TlsParameters
::new(self.config
.server
.clone())
153 .map_err(|err
| Error
::NotifyFailed(self.name().into(), Box
::new(err
)))?
;
155 let (port
, tls
) = match self.config
.mode
.unwrap_or_default() {
156 SmtpMode
::Insecure
=> {
157 let port
= self.config
.port
.unwrap_or(SMTP_PORT
);
160 SmtpMode
::StartTls
=> {
161 let port
= self.config
.port
.unwrap_or(SMTP_SUBMISSION_STARTTLS_PORT
);
162 (port
, Tls
::Required(tls_parameters
))
165 let port
= self.config
.port
.unwrap_or(SMTP_SUBMISSION_TLS_PORT
);
166 (port
, Tls
::Wrapper(tls_parameters
))
170 let mut transport_builder
= SmtpTransport
::builder_dangerous(&self.config
.server
)
173 .timeout(Some(Duration
::from_secs(SMTP_TIMEOUT
.into())));
175 if let Some(username
) = self.config
.username
.as_deref() {
176 if let Some(password
) = self.private_config
.password
.as_deref() {
177 transport_builder
= transport_builder
.credentials((username
, password
).into());
179 return Err(Error
::NotifyFailed(
181 Box
::new(Error
::Generic(
182 "username is set but no password was provided".to_owned(),
188 let transport
= transport_builder
.build();
190 let recipients
= mail
::get_recipients(
191 self.config
.mailto
.as_slice(),
192 self.config
.mailto_user
.as_slice(),
194 let mail_from
= self.config
.from_address
.clone();
196 let parse_address
= |addr
: &str| -> Result
<Mailbox
, Error
> {
198 .map_err(|err
| Error
::NotifyFailed(self.name().into(), Box
::new(err
)))
205 .unwrap_or_else(|| context().default_sendmail_author());
207 let mut email_builder
=
208 Message
::builder().from(parse_address(&format
!("{author} <{mail_from}>"))?
);
210 for recipient
in recipients
{
211 email_builder
= email_builder
.to(parse_address(&recipient
)?
);
214 let mut email
= match ¬ification
.content
{
220 renderer
::render_template(TemplateType
::Subject
, template_name
, data
)?
;
222 renderer
::render_template(TemplateType
::HtmlBody
, template_name
, data
)?
;
224 renderer
::render_template(TemplateType
::PlaintextBody
, template_name
, data
)?
;
226 email_builder
= email_builder
.subject(subject
);
230 MultiPart
::alternative()
232 SinglePart
::builder()
233 .header(ContentType
::TEXT_PLAIN
)
237 SinglePart
::builder()
238 .header(ContentType
::TEXT_HTML
)
242 .map_err(|err
| Error
::NotifyFailed(self.name().into(), Box
::new(err
)))?
244 #[cfg(feature = "mail-forwarder")]
245 Content
::ForwardedMail { ref raw, title, .. }
=> {
246 use lettre
::message
::header
::ContentTransferEncoding
;
247 use lettre
::message
::Body
;
249 let parsed_message
= mail_parser
::Message
::parse(raw
)
250 .ok_or_else(|| Error
::Generic("could not parse forwarded email".to_string()))?
;
252 let root_part
= parsed_message
254 .ok_or_else(|| Error
::Generic("root message part not present".to_string()))?
;
256 let raw_body
= parsed_message
258 .get(root_part
.offset_body
..root_part
.offset_end
)
259 .ok_or_else(|| Error
::Generic("could not get raw body content".to_string()))?
;
261 // We assume that the original message content is already properly
262 // encoded, thus we add the original message body in 'Binary' encoding.
263 // This prohibits lettre from trying to re-encode our raw body data.
264 // lettre will automatically set the `Content-Transfer-Encoding: binary` header,
265 // which we need to remove. The actual transfer encoding is later
266 // copied from the original message headers.
268 Body
::new_with_encoding(raw_body
.to_vec(), ContentTransferEncoding
::Binary
)
269 .map_err(|_
| Error
::Generic("could not create body".into()))?
;
270 let mut message
= email_builder
273 .map_err(|err
| Error
::NotifyFailed(self.name().into(), Box
::new(err
)))?
;
276 .remove_raw("Content-Transfer-Encoding");
278 // Copy over all headers that are relevant to display the original body correctly.
279 // Unfortunately this is a bit cumbersome, as we use separate crates for mail parsing (mail-parser)
280 // and creating/sending mails (lettre).
281 // Note: Other MIME-Headers, such as Content-{ID,Description,Disposition} are only used
282 // for body-parts in multipart messages, so we can ignore them for the messages headers.
283 // Since we send the original raw body, the part-headers will be included any way.
284 for header
in parsed_message
.headers() {
285 let header_name
= header
.name
.as_str();
286 // Email headers are case-insensitive, so convert to lowercase...
287 let value
= match header_name
.to_lowercase().as_str() {
289 if let mail_parser
::HeaderValue
::ContentType(ct
) = header
.value() {
290 // mail_parser does not give us access to the full decoded and unfolded
291 // header value, so we unfortunately need to reassemble it ourselves.
293 let mut value
= ct
.ctype().to_string();
294 if let Some(subtype
) = ct
.subtype() {
296 value
.push_str(subtype
);
298 if let Some(attributes
) = ct
.attributes() {
301 for attribute
in attributes
{
305 attribute
.0, attribute
.1
314 "content-transfer-encoding" | "mime-version" => {
315 if let mail_parser
::HeaderValue
::Text(text
) = header
.value() {
316 Some(text
.to_string())
324 if let Some(value
) = value
{
325 match HeaderName
::new_from_ascii(header_name
.into()) {
327 let header
= HeaderValue
::new(name
, value
);
328 message
.headers_mut().insert_raw(header
);
330 Err(e
) => log
::error
!("could not set header: {e}"),
339 // `Auto-Submitted` is defined in RFC 5436 and describes how
340 // an automatic response (f.e. ooo replies, etc.) should behave on the
341 // emails. When using `Auto-Submitted: auto-generated` (or any value
342 // other than `none`) automatic replies won't be triggered.
343 email
.headers_mut().insert_raw(HeaderValue
::new(
344 HeaderName
::new_from_ascii_str("Auto-Submitted"),
345 "auto-generated;".into(),
350 .map_err(|err
| Error
::NotifyFailed(self.name().into(), err
.into()))?
;
355 fn name(&self) -> &str {
359 /// Check if the endpoint is disabled
360 fn disabled(&self) -> bool
{
361 self.config
.disable
.unwrap_or_default()