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
::TemplateRenderer
;
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(skip_serializing_if = "Option::is_none")]
84 pub mailto
: Option
<Vec
<String
>>,
86 #[serde(skip_serializing_if = "Option::is_none")]
87 pub mailto_user
: Option
<Vec
<String
>>,
88 /// `From` address for the mail
89 pub from_address
: String
,
90 /// Author of the mail
91 #[serde(skip_serializing_if = "Option::is_none")]
92 pub author
: Option
<String
>,
94 #[serde(skip_serializing_if = "Option::is_none")]
95 pub comment
: Option
<String
>,
96 /// Disable this target.
97 #[serde(skip_serializing_if = "Option::is_none")]
98 pub disable
: Option
<bool
>,
99 /// Origin of this config entry.
100 #[serde(skip_serializing_if = "Option::is_none")]
102 pub origin
: Option
<Origin
>,
105 #[derive(Serialize, Deserialize)]
106 #[serde(rename_all = "kebab-case")]
107 pub enum DeleteableSmtpProperty
{
119 #[derive(Serialize, Deserialize, Clone, Updater, Debug)]
120 #[serde(rename_all = "kebab-case")]
121 /// Private configuration for SMTP notification endpoints.
122 /// This config will be saved to a separate configuration file with stricter
123 /// permissions (root:root 0600)
124 pub struct SmtpPrivateConfig
{
125 /// Name of the endpoint
128 /// Authentication token
129 #[serde(skip_serializing_if = "Option::is_none")]
130 pub password
: Option
<String
>,
133 /// A sendmail notification endpoint.
134 pub struct SmtpEndpoint
{
135 pub config
: SmtpConfig
,
136 pub private_config
: SmtpPrivateConfig
,
139 impl Endpoint
for SmtpEndpoint
{
140 fn send(&self, notification
: &Notification
) -> Result
<(), Error
> {
141 let tls_parameters
= TlsParameters
::new(self.config
.server
.clone())
142 .map_err(|err
| Error
::NotifyFailed(self.name().into(), Box
::new(err
)))?
;
144 let (port
, tls
) = match self.config
.mode
.unwrap_or_default() {
145 SmtpMode
::Insecure
=> {
146 let port
= self.config
.port
.unwrap_or(SMTP_PORT
);
149 SmtpMode
::StartTls
=> {
150 let port
= self.config
.port
.unwrap_or(SMTP_SUBMISSION_STARTTLS_PORT
);
151 (port
, Tls
::Required(tls_parameters
))
154 let port
= self.config
.port
.unwrap_or(SMTP_SUBMISSION_TLS_PORT
);
155 (port
, Tls
::Wrapper(tls_parameters
))
159 let mut transport_builder
= SmtpTransport
::builder_dangerous(&self.config
.server
)
162 .timeout(Some(Duration
::from_secs(SMTP_TIMEOUT
.into())));
164 if let Some(username
) = self.config
.username
.as_deref() {
165 if let Some(password
) = self.private_config
.password
.as_deref() {
166 transport_builder
= transport_builder
.credentials((username
, password
).into());
168 return Err(Error
::NotifyFailed(
170 Box
::new(Error
::Generic(
171 "username is set but no password was provided".to_owned(),
177 let transport
= transport_builder
.build();
179 let recipients
= mail
::get_recipients(
180 self.config
.mailto
.as_deref(),
181 self.config
.mailto_user
.as_deref(),
183 let mail_from
= self.config
.from_address
.clone();
185 let parse_address
= |addr
: &str| -> Result
<Mailbox
, Error
> {
187 .map_err(|err
| Error
::NotifyFailed(self.name().into(), Box
::new(err
)))
194 .unwrap_or_else(|| context().default_sendmail_author());
196 let mut email_builder
=
197 Message
::builder().from(parse_address(&format
!("{author} <{mail_from}>"))?
);
199 for recipient
in recipients
{
200 email_builder
= email_builder
.to(parse_address(&recipient
)?
);
203 let mut email
= match ¬ification
.content
{
210 renderer
::render_template(TemplateRenderer
::Plaintext
, title_template
, data
)?
;
212 renderer
::render_template(TemplateRenderer
::Html
, body_template
, data
)?
;
214 renderer
::render_template(TemplateRenderer
::Plaintext
, body_template
, data
)?
;
216 email_builder
= email_builder
.subject(subject
);
220 MultiPart
::alternative()
222 SinglePart
::builder()
223 .header(ContentType
::TEXT_PLAIN
)
227 SinglePart
::builder()
228 .header(ContentType
::TEXT_HTML
)
232 .map_err(|err
| Error
::NotifyFailed(self.name().into(), Box
::new(err
)))?
234 #[cfg(feature = "mail-forwarder")]
235 Content
::ForwardedMail { ref raw, title, .. }
=> {
236 use lettre
::message
::header
::ContentTransferEncoding
;
237 use lettre
::message
::Body
;
239 let parsed_message
= mail_parser
::Message
::parse(raw
)
240 .ok_or_else(|| Error
::Generic("could not parse forwarded email".to_string()))?
;
242 let root_part
= parsed_message
244 .ok_or_else(|| Error
::Generic("root message part not present".to_string()))?
;
246 let raw_body
= parsed_message
248 .get(root_part
.offset_body
..root_part
.offset_end
)
249 .ok_or_else(|| Error
::Generic("could not get raw body content".to_string()))?
;
251 // We assume that the original message content is already properly
252 // encoded, thus we add the original message body in 'Binary' encoding.
253 // This prohibits lettre from trying to re-encode our raw body data.
254 // lettre will automatically set the `Content-Transfer-Encoding: binary` header,
255 // which we need to remove. The actual transfer encoding is later
256 // copied from the original message headers.
258 Body
::new_with_encoding(raw_body
.to_vec(), ContentTransferEncoding
::Binary
)
259 .map_err(|_
| Error
::Generic("could not create body".into()))?
;
260 let mut message
= email_builder
263 .map_err(|err
| Error
::NotifyFailed(self.name().into(), Box
::new(err
)))?
;
266 .remove_raw("Content-Transfer-Encoding");
268 // Copy over all headers that are relevant to display the original body correctly.
269 // Unfortunately this is a bit cumbersome, as we use separate crates for mail parsing (mail-parser)
270 // and creating/sending mails (lettre).
271 // Note: Other MIME-Headers, such as Content-{ID,Description,Disposition} are only used
272 // for body-parts in multipart messages, so we can ignore them for the messages headers.
273 // Since we send the original raw body, the part-headers will be included any way.
274 for header
in parsed_message
.headers() {
275 let header_name
= header
.name
.as_str();
276 // Email headers are case-insensitive, so convert to lowercase...
277 let value
= match header_name
.to_lowercase().as_str() {
279 if let mail_parser
::HeaderValue
::ContentType(ct
) = header
.value() {
280 // mail_parser does not give us access to the full decoded and unfolded
281 // header value, so we unfortunately need to reassemble it ourselves.
283 let mut value
= ct
.ctype().to_string();
284 if let Some(subtype
) = ct
.subtype() {
286 value
.push_str(subtype
);
288 if let Some(attributes
) = ct
.attributes() {
291 for attribute
in attributes
{
295 attribute
.0, attribute
.1
304 "content-transfer-encoding" | "mime-version" => {
305 if let mail_parser
::HeaderValue
::Text(text
) = header
.value() {
306 Some(text
.to_string())
314 if let Some(value
) = value
{
315 match HeaderName
::new_from_ascii(header_name
.into()) {
317 let header
= HeaderValue
::new(name
, value
);
318 message
.headers_mut().insert_raw(header
);
320 Err(e
) => log
::error
!("could not set header: {e}"),
329 // `Auto-Submitted` is defined in RFC 5436 and describes how
330 // an automatic response (f.e. ooo replies, etc.) should behave on the
331 // emails. When using `Auto-Submitted: auto-generated` (or any value
332 // other than `none`) automatic replies won't be triggered.
333 email
.headers_mut().insert_raw(HeaderValue
::new(
334 HeaderName
::new_from_ascii_str("Auto-Submitted"),
335 "auto-generated;".into(),
340 .map_err(|err
| Error
::NotifyFailed(self.name().into(), err
.into()))?
;
345 fn name(&self) -> &str {
349 /// Check if the endpoint is disabled
350 fn disabled(&self) -> bool
{
351 self.config
.disable
.unwrap_or_default()