]> git.proxmox.com Git - proxmox-backup.git/blame - src/server/email_notifications.rs
get rid of backup@pam
[proxmox-backup.git] / src / server / email_notifications.rs
CommitLineData
b9e7bcc2
DM
1use anyhow::Error;
2use serde_json::json;
3
4use handlebars::{Handlebars, Helper, Context, RenderError, RenderContext, Output, HelperResult};
5
6use proxmox::tools::email::sendmail;
c26c9390 7use proxmox::api::schema::parse_property_string;
b9e7bcc2
DM
8
9use crate::{
f47c1d3a 10 config::datastore::DataStoreConfig,
b9e7bcc2 11 config::verify::VerificationJobConfig,
9e733dae 12 config::sync::SyncJobConfig,
b9e7bcc2 13 api2::types::{
86d60245 14 APTUpdateInfo,
b9e7bcc2 15 GarbageCollectionStatus,
86d60245 16 Userid,
f47c1d3a 17 Notify,
c26c9390 18 DatastoreNotify,
b9e7bcc2
DM
19 },
20 tools::format::HumanByte,
21};
22
23const GC_OK_TEMPLATE: &str = r###"
24
d6373f35
DM
25Datastore: {{datastore}}
26Task ID: {{status.upid}}
27Index file count: {{status.index-file-count}}
b9e7bcc2 28
d6373f35
DM
29Removed garbage: {{human-bytes status.removed-bytes}}
30Removed chunks: {{status.removed-chunks}}
1143f6ca 31Removed bad chunks: {{status.removed-bad}}
b9e7bcc2 32
1143f6ca 33Leftover bad chunks: {{status.still-bad}}
d6373f35 34Pending removals: {{human-bytes status.pending-bytes}} (in {{status.pending-chunks}} chunks)
b9e7bcc2 35
d6373f35 36Original Data usage: {{human-bytes status.index-data-bytes}}
1143f6ca
DM
37On-Disk usage: {{human-bytes status.disk-bytes}} ({{relative-percentage status.disk-bytes status.index-data-bytes}})
38On-Disk chunks: {{status.disk-chunks}}
d6373f35
DM
39
40Deduplication Factor: {{deduplication-factor}}
b9e7bcc2
DM
41
42Garbage collection successful.
43
3066f564
DM
44
45Please visit the web interface for futher details:
46
47<https://{{fqdn}}:{{port}}/#DataStore-{{datastore}}>
48
b9e7bcc2
DM
49"###;
50
51
52const GC_ERR_TEMPLATE: &str = r###"
53
54Datastore: {{datastore}}
55
56Garbage collection failed: {{error}}
57
3066f564
DM
58
59Please visit the web interface for futher details:
60
61<https://{{fqdn}}:{{port}}/#pbsServerAdministration:tasks>
62
b9e7bcc2
DM
63"###;
64
65const VERIFY_OK_TEMPLATE: &str = r###"
66
67Job ID: {{job.id}}
68Datastore: {{job.store}}
69
70Verification successful.
71
3066f564
DM
72
73Please visit the web interface for futher details:
74
75<https://{{fqdn}}:{{port}}/#DataStore-{{job.store}}>
76
b9e7bcc2
DM
77"###;
78
79const VERIFY_ERR_TEMPLATE: &str = r###"
80
81Job ID: {{job.id}}
82Datastore: {{job.store}}
83
a4915dfc
DM
84Verification failed on these snapshots:
85
86{{#each errors}}
07ca4e36 87 {{this~}}
a4915dfc 88{{/each}}
b9e7bcc2 89
3066f564
DM
90
91Please visit the web interface for futher details:
92
93<https://{{fqdn}}:{{port}}/#pbsServerAdministration:tasks>
94
b9e7bcc2
DM
95"###;
96
9e733dae
DM
97const SYNC_OK_TEMPLATE: &str = r###"
98
99Job ID: {{job.id}}
100Datastore: {{job.store}}
101Remote: {{job.remote}}
102Remote Store: {{job.remote-store}}
103
104Synchronization successful.
105
3066f564
DM
106
107Please visit the web interface for futher details:
108
109<https://{{fqdn}}:{{port}}/#DataStore-{{job.store}}>
110
9e733dae
DM
111"###;
112
113const SYNC_ERR_TEMPLATE: &str = r###"
114
115Job ID: {{job.id}}
116Datastore: {{job.store}}
117Remote: {{job.remote}}
118Remote Store: {{job.remote-store}}
119
120Synchronization failed: {{error}}
121
3066f564
DM
122
123Please visit the web interface for futher details:
124
125<https://{{fqdn}}:{{port}}/#pbsServerAdministration:tasks>
126
9e733dae
DM
127"###;
128
86d60245
TL
129const PACKAGE_UPDATES_TEMPLATE: &str = r###"
130Proxmox Backup Server has the following updates available:
131{{#each updates }}
132 {{Package}}: {{OldVersion}} -> {{Version~}}
133{{/each }}
134
3066f564
DM
135To upgrade visit the web interface:
136
137<https://{{fqdn}}:{{port}}/#pbsServerAdministration:updates>
138
86d60245
TL
139"###;
140
141
b9e7bcc2
DM
142lazy_static::lazy_static!{
143
144 static ref HANDLEBARS: Handlebars<'static> = {
145 let mut hb = Handlebars::new();
146
147 hb.set_strict_mode(true);
148
149 hb.register_helper("human-bytes", Box::new(handlebars_humam_bytes_helper));
150 hb.register_helper("relative-percentage", Box::new(handlebars_relative_percentage_helper));
151
152 hb.register_template_string("gc_ok_template", GC_OK_TEMPLATE).unwrap();
153 hb.register_template_string("gc_err_template", GC_ERR_TEMPLATE).unwrap();
154
155 hb.register_template_string("verify_ok_template", VERIFY_OK_TEMPLATE).unwrap();
156 hb.register_template_string("verify_err_template", VERIFY_ERR_TEMPLATE).unwrap();
157
9e733dae
DM
158 hb.register_template_string("sync_ok_template", SYNC_OK_TEMPLATE).unwrap();
159 hb.register_template_string("sync_err_template", SYNC_ERR_TEMPLATE).unwrap();
160
86d60245
TL
161 hb.register_template_string("package_update_template", PACKAGE_UPDATES_TEMPLATE).unwrap();
162
b9e7bcc2
DM
163 hb
164 };
165}
166
167fn send_job_status_mail(
168 email: &str,
169 subject: &str,
170 text: &str,
171) -> Result<(), Error> {
172
173 // Note: OX has serious problems displaying text mails,
174 // so we include html as well
385cf2bd 175 let html = format!("<html><body><pre>\n{}\n<pre>", handlebars::html_escape(text));
b9e7bcc2
DM
176
177 let nodename = proxmox::tools::nodename();
178
179 let author = format!("Proxmox Backup Server - {}", nodename);
180
181 sendmail(
182 &[email],
183 &subject,
184 Some(&text),
185 Some(&html),
186 None,
187 Some(&author),
188 )?;
189
190 Ok(())
191}
192
193pub fn send_gc_status(
194 email: &str,
c26c9390 195 notify: DatastoreNotify,
b9e7bcc2
DM
196 datastore: &str,
197 status: &GarbageCollectionStatus,
198 result: &Result<(), Error>,
199) -> Result<(), Error> {
200
c26c9390
DM
201 match notify.gc {
202 None => { /* send notifications by default */ },
203 Some(notify) => {
204 if notify == Notify::Never || (result.is_ok() && notify == Notify::Error) {
205 return Ok(());
206 }
207 }
f47c1d3a
DM
208 }
209
3066f564
DM
210 let (fqdn, port) = get_server_url();
211 let mut data = json!({
212 "datastore": datastore,
213 "fqdn": fqdn,
214 "port": port,
215 });
216
b9e7bcc2
DM
217 let text = match result {
218 Ok(()) => {
d6373f35
DM
219 let deduplication_factor = if status.disk_bytes > 0 {
220 (status.index_data_bytes as f64)/(status.disk_bytes as f64)
221 } else {
222 1.0
223 };
224
3066f564
DM
225 data["status"] = json!(status);
226 data["deduplication-factor"] = format!("{:.2}", deduplication_factor).into();
d6373f35 227
b9e7bcc2
DM
228 HANDLEBARS.render("gc_ok_template", &data)?
229 }
230 Err(err) => {
3066f564 231 data["error"] = err.to_string().into();
b9e7bcc2
DM
232 HANDLEBARS.render("gc_err_template", &data)?
233 }
234 };
235
236 let subject = match result {
237 Ok(()) => format!(
238 "Garbage Collect Datastore '{}' successful",
239 datastore,
240 ),
241 Err(_) => format!(
242 "Garbage Collect Datastore '{}' failed",
243 datastore,
244 ),
245 };
246
247 send_job_status_mail(email, &subject, &text)?;
248
249 Ok(())
250}
251
252pub fn send_verify_status(
253 email: &str,
c26c9390 254 notify: DatastoreNotify,
b9e7bcc2 255 job: VerificationJobConfig,
a4915dfc 256 result: &Result<Vec<String>, Error>,
b9e7bcc2
DM
257) -> Result<(), Error> {
258
3066f564
DM
259 let (fqdn, port) = get_server_url();
260 let mut data = json!({
261 "job": job,
262 "fqdn": fqdn,
263 "port": port,
264 });
b9e7bcc2 265
c26c9390
DM
266 let mut result_is_ok = false;
267
b9e7bcc2 268 let text = match result {
a4915dfc 269 Ok(errors) if errors.is_empty() => {
c26c9390 270 result_is_ok = true;
b9e7bcc2
DM
271 HANDLEBARS.render("verify_ok_template", &data)?
272 }
a4915dfc 273 Ok(errors) => {
3066f564 274 data["errors"] = json!(errors);
b9e7bcc2
DM
275 HANDLEBARS.render("verify_err_template", &data)?
276 }
a4915dfc 277 Err(_) => {
d0abba33 278 // aborted job - do not send any email
a4915dfc
DM
279 return Ok(());
280 }
b9e7bcc2
DM
281 };
282
c26c9390
DM
283 match notify.verify {
284 None => { /* send notifications by default */ },
285 Some(notify) => {
286 if notify == Notify::Never || (result_is_ok && notify == Notify::Error) {
287 return Ok(());
288 }
289 }
290 }
291
b9e7bcc2 292 let subject = match result {
a4915dfc 293 Ok(errors) if errors.is_empty() => format!(
b9e7bcc2
DM
294 "Verify Datastore '{}' successful",
295 job.store,
296 ),
a4915dfc 297 _ => format!(
b9e7bcc2
DM
298 "Verify Datastore '{}' failed",
299 job.store,
300 ),
301 };
302
303 send_job_status_mail(email, &subject, &text)?;
304
305 Ok(())
306}
307
9e733dae
DM
308pub fn send_sync_status(
309 email: &str,
c26c9390 310 notify: DatastoreNotify,
9e733dae
DM
311 job: &SyncJobConfig,
312 result: &Result<(), Error>,
313) -> Result<(), Error> {
314
c26c9390
DM
315 match notify.sync {
316 None => { /* send notifications by default */ },
317 Some(notify) => {
318 if notify == Notify::Never || (result.is_ok() && notify == Notify::Error) {
319 return Ok(());
320 }
321 }
f47c1d3a
DM
322 }
323
3066f564
DM
324 let (fqdn, port) = get_server_url();
325 let mut data = json!({
326 "job": job,
327 "fqdn": fqdn,
328 "port": port,
329 });
330
9e733dae
DM
331 let text = match result {
332 Ok(()) => {
9e733dae
DM
333 HANDLEBARS.render("sync_ok_template", &data)?
334 }
335 Err(err) => {
3066f564 336 data["error"] = err.to_string().into();
9e733dae
DM
337 HANDLEBARS.render("sync_err_template", &data)?
338 }
339 };
340
341 let subject = match result {
342 Ok(()) => format!(
343 "Sync remote '{}' datastore '{}' successful",
344 job.remote,
345 job.remote_store,
346 ),
347 Err(_) => format!(
348 "Sync remote '{}' datastore '{}' failed",
349 job.remote,
350 job.remote_store,
351 ),
352 };
353
354 send_job_status_mail(email, &subject, &text)?;
355
356 Ok(())
357}
358
3066f564
DM
359fn get_server_url() -> (String, usize) {
360
361 // user will surely request that they can change this
362
363 let nodename = proxmox::tools::nodename();
364 let mut fqdn = nodename.to_owned();
365
366 if let Ok(resolv_conf) = crate::api2::node::dns::read_etc_resolv_conf() {
367 if let Some(search) = resolv_conf["search"].as_str() {
368 fqdn.push('.');
369 fqdn.push_str(search);
370 }
371 }
372
373 let port = 8007;
374
375 (fqdn, port)
376}
377
86d60245
TL
378pub fn send_updates_available(
379 updates: &Vec<&APTUpdateInfo>,
380) -> Result<(), Error> {
381 // update mails always go to the root@pam configured email..
382 if let Some(email) = lookup_user_email(Userid::root_userid()) {
383 let nodename = proxmox::tools::nodename();
384 let subject = format!("New software packages available ({})", nodename);
385
3066f564
DM
386 let (fqdn, port) = get_server_url();
387
86d60245 388 let text = HANDLEBARS.render("package_update_template", &json!({
3066f564
DM
389 "fqdn": fqdn,
390 "port": port,
86d60245
TL
391 "updates": updates,
392 }))?;
393
394 send_job_status_mail(&email, &subject, &text)?;
395 }
396 Ok(())
397}
398
b9e7bcc2 399/// Lookup users email address
f47c1d3a 400fn lookup_user_email(userid: &Userid) -> Option<String> {
b9e7bcc2
DM
401
402 use crate::config::user::{self, User};
403
b9e7bcc2
DM
404 if let Ok(user_config) = user::cached_config() {
405 if let Ok(user) = user_config.lookup::<User>("user", userid.as_str()) {
406 return user.email.clone();
407 }
408 }
409
410 None
411}
412
f47c1d3a
DM
413/// Lookup Datastore notify settings
414pub fn lookup_datastore_notify_settings(
415 store: &str,
c26c9390 416) -> (Option<String>, DatastoreNotify) {
f47c1d3a 417
f47c1d3a
DM
418 let mut email = None;
419
c26c9390
DM
420 let notify = DatastoreNotify { gc: None, verify: None, sync: None };
421
f47c1d3a
DM
422 let (config, _digest) = match crate::config::datastore::config() {
423 Ok(result) => result,
424 Err(_) => return (email, notify),
425 };
426
427 let config: DataStoreConfig = match config.lookup("datastore", store) {
428 Ok(result) => result,
429 Err(_) => return (email, notify),
430 };
431
432 email = match config.notify_user {
433 Some(ref userid) => lookup_user_email(userid),
ad54df31 434 None => lookup_user_email(Userid::root_userid()),
f47c1d3a
DM
435 };
436
c26c9390
DM
437 let notify_str = config.notify.unwrap_or(String::new());
438
439 if let Ok(value) = parse_property_string(&notify_str, &DatastoreNotify::API_SCHEMA) {
440 if let Ok(notify) = serde_json::from_value(value) {
441 return (email, notify);
442 }
f47c1d3a
DM
443 }
444
445 (email, notify)
446}
447
b9e7bcc2
DM
448// Handlerbar helper functions
449
450fn handlebars_humam_bytes_helper(
451 h: &Helper,
452 _: &Handlebars,
453 _: &Context,
454 _rc: &mut RenderContext,
455 out: &mut dyn Output
456) -> HelperResult {
457 let param = h.param(0).map(|v| v.value().as_u64())
458 .flatten()
459 .ok_or(RenderError::new("human-bytes: param not found"))?;
460
461 out.write(&HumanByte::from(param).to_string())?;
462
463 Ok(())
464}
465
466fn handlebars_relative_percentage_helper(
467 h: &Helper,
468 _: &Handlebars,
469 _: &Context,
470 _rc: &mut RenderContext,
471 out: &mut dyn Output
472) -> HelperResult {
473 let param0 = h.param(0).map(|v| v.value().as_f64())
474 .flatten()
475 .ok_or(RenderError::new("relative-percentage: param0 not found"))?;
476 let param1 = h.param(1).map(|v| v.value().as_f64())
477 .flatten()
478 .ok_or(RenderError::new("relative-percentage: param1 not found"))?;
479
480 if param1 == 0.0 {
481 out.write("-")?;
482 } else {
483 out.write(&format!("{:.2}%", (param0*100.0)/param1))?;
484 }
485 Ok(())
486}