]>
Commit | Line | Data |
---|---|---|
28f222cd FE |
1 | use std::path::Path; |
2 | use std::process::Command; | |
3 | ||
4 | use anyhow::{bail, format_err, Error}; | |
5 | use serde::Deserialize; | |
6 | ||
7 | use proxmox_schema::{ObjectSchema, Schema, StringSchema}; | |
8 | use proxmox_section_config::{SectionConfig, SectionConfigPlugin}; | |
9 | use proxmox_sys::fs; | |
10 | ||
11 | const PBS_USER_CFG_FILENAME: &str = "/etc/proxmox-backup/user.cfg"; | |
12 | const PBS_ROOT_USER: &str = "root@pam"; | |
13 | ||
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", | |
21 | properties: &[ | |
22 | ("userid", false, &DUMMY_ID_SCHEMA), | |
23 | ("email", true, &DUMMY_EMAIL_SCHEMA), | |
24 | ], | |
25 | additional_properties: true, | |
26 | default_key: None, | |
27 | }; | |
28 | ||
29 | #[derive(Deserialize)] | |
30 | struct DummyPbsUser { | |
31 | pub email: Option<String>, | |
32 | } | |
33 | ||
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"; | |
37 | ||
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> { | |
41 | match s?.trim() { | |
42 | "" => None, | |
43 | s => Some(s.to_string()), | |
44 | } | |
45 | } | |
46 | ||
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); | |
50 | let user_plugin = SectionConfigPlugin::new( | |
51 | "user".to_string(), | |
52 | Some("userid".to_string()), | |
53 | &DUMMY_USER_SCHEMA, | |
54 | ); | |
55 | config.register_plugin(user_plugin); | |
56 | ||
57 | match config.parse(PBS_USER_CFG_FILENAME, content) { | |
58 | Ok(parsed) => { | |
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()), | |
62 | Err(err) => { | |
63 | log::error!("unable to parse {} - {}", PBS_USER_CFG_FILENAME, err); | |
64 | None | |
65 | } | |
66 | } | |
67 | } | |
68 | Err(err) => { | |
69 | log::error!("unable to parse {} - {}", PBS_USER_CFG_FILENAME, err); | |
70 | None | |
71 | } | |
72 | } | |
73 | } | |
74 | ||
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(), | |
82 | false => None, | |
83 | } | |
84 | })) | |
85 | } | |
86 | ||
87 | /// Extract the From-address configured in the PVE datacenter config. | |
88 | fn get_pve_mail_from(content: &str) -> Option<String> { | |
89 | normalize_for_return( | |
90 | content | |
91 | .lines() | |
92 | .find_map(|line| line.strip_prefix("email_from:")), | |
93 | ) | |
94 | } | |
95 | ||
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"); | |
101 | } | |
102 | ||
103 | log::info!("forward mail to <{}>", mail_to.join(",")); | |
104 | ||
105 | let mut cmd = Command::new("sendmail"); | |
106 | cmd.args([ | |
107 | "-bm", "-N", "never", // never send DSN (avoid mail loops) | |
108 | "-f", &mail_from, "--", | |
109 | ]); | |
110 | cmd.args(mail_to); | |
111 | cmd.env("PATH", "/sbin:/bin:/usr/sbin:/usr/bin"); | |
112 | ||
113 | // with status(), child inherits stdin | |
114 | cmd.status() | |
115 | .map_err(|err| format_err!("command {:?} failed - {}", cmd, err))?; | |
116 | ||
117 | Ok(()) | |
118 | } | |
119 | ||
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, | |
125 | Err(err) => { | |
126 | log::error!("{}", err); | |
127 | None | |
128 | } | |
129 | } | |
130 | } | |
131 | ||
132 | fn main() { | |
133 | if let Err(err) = syslog::init( | |
134 | syslog::Facility::LOG_DAEMON, | |
135 | log::LevelFilter::Info, | |
136 | Some("proxmox-mail-forward"), | |
137 | ) { | |
138 | eprintln!("unable to inititialize syslog - {}", err); | |
139 | } | |
140 | ||
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); | |
144 | ||
145 | let real_uid = nix::unistd::getuid(); | |
33617e7e | 146 | if let Err(err) = nix::unistd::setresuid(real_uid, real_uid, real_uid) { |
28f222cd FE |
147 | log::error!( |
148 | "mail forward failed: unable to set effective uid to {}: {}", | |
149 | real_uid, | |
150 | err | |
151 | ); | |
152 | return; | |
153 | } | |
154 | ||
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)); | |
158 | ||
159 | let mail_from = pve_mail_from.unwrap_or_else(|| "root".to_string()); | |
160 | ||
161 | let mut mail_to = vec![]; | |
162 | if let Some(pve_mail_to) = pve_mail_to { | |
163 | mail_to.push(pve_mail_to); | |
164 | } | |
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); | |
168 | } | |
169 | } | |
170 | ||
171 | if let Err(err) = forward_mail(mail_from, mail_to) { | |
172 | log::error!("mail forward failed: {}", err); | |
173 | } | |
174 | } |