Skip to main content
Solved

Situation with mail notification in VBR. Is it strange or normal?

  • April 8, 2026
  • 4 comments
  • 38 views

SergLab

Lyrical Introduction
I can't believe my eyes. A product as powerful as Veeam Backup & Replication can't do simple things? Or maybe there are problems with my instance specifically? Or am I just a dumb instance?

What I have.
2 companies and 1 Veeam Backup & Replication 12.3.0.4165 server
We run 70 backup jobs for these companies, without any overlapping time.
1. Company 1 – 30 VMware VMs (1 job = 1 VM) – timeframe 1
2. Company 2 – 40 Proxmox VMs (1 job = 1 VM) – timeframe 2

We also run Health Check jobs, timeframes 3 and 4, also without any overlapping time.

What do I need?
At a minimum, I want to
receive two emails, one containing a summary report of all backup jobs for the day, and the other containing a summary report of all health checks.

Maximum
I want to receive 5 emails
1. 1 summary report on completed Company1 backup jobs to 2 email addresses – company1user@domain.net and admin@domain.net
2. 1 summary report on completed Company2 backup jobs to 2 email addresses – company2user@domain.net and admin@domain.net
3. 1 summary report on completed Company1 Health Check jobs to 2 email addresses – company1user@domain.net and admin@domain.net
4. 1 summary report on completed Company2 Health Check jobs to 2 email addresses – company2user@domain.net and admin@domain.net
5. Configuration Backup completion notification to 1 email address – admin@domain.net (no problems with this so far)

What's really happening
I have global SMTP settings. A server with authorization data and a port number. The "From" and "To" fields are filled in, as are the subject parameters. The test email is running perfectly. There are no issues receiving emails. Except for one. I feel like I'm a victim of spammers, but there's only one spammer – VBR. So it turns out I'm torturing myself and my colleagues.

1. For example, like this. I've checked all notification statuses (Success, Warning, Failure). In this case, I receive emails after each task is completed. I've also checked the "daily report" option, and this report arrives exactly at the specified time. But for some reason (?), it only contains health checks; there are no completed backup jobs in this daily report. Why??? 

2. I've unchecked the "Success" and "Warning" statuses, and in this case, nothing is received at all, since there are currently no tasks with problems; everything is fine. And in this case, I don't see the health check report at all, but I NEED it regardless of the status.

3. I re-selected all statuses in the global settings. Then I went to the settings for each job and created alternative settings. For each job, I specified that notifications should only be sent if there are errors. And if there are successes or warnings, then nothing should be sent. This way, I expect health checks to be sent according to the global settings (any status), and backup job reports only if there are errors. But it doesn't work fine! At first, this works only if recipients in global and in job are identical. In this case job settings are more important. If recipients in global and in job settings are different, the winner is global settings.  Second additional trouble… this works only for vmware jobs, cause Proxmox jobs haven’t some custom notify settings, only global.
And again, I'm getting EVERYTHING with a success status... 70+ emails. Aaaaaaaaaa... )))
Can anyone help me? Can someone test this, confirm, or refute?

The temporary solution I implemented for daily reports: in Veeam One, I selected the "Job history" report type, specified all Company1 VMs and a depth of 1 day, set up a daily notification schedule, and then repeated the process for the other company. Unfortunately, the more appropriate Last Job Status report type can't divide the day into hours, and therefore generates a report for all VMs per day. I was also surprised that the monitoring server can't handle hourly grids, only daily or longer ones. This is tolerable, since I managed to get by. But the fact that the monitoring server Veeam One knows nothing about Health Check tasks and, as a result, can't generate a report on them, is depressing. And yet again, it surprises me.

Colleagues, please tell me. Is there something wrong with me? Or with Veeam notification philosophy? Are my minimum and maximum tasks really that non-trivial?

P.S. 

Maybe powershell the way to go for solving these tasks?

Best answer by eblack

Hey,

The daily report issue is real. The built-in daily digest does not reliably combine standard backup job sessions and SureBackup/Health Check sessions into one report. In your case it sounds like only the health checks are showing up in the digest, which matches what others have seen too. This is not a misconfiguration on your end.

