2 use std
::process
::Command
;
4 use anyhow
::{bail, format_err, Error}
;
5 use serde
::Deserialize
;
7 use proxmox_schema
::{ObjectSchema, Schema, StringSchema}
;
8 use proxmox_section_config
::{SectionConfig, SectionConfigPlugin}
;
11 const PBS_USER_CFG_FILENAME
: &str = "/etc/proxmox-backup/user.cfg";
12 const PBS_ROOT_USER
: &str = "root@pam";
14 // FIXME: Switch to the actual schema when possible in terms of dependency.
15 // It's safe to assume that the config was written with the actual schema restrictions, so parsing
16 // it with the less restrictive schema should be enough for the purpose of getting the mail address.
17 const DUMMY_ID_SCHEMA
: Schema
= StringSchema
::new("dummy ID").min_length(3).schema();
18 const DUMMY_EMAIL_SCHEMA
: Schema
= StringSchema
::new("dummy email").schema();
19 const DUMMY_USER_SCHEMA
: ObjectSchema
= ObjectSchema
{
20 description
: "minimal PBS user",
22 ("userid", false, &DUMMY_ID_SCHEMA
),
23 ("email", true, &DUMMY_EMAIL_SCHEMA
),
25 additional_properties
: true,
29 #[derive(Deserialize)]
31 pub email
: Option
<String
>,
34 const PVE_USER_CFG_FILENAME
: &str = "/etc/pve/user.cfg";
35 const PVE_DATACENTER_CFG_FILENAME
: &str = "/etc/pve/datacenter.cfg";
36 const PVE_ROOT_USER
: &str = "root@pam";
38 /// Convenience helper to get the trimmed contents of an optional &str, mapping blank ones to `None`
39 /// and creating a String from it for returning.
40 fn normalize_for_return(s
: Option
<&str>) -> Option
<String
> {
43 s
=> Some(s
.to_string()),
47 /// Extract the root user's email address from the PBS user config.
48 fn get_pbs_mail_to(content
: &str) -> Option
<String
> {
49 let mut config
= SectionConfig
::new(&DUMMY_ID_SCHEMA
).allow_unknown_sections(true);
50 let user_plugin
= SectionConfigPlugin
::new(
52 Some("userid".to_string()),
55 config
.register_plugin(user_plugin
);
57 match config
.parse(PBS_USER_CFG_FILENAME
, content
) {
59 parsed
.sections
.get(PBS_ROOT_USER
)?
;
60 match parsed
.lookup
::<DummyPbsUser
>("user", PBS_ROOT_USER
) {
61 Ok(user
) => normalize_for_return(user
.email
.as_deref()),
63 log
::error
!("unable to parse {} - {}", PBS_USER_CFG_FILENAME
, err
);
69 log
::error
!("unable to parse {} - {}", PBS_USER_CFG_FILENAME
, err
);
75 /// Extract the root user's email address from the PVE user config.
76 fn get_pve_mail_to(content
: &str) -> Option
<String
> {
77 normalize_for_return(content
.lines().find_map(|line
| {
78 let fields
: Vec
<&str> = line
.split('
:'
).collect();
79 #[allow(clippy::get_first)] // to keep expression style consistent
80 match fields
.get(0)?
.trim() == "user" && fields
.get(1)?
.trim() == PVE_ROOT_USER
{
81 true => fields
.get(6).copied(),
87 /// Extract the From-address configured in the PVE datacenter config.
88 fn get_pve_mail_from(content
: &str) -> Option
<String
> {
92 .find_map(|line
| line
.strip_prefix("email_from:")),
96 /// Executes sendmail as a child process with the specified From/To-addresses, expecting the mail
97 /// contents to be passed via stdin inherited from this program.
98 fn forward_mail(mail_from
: String
, mail_to
: Vec
<String
>) -> Result
<(), Error
> {
99 if mail_to
.is_empty() {
100 bail
!("user 'root@pam' does not have an email address");
103 log
::info
!("forward mail to <{}>", mail_to
.join(","));
105 let mut cmd
= Command
::new("sendmail");
107 "-bm", "-N", "never", // never send DSN (avoid mail loops)
108 "-f", &mail_from
, "--",
111 cmd
.env("PATH", "/sbin:/bin:/usr/sbin:/usr/bin");
113 // with status(), child inherits stdin
115 .map_err(|err
| format_err
!("command {:?} failed - {}", cmd
, err
))?
;
120 /// Wrapper around `proxmox_sys::fs::file_read_optional_string` which also returns `None` upon error
121 /// after logging it.
122 fn attempt_file_read
<P
: AsRef
<Path
>>(path
: P
) -> Option
<String
> {
123 match fs
::file_read_optional_string(path
) {
124 Ok(contents
) => contents
,
126 log
::error
!("{}", err
);
133 if let Err(err
) = syslog
::init(
134 syslog
::Facility
::LOG_DAEMON
,
135 log
::LevelFilter
::Info
,
136 Some("proxmox-mail-forward"),
138 eprintln
!("unable to inititialize syslog - {}", err
);
141 let pbs_user_cfg_content
= attempt_file_read(PBS_USER_CFG_FILENAME
);
142 let pve_user_cfg_content
= attempt_file_read(PVE_USER_CFG_FILENAME
);
143 let pve_datacenter_cfg_content
= attempt_file_read(PVE_DATACENTER_CFG_FILENAME
);
145 let real_uid
= nix
::unistd
::getuid();
146 if let Err(err
) = nix
::unistd
::setresuid(real_uid
, real_uid
, real_uid
) {
148 "mail forward failed: unable to set effective uid to {}: {}",
155 let pbs_mail_to
= pbs_user_cfg_content
.and_then(|content
| get_pbs_mail_to(&content
));
156 let pve_mail_to
= pve_user_cfg_content
.and_then(|content
| get_pve_mail_to(&content
));
157 let pve_mail_from
= pve_datacenter_cfg_content
.and_then(|content
| get_pve_mail_from(&content
));
159 let mail_from
= pve_mail_from
.unwrap_or_else(|| "root".to_string());
161 let mut mail_to
= vec
![];
162 if let Some(pve_mail_to
) = pve_mail_to
{
163 mail_to
.push(pve_mail_to
);
165 if let Some(pbs_mail_to
) = pbs_mail_to
{
166 if !mail_to
.contains(&pbs_mail_to
) {
167 mail_to
.push(pbs_mail_to
);
171 if let Err(err
) = forward_mail(mail_from
, mail_to
) {
172 log
::error
!("mail forward failed: {}", err
);