]> git.proxmox.com Git - proxmox-backup.git/blob - src/server/email_notifications.rs
4a26535ba0a0ade4ca7b1daa3b8fa058f3ac01c1
[proxmox-backup.git] / src / server / email_notifications.rs
1 use anyhow::Error;
2 use serde_json::json;
3
4 use handlebars::{Handlebars, Helper, Context, RenderError, RenderContext, Output, HelperResult};
5
6 use proxmox::tools::email::sendmail;
7
8 use crate::{
9 config::verify::VerificationJobConfig,
10 config::sync::SyncJobConfig,
11 api2::types::{
12 APTUpdateInfo,
13 GarbageCollectionStatus,
14 Userid,
15 },
16 tools::format::HumanByte,
17 };
18
19 const GC_OK_TEMPLATE: &str = r###"
20
21 Datastore: {{datastore}}
22 Task ID: {{status.upid}}
23 Index file count: {{status.index-file-count}}
24
25 Removed garbage: {{human-bytes status.removed-bytes}}
26 Removed chunks: {{status.removed-chunks}}
27 Removed bad chunks: {{status.removed-bad}}
28
29 Leftover bad chunks: {{status.still-bad}}
30 Pending removals: {{human-bytes status.pending-bytes}} (in {{status.pending-chunks}} chunks)
31
32 Original Data usage: {{human-bytes status.index-data-bytes}}
33 On-Disk usage: {{human-bytes status.disk-bytes}} ({{relative-percentage status.disk-bytes status.index-data-bytes}})
34 On-Disk chunks: {{status.disk-chunks}}
35
36 Deduplication Factor: {{deduplication-factor}}
37
38 Garbage collection successful.
39
40 "###;
41
42
43 const GC_ERR_TEMPLATE: &str = r###"
44
45 Datastore: {{datastore}}
46
47 Garbage collection failed: {{error}}
48
49 "###;
50
51 const VERIFY_OK_TEMPLATE: &str = r###"
52
53 Job ID: {{job.id}}
54 Datastore: {{job.store}}
55
56 Verification successful.
57
58 "###;
59
60 const VERIFY_ERR_TEMPLATE: &str = r###"
61
62 Job ID: {{job.id}}
63 Datastore: {{job.store}}
64
65 Verification failed on these snapshots:
66
67 {{#each errors}}
68 {{this}}
69 {{/each}}
70
71 "###;
72
73 const SYNC_OK_TEMPLATE: &str = r###"
74
75 Job ID: {{job.id}}
76 Datastore: {{job.store}}
77 Remote: {{job.remote}}
78 Remote Store: {{job.remote-store}}
79
80 Synchronization successful.
81
82 "###;
83
84 const SYNC_ERR_TEMPLATE: &str = r###"
85
86 Job ID: {{job.id}}
87 Datastore: {{job.store}}
88 Remote: {{job.remote}}
89 Remote Store: {{job.remote-store}}
90
91 Synchronization failed: {{error}}
92
93 "###;
94
95 const PACKAGE_UPDATES_TEMPLATE: &str = r###"
96 Proxmox Backup Server has the following updates available:
97 {{#each updates }}
98 {{Package}}: {{OldVersion}} -> {{Version~}}
99 {{/each }}
100
101 To upgrade visit the webinderface: <https://{{fqdn}}:{{port}}/#pbsServerAdministration:updates>
102 "###;
103
104
105 lazy_static::lazy_static!{
106
107 static ref HANDLEBARS: Handlebars<'static> = {
108 let mut hb = Handlebars::new();
109
110 hb.set_strict_mode(true);
111
112 hb.register_helper("human-bytes", Box::new(handlebars_humam_bytes_helper));
113 hb.register_helper("relative-percentage", Box::new(handlebars_relative_percentage_helper));
114
115 hb.register_template_string("gc_ok_template", GC_OK_TEMPLATE).unwrap();
116 hb.register_template_string("gc_err_template", GC_ERR_TEMPLATE).unwrap();
117
118 hb.register_template_string("verify_ok_template", VERIFY_OK_TEMPLATE).unwrap();
119 hb.register_template_string("verify_err_template", VERIFY_ERR_TEMPLATE).unwrap();
120
121 hb.register_template_string("sync_ok_template", SYNC_OK_TEMPLATE).unwrap();
122 hb.register_template_string("sync_err_template", SYNC_ERR_TEMPLATE).unwrap();
123
124 hb.register_template_string("package_update_template", PACKAGE_UPDATES_TEMPLATE).unwrap();
125
126 hb
127 };
128 }
129
130 fn send_job_status_mail(
131 email: &str,
132 subject: &str,
133 text: &str,
134 ) -> Result<(), Error> {
135
136 // Note: OX has serious problems displaying text mails,
137 // so we include html as well
138 let html = format!("<html><body><pre>\n{}\n<pre>", handlebars::html_escape(text));
139
140 let nodename = proxmox::tools::nodename();
141
142 let author = format!("Proxmox Backup Server - {}", nodename);
143
144 sendmail(
145 &[email],
146 &subject,
147 Some(&text),
148 Some(&html),
149 None,
150 Some(&author),
151 )?;
152
153 Ok(())
154 }
155
156 pub fn send_gc_status(
157 email: &str,
158 datastore: &str,
159 status: &GarbageCollectionStatus,
160 result: &Result<(), Error>,
161 ) -> Result<(), Error> {
162
163 let text = match result {
164 Ok(()) => {
165 let deduplication_factor = if status.disk_bytes > 0 {
166 (status.index_data_bytes as f64)/(status.disk_bytes as f64)
167 } else {
168 1.0
169 };
170
171 let data = json!({
172 "status": status,
173 "datastore": datastore,
174 "deduplication-factor": format!("{:.2}", deduplication_factor),
175 });
176
177 HANDLEBARS.render("gc_ok_template", &data)?
178 }
179 Err(err) => {
180 let data = json!({
181 "error": err.to_string(),
182 "datastore": datastore,
183 });
184 HANDLEBARS.render("gc_err_template", &data)?
185 }
186 };
187
188 let subject = match result {
189 Ok(()) => format!(
190 "Garbage Collect Datastore '{}' successful",
191 datastore,
192 ),
193 Err(_) => format!(
194 "Garbage Collect Datastore '{}' failed",
195 datastore,
196 ),
197 };
198
199 send_job_status_mail(email, &subject, &text)?;
200
201 Ok(())
202 }
203
204 pub fn send_verify_status(
205 email: &str,
206 job: VerificationJobConfig,
207 result: &Result<Vec<String>, Error>,
208 ) -> Result<(), Error> {
209
210
211 let text = match result {
212 Ok(errors) if errors.is_empty() => {
213 let data = json!({ "job": job });
214 HANDLEBARS.render("verify_ok_template", &data)?
215 }
216 Ok(errors) => {
217 let data = json!({ "job": job, "errors": errors });
218 HANDLEBARS.render("verify_err_template", &data)?
219 }
220 Err(_) => {
221 // aborted job - do not send any email
222 return Ok(());
223 }
224 };
225
226 let subject = match result {
227 Ok(errors) if errors.is_empty() => format!(
228 "Verify Datastore '{}' successful",
229 job.store,
230 ),
231 _ => format!(
232 "Verify Datastore '{}' failed",
233 job.store,
234 ),
235 };
236
237 send_job_status_mail(email, &subject, &text)?;
238
239 Ok(())
240 }
241
242 pub fn send_sync_status(
243 email: &str,
244 job: &SyncJobConfig,
245 result: &Result<(), Error>,
246 ) -> Result<(), Error> {
247
248 let text = match result {
249 Ok(()) => {
250 let data = json!({ "job": job });
251 HANDLEBARS.render("sync_ok_template", &data)?
252 }
253 Err(err) => {
254 let data = json!({ "job": job, "error": err.to_string() });
255 HANDLEBARS.render("sync_err_template", &data)?
256 }
257 };
258
259 let subject = match result {
260 Ok(()) => format!(
261 "Sync remote '{}' datastore '{}' successful",
262 job.remote,
263 job.remote_store,
264 ),
265 Err(_) => format!(
266 "Sync remote '{}' datastore '{}' failed",
267 job.remote,
268 job.remote_store,
269 ),
270 };
271
272 send_job_status_mail(email, &subject, &text)?;
273
274 Ok(())
275 }
276
277 pub fn send_updates_available(
278 updates: &Vec<&APTUpdateInfo>,
279 ) -> Result<(), Error> {
280 // update mails always go to the root@pam configured email..
281 if let Some(email) = lookup_user_email(Userid::root_userid()) {
282 let nodename = proxmox::tools::nodename();
283 let subject = format!("New software packages available ({})", nodename);
284
285 let text = HANDLEBARS.render("package_update_template", &json!({
286 "fqdn": nix::sys::utsname::uname().nodename(), // FIXME: add get_fqdn helper like PVE?
287 "port": 8007, // user will surely request that they can change this
288 "updates": updates,
289 }))?;
290
291 send_job_status_mail(&email, &subject, &text)?;
292 }
293 Ok(())
294 }
295
296 /// Lookup users email address
297 ///
298 /// For "backup@pam", this returns the address from "root@pam".
299 pub fn lookup_user_email(userid: &Userid) -> Option<String> {
300
301 use crate::config::user::{self, User};
302
303 if userid == Userid::backup_userid() {
304 return lookup_user_email(Userid::root_userid());
305 }
306
307 if let Ok(user_config) = user::cached_config() {
308 if let Ok(user) = user_config.lookup::<User>("user", userid.as_str()) {
309 return user.email.clone();
310 }
311 }
312
313 None
314 }
315
316 // Handlerbar helper functions
317
318 fn handlebars_humam_bytes_helper(
319 h: &Helper,
320 _: &Handlebars,
321 _: &Context,
322 _rc: &mut RenderContext,
323 out: &mut dyn Output
324 ) -> HelperResult {
325 let param = h.param(0).map(|v| v.value().as_u64())
326 .flatten()
327 .ok_or(RenderError::new("human-bytes: param not found"))?;
328
329 out.write(&HumanByte::from(param).to_string())?;
330
331 Ok(())
332 }
333
334 fn handlebars_relative_percentage_helper(
335 h: &Helper,
336 _: &Handlebars,
337 _: &Context,
338 _rc: &mut RenderContext,
339 out: &mut dyn Output
340 ) -> HelperResult {
341 let param0 = h.param(0).map(|v| v.value().as_f64())
342 .flatten()
343 .ok_or(RenderError::new("relative-percentage: param0 not found"))?;
344 let param1 = h.param(1).map(|v| v.value().as_f64())
345 .flatten()
346 .ok_or(RenderError::new("relative-percentage: param1 not found"))?;
347
348 if param1 == 0.0 {
349 out.write("-")?;
350 } else {
351 out.write(&format!("{:.2}%", (param0*100.0)/param1))?;
352 }
353 Ok(())
354 }