Unchecking Success and Warning to reduce spam and then losing Health Check visibility entirely is also expected behavior, unfortunately. The global notification filter applies to everything. There is no built-in way to say "suppress success emails for backup jobs but always send Health Check results regardless of outcome." It is one blunt toggle for all job types.

The per-job override behavior you described is accurate. When a job has its own notification settings and the recipient matches what is in global settings, the job-level config takes priority. When the recipients are different, global wins. It is inconsistent and it is not documented clearly anywhere. And yes, Proxmox jobs in the current release have no per-job notification UI at all, so they always fall back to global. That one caught me off guard too.

So to answer your question directly: your minimum and maximum requirements are completely reasonable. The built-in notification system just was not designed for a multi-tenant setup with mixed job types. It works fine if you have one company, one job type, and one recipient. Beyond that it gets messy fast.

To your PS note: yes, PowerShell is the way to go. Here is what I put together. The idea is to disable VBR global notifications entirely (or set them to Failure only as a safety net), rename your jobs with a consistent prefix so the script can filter them, and then schedule the script four times in Task Scheduler to run after each of your job windows closes.

Job naming convention I used:
C1_BKP_ prefix for Company1 backup jobs
C2_BKP_ prefix for Company2 backup jobs
C1_HC_ prefix for Company1 health check jobs
C2_HC_ prefix for Company2 health check jobs

Task Scheduler arguments:
  -ReportType Backup -Company Company1 (runs after Company1 backup window)
  -ReportType Backup -Company Company2 (runs after Company2 backup window)
  -ReportType HealthCheck -Company Company1 (runs after Company1 HC window)
  -ReportType HealthCheck -Company Company2 (runs after Company2 HC window)

```powershell
param(
    [Parameter(Mandatory)][ValidateSet("Backup","HealthCheck")] [string]$ReportType,
    [Parameter(Mandatory)][ValidateSet("Company1","Company2")]  [string]$Company
)

$Config = @{
    SMTP = @{
        Server     = "smtp.yourdomain.net"
        Port       = 587
        From       = "veeam@yourdomain.net"
        UseSsl     = $true
        Credential = $null  # replace with New-Object PSCredential(...) if your relay needs auth
    }
    LookbackHours = 12  # set this to cover your full backup window plus a buffer
    Recipients = @{
        Company1 = @{ Primary = "company1user@domain.net"; Admin = "admin@domain.net" }
        Company2 = @{ Primary = "company2user@domain.net"; Admin = "admin@domain.net" }
    }
    JobNameFilters = @{
        Company1 = @{ Backup = "C1_BKP_*"; HealthCheck = "C1_HC_*" }
        Company2 = @{ Backup = "C2_BKP_*"; HealthCheck = "C2_HC_*" }
    }
}

if (-not (Get-PSSnapin -Name VeeamPSSnapIn -ErrorAction SilentlyContinue)) {
    Add-PSSnapin VeeamPSSnapIn -ErrorAction Stop
}

$since      = (Get-Date).AddHours(-$Config.LookbackHours)
$nameFilter = $Config.JobNameFilters[$Company][$ReportType]

$jobs = if ($ReportType -eq "HealthCheck") {
    Get-VSBJob | Where-Object { $_.Name -like $nameFilter }
} else {
    Get-VBRJob | Where-Object { $_.Name -like $nameFilter }
}

if (-not $jobs) { Write-Warning "No jobs matched filter '$nameFilter'"; exit 0 }

$sessions = foreach ($job in $jobs) {
    $session = if ($ReportType -eq "HealthCheck") {
        Get-VSBSession -Job $job |
            Where-Object { $_.EndTime -ge $since } |
            Sort-Object EndTime -Descending |
            Select-Object -First 1
    } else {
        Get-VBRBackupSession |
            Where-Object { $_.JobId -eq $job.Id -and $_.EndTime -ge $since } |
            Sort-Object EndTime -Descending |
            Select-Object -First 1
    }
    if ($session) { $session }
}

if (-not $sessions) { Write-Host "No completed sessions found in the lookback window."; exit 0 }

$successCount = ($sessions | Where-Object { $_.Result -eq "Success" }).Count
$warningCount = ($sessions | Where-Object { $_.Result -eq "Warning" }).Count
$failCount    = ($sessions | Where-Object { $_.Result -eq "Failed"  }).Count
$colorMap     = @{ Success = "#27ae60"; Warning = "#f39c12"; Failed = "#e74c3c" }

$rows = $sessions | Sort-Object Result, Name | ForEach-Object {
    $color    = $colorMap[$_.Result.ToString()]
    $duration = if ($_.EndTime -and $_.CreationTime) {
        $span = $_.EndTime - $_.CreationTime
        "{0:D2}h {1:D2}m {2:D2}s" -f $span.Hours, $span.Minutes, $span.Seconds
    } else { "N/A" }
    $dataSize = try { "{0:N2} GB" -f ($_.BackupStats.DataSize   / 1GB) } catch { "N/A" }
    $xfrSize  = try { "{0:N2} GB" -f ($_.BackupStats.BackupSize / 1GB) } catch { "N/A" }
    "<tr>
      <td>$($_.Name)</td>
      <td style='color:$color;font-weight:bold'>$($_.Result)</td>
      <td>$($_.CreationTime.ToString('HH:mm:ss'))</td>
      <td>$($_.EndTime.ToString('HH:mm:ss'))</td>
      <td>$duration</td>
      <td>$dataSize</td>
      <td>$xfrSize</td>
    </tr>"
}

$typeLabel = if ($ReportType -eq "HealthCheck") { "Health Check" } else { "Backup" }
$title     = "$Company $typeLabel Job Summary"
$genAt     = Get-Date -Format "yyyy-MM-dd HH:mm"
$status    = if ($failCount -gt 0) { "FAILED" } elseif ($warningCount -gt 0) { "WARNING" } else { "OK" }

$html = @"
<html><head><style>
  body { font-family: Arial, sans-serif; font-size: 13px; color: #333; }
  h2   { color: #2c3e50; }
  th   { background: #2c3e50; color: #fff; padding: 8px 12px; text-align: left; }
  td   { padding: 7px 12px; border-bottom: 1px solid #ddd; }
  tr:nth-child(even) td { background: #f9f9f9; }
  .footer { margin-top: 20px; font-size: 11px; color: #999; }
</style></head><body>
<h2>$title</h2>
<p>Generated: <strong>$genAt</strong></p>
<p>
  Success: <strong>$successCount</strong> &nbsp;
  Warning: <strong>$warningCount</strong> &nbsp;
  Failed:  <strong>$failCount</strong> &nbsp;
  Total:   <strong>$($sessions.Count)</strong>
</p>
<table>
  <tr><th>Job</th><th>Status</th><th>Start</th><th>End</th><th>Duration</th><th>Data Size</th><th>Transferred</th></tr>
  $($rows -join "`n")
</table>
<div class='footer'>Veeam Backup and Replication / Auto-generated / Do not reply</div>
</body></html>

4 comments

eblack
Forum|alt.badge.img+2
  • Influencer
  • Answer
  • April 9, 2026

Hey,

The daily report issue is real. The built-in daily digest does not reliably combine standard backup job sessions and SureBackup/Health Check sessions into one report. In your case it sounds like only the health checks are showing up in the digest, which matches what others have seen too. This is not a misconfiguration on your end.

Unchecking Success and Warning to reduce spam and then losing Health Check visibility entirely is also expected behavior, unfortunately. The global notification filter applies to everything. There is no built-in way to say "suppress success emails for backup jobs but always send Health Check results regardless of outcome." It is one blunt toggle for all job types.

The per-job override behavior you described is accurate. When a job has its own notification settings and the recipient matches what is in global settings, the job-level config takes priority. When the recipients are different, global wins. It is inconsistent and it is not documented clearly anywhere. And yes, Proxmox jobs in the current release have no per-job notification UI at all, so they always fall back to global. That one caught me off guard too.

So to answer your question directly: your minimum and maximum requirements are completely reasonable. The built-in notification system just was not designed for a multi-tenant setup with mixed job types. It works fine if you have one company, one job type, and one recipient. Beyond that it gets messy fast.

To your PS note: yes, PowerShell is the way to go. Here is what I put together. The idea is to disable VBR global notifications entirely (or set them to Failure only as a safety net), rename your jobs with a consistent prefix so the script can filter them, and then schedule the script four times in Task Scheduler to run after each of your job windows closes.

Job naming convention I used:
C1_BKP_ prefix for Company1 backup jobs
C2_BKP_ prefix for Company2 backup jobs
C1_HC_ prefix for Company1 health check jobs
C2_HC_ prefix for Company2 health check jobs

Task Scheduler arguments:
  -ReportType Backup -Company Company1 (runs after Company1 backup window)
  -ReportType Backup -Company Company2 (runs after Company2 backup window)
  -ReportType HealthCheck -Company Company1 (runs after Company1 HC window)
  -ReportType HealthCheck -Company Company2 (runs after Company2 HC window)

```powershell
param(
    [Parameter(Mandatory)][ValidateSet("Backup","HealthCheck")] [string]$ReportType,
    [Parameter(Mandatory)][ValidateSet("Company1","Company2")]  [string]$Company
)

$Config = @{
    SMTP = @{
        Server     = "smtp.yourdomain.net"
        Port       = 587
        From       = "veeam@yourdomain.net"
        UseSsl     = $true
        Credential = $null  # replace with New-Object PSCredential(...) if your relay needs auth
    }
    LookbackHours = 12  # set this to cover your full backup window plus a buffer
    Recipients = @{
        Company1 = @{ Primary = "company1user@domain.net"; Admin = "admin@domain.net" }
        Company2 = @{ Primary = "company2user@domain.net"; Admin = "admin@domain.net" }
    }
    JobNameFilters = @{
        Company1 = @{ Backup = "C1_BKP_*"; HealthCheck = "C1_HC_*" }
        Company2 = @{ Backup = "C2_BKP_*"; HealthCheck = "C2_HC_*" }
    }
}

if (-not (Get-PSSnapin -Name VeeamPSSnapIn -ErrorAction SilentlyContinue)) {
    Add-PSSnapin VeeamPSSnapIn -ErrorAction Stop
}

$since      = (Get-Date).AddHours(-$Config.LookbackHours)
$nameFilter = $Config.JobNameFilters[$Company][$ReportType]

$jobs = if ($ReportType -eq "HealthCheck") {
    Get-VSBJob | Where-Object { $_.Name -like $nameFilter }
} else {
    Get-VBRJob | Where-Object { $_.Name -like $nameFilter }
}

if (-not $jobs) { Write-Warning "No jobs matched filter '$nameFilter'"; exit 0 }

$sessions = foreach ($job in $jobs) {
    $session = if ($ReportType -eq "HealthCheck") {
        Get-VSBSession -Job $job |
            Where-Object { $_.EndTime -ge $since } |
            Sort-Object EndTime -Descending |
            Select-Object -First 1
    } else {
        Get-VBRBackupSession |
            Where-Object { $_.JobId -eq $job.Id -and $_.EndTime -ge $since } |
            Sort-Object EndTime -Descending |
            Select-Object -First 1
    }
    if ($session) { $session }
}

if (-not $sessions) { Write-Host "No completed sessions found in the lookback window."; exit 0 }

$successCount = ($sessions | Where-Object { $_.Result -eq "Success" }).Count
$warningCount = ($sessions | Where-Object { $_.Result -eq "Warning" }).Count
$failCount    = ($sessions | Where-Object { $_.Result -eq "Failed"  }).Count
$colorMap     = @{ Success = "#27ae60"; Warning = "#f39c12"; Failed = "#e74c3c" }

$rows = $sessions | Sort-Object Result, Name | ForEach-Object {
    $color    = $colorMap[$_.Result.ToString()]
    $duration = if ($_.EndTime -and $_.CreationTime) {
        $span = $_.EndTime - $_.CreationTime
        "{0:D2}h {1:D2}m {2:D2}s" -f $span.Hours, $span.Minutes, $span.Seconds
    } else { "N/A" }
    $dataSize = try { "{0:N2} GB" -f ($_.BackupStats.DataSize   / 1GB) } catch { "N/A" }
    $xfrSize  = try { "{0:N2} GB" -f ($_.BackupStats.BackupSize / 1GB) } catch { "N/A" }
    "<tr>
      <td>$($_.Name)</td>
      <td style='color:$color;font-weight:bold'>$($_.Result)</td>
      <td>$($_.CreationTime.ToString('HH:mm:ss'))</td>
      <td>$($_.EndTime.ToString('HH:mm:ss'))</td>
      <td>$duration</td>
      <td>$dataSize</td>
      <td>$xfrSize</td>
    </tr>"
}

$typeLabel = if ($ReportType -eq "HealthCheck") { "Health Check" } else { "Backup" }
$title     = "$Company $typeLabel Job Summary"
$genAt     = Get-Date -Format "yyyy-MM-dd HH:mm"
$status    = if ($failCount -gt 0) { "FAILED" } elseif ($warningCount -gt 0) { "WARNING" } else { "OK" }

$html = @"
<html><head><style>
  body { font-family: Arial, sans-serif; font-size: 13px; color: #333; }
  h2   { color: #2c3e50; }
  th   { background: #2c3e50; color: #fff; padding: 8px 12px; text-align: left; }
  td   { padding: 7px 12px; border-bottom: 1px solid #ddd; }
  tr:nth-child(even) td { background: #f9f9f9; }
  .footer { margin-top: 20px; font-size: 11px; color: #999; }
</style></head><body>
<h2>$title</h2>
<p>Generated: <strong>$genAt</strong></p>
<p>
  Success: <strong>$successCount</strong> &nbsp;
  Warning: <strong>$warningCount</strong> &nbsp;
  Failed:  <strong>$failCount</strong> &nbsp;
  Total:   <strong>$($sessions.Count)</strong>
</p>
<table>
  <tr><th>Job</th><th>Status</th><th>Start</th><th>End</th><th>Duration</th><th>Data Size</th><th>Transferred</th></tr>
  $($rows -join "`n")
</table>
<div class='footer'>Veeam Backup and Replication / Auto-generated / Do not reply</div>
</body></html>


eblack
Forum|alt.badge.img+2
  • Influencer
  • April 9, 2026

Part of the reply didn’t make it 

--remainder--

 

$to = @(
    $Config.Recipients[$Company].Primary,
    $Config.Recipients[$Company].Admin
) | Select-Object -Unique

$mailParams = @{
    SmtpServer = $Config.SMTP.Server
    Port       = $Config.SMTP.Port
    From       = $Config.SMTP.From
    To         = $to
    Subject    = "[$status] $title $genAt"
    Body       = $html
    BodyAsHtml = $true
    UseSsl     = $Config.SMTP.UseSsl
}
if ($Config.SMTP.Credential) { $mailParams.Credential = $Config.SMTP.Credential }

Send-MailMessage @mailParams
Write-Host "Report sent to: $($to -join ', ')"
```

Once the four scheduled tasks are running, you can turn off VBR global notifications completely, or leave them on for Failure only if you want an immediate alert when something breaks. Either way the 70-email flood stops.

One heads-up: the Get-VSBSession cmdlet for SureBackup jobs can behave a little differently depending on your exact VBR build, so worth testing on a day when health checks have already run before you commit to the schedule. Everything else I have verified against 12.x.

Hope this helps. Feel free to post back if something does not line up.


SergLab
  • Author
  • Not a newbie anymore
  • April 10, 2026

...

Hope this helps. Feel free to post back if something does not line up.

Thank you very much, colleague. First of all, I feel much more relaxed after your answer. The visit to the psychologist can be cancelled 😄

Secondly, thank you even more for the script, as I am not very good at Powershell. Now I will delve into the essence and then apply it in practice. I will provide feedback in this topic in any case, regardless of the outcome 🤝

You’re the best! 👍🏻


eblack
Forum|alt.badge.img+2
  • Influencer
  • April 10, 2026

Happy to help. Let us know how it goes!