525 lines
17 KiB
PowerShell
525 lines
17 KiB
PowerShell
param(
|
||
[Parameter(Mandatory = $true)]
|
||
[string]$Subject,
|
||
|
||
[Parameter(Mandatory = $true)]
|
||
[string]$Body
|
||
)
|
||
|
||
# Se Write-Log non esiste (esecuzione standalone), creo uno stub minimale
|
||
if (-not (Get-Command -Name Write-Log -ErrorAction SilentlyContinue)) {
|
||
function Write-Log {
|
||
param(
|
||
[string]$Level,
|
||
[string]$Message
|
||
)
|
||
$ts = Get-Date -Format 'dd/MM/yyyy HH:mm:ss'
|
||
Write-Host "[$ts] [$Level] $Message"
|
||
}
|
||
}
|
||
|
||
function Format-Bytes {
|
||
param(
|
||
[long]$Bytes
|
||
)
|
||
if ($Bytes -ge 1GB) {
|
||
return ("{0:N2} GB" -f ($Bytes / 1GB))
|
||
} elseif ($Bytes -ge 1MB) {
|
||
return ("{0:N2} MB" -f ($Bytes / 1MB))
|
||
} elseif ($Bytes -ge 1KB) {
|
||
return ("{0:N2} KB" -f ($Bytes / 1KB))
|
||
} else {
|
||
return ("{0} B" -f $Bytes)
|
||
}
|
||
}
|
||
|
||
function Format-Duration {
|
||
param(
|
||
[TimeSpan]$Span
|
||
)
|
||
if ($Span.TotalDays -ge 1) {
|
||
$days = [int]$Span.TotalDays
|
||
return ("{0}g {1:00}:{2:00}:{3:00}" -f $days, $Span.Hours, $Span.Minutes, $Span.Seconds)
|
||
} else {
|
||
return ("{0:00}:{1:00}:{2:00}" -f [int]$Span.TotalHours, $Span.Minutes, $Span.Seconds)
|
||
}
|
||
}
|
||
|
||
function Get-StatusInfo {
|
||
param(
|
||
[string]$Body
|
||
)
|
||
|
||
# Default: COMPLETATO (verde)
|
||
$statusText = 'COMPLETATO'
|
||
$color = '#4CAF50' # verde
|
||
$succ = '1'
|
||
$warn = '0'
|
||
$err = '0'
|
||
|
||
if ($Body -match 'ESITO:\s*FALLITO') {
|
||
$statusText = 'FALLITO'
|
||
$color = '#F44336' # rosso
|
||
$succ = '0'
|
||
$warn = '0'
|
||
$err = '1'
|
||
}
|
||
elseif ($Body -match 'ESITO:\s*COMPLETATO CON WARNING') {
|
||
$statusText = 'COMPLETATO CON WARNING'
|
||
$color = '#FFC107' # giallo
|
||
$succ = '0'
|
||
$warn = '1'
|
||
$err = '0'
|
||
}
|
||
|
||
[PSCustomObject]@{
|
||
Text = $statusText
|
||
Color = $color
|
||
Success = $succ
|
||
Warning = $warn
|
||
Error = $err
|
||
}
|
||
}
|
||
|
||
function Build-ReportHtml {
|
||
param(
|
||
[string]$Subject,
|
||
[string]$Body
|
||
)
|
||
|
||
# Cerca il template nella cartella di configurazione:
|
||
# 1) eventuale $script:InstallerConfDir / $InstallerConfDir
|
||
# 2) cartella bin\conf sotto la cartella dello script
|
||
# 3) percorso fisso C:\polo\scripts\bin\conf
|
||
$templatePath = $null
|
||
|
||
$candidateDirs = @()
|
||
|
||
if ($script:InstallerConfDir) { $candidateDirs += $script:InstallerConfDir }
|
||
if ($InstallerConfDir) { $candidateDirs += $InstallerConfDir }
|
||
|
||
if ($PSScriptRoot) {
|
||
$candidateDirs += (Join-Path $PSScriptRoot 'bin/conf')
|
||
} elseif ($PSCommandPath) {
|
||
$candidateDirs += (Join-Path (Split-Path -Parent $PSCommandPath) 'bin/conf')
|
||
}
|
||
|
||
$candidateDirs += 'C:\polo\scripts\bin\conf'
|
||
|
||
foreach ($dir in $candidateDirs) {
|
||
if (-not $dir) { continue }
|
||
$path = Join-Path $dir 'report_template.html'
|
||
if (Test-Path -LiteralPath $path) {
|
||
$templatePath = $path
|
||
break
|
||
}
|
||
}
|
||
|
||
# Fallback: invio testo semplice
|
||
if (-not $templatePath) {
|
||
Write-Log WARN "[MAIL] Template HTML non trovato. Invio in testo semplice."
|
||
$encoded = $Body -replace '&','&' -replace '<','<' -replace '>','>'
|
||
$encoded = $encoded -replace "`r`n","<br />" -replace "`n","<br />"
|
||
|
||
@"
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>$Subject</title>
|
||
</head>
|
||
<body style="font-family: Tahoma, Arial, sans-serif; font-size: 12px;">
|
||
$encoded
|
||
</body>
|
||
</html>
|
||
"@
|
||
return
|
||
}
|
||
|
||
try {
|
||
# IMPORTANTE: leggi il template come UTF-8 per evitare i caratteri tipo – / Ã
|
||
$template = Get-Content -LiteralPath $templatePath -Raw -Encoding UTF8
|
||
} catch {
|
||
Write-Log WARN "[MAIL] Errore lettura template HTML: $_. Invio fallback."
|
||
$template = $null
|
||
}
|
||
|
||
if (-not $template) {
|
||
$encoded = $Body -replace '&','&' -replace '<','<' -replace '>','>'
|
||
$encoded = $encoded -replace "`r`n","<br />" -replace "`n","<br />"
|
||
|
||
@"
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>$Subject</title>
|
||
</head>
|
||
<body style="font-family: Tahoma, Arial, sans-serif; font-size: 12px;">
|
||
$encoded
|
||
</body>
|
||
</html>
|
||
"@
|
||
return
|
||
}
|
||
|
||
$statusInfo = Get-StatusInfo -Body $Body
|
||
|
||
# Host / agente
|
||
$agent = if ($HostName) {
|
||
$HostName
|
||
} elseif ($env:COMPUTERNAME) {
|
||
$env:COMPUTERNAME
|
||
} else {
|
||
'Sconosciuto'
|
||
}
|
||
|
||
# Versione script (se definita nel main, es: $ScriptVersion)
|
||
$scriptVersion = 'N/D'
|
||
try {
|
||
$sv = Get-Variable -Name ScriptVersion -ErrorAction SilentlyContinue
|
||
if ($sv) { $scriptVersion = $sv.Value }
|
||
} catch { }
|
||
|
||
# --- Estrazione Start / Fine dal body tramite regex ---
|
||
$startTime = ''
|
||
$endTime = ''
|
||
|
||
$startMatch = [regex]::Match($Body, 'Start:\s*(\d{2}-\d{2}-\d{4}\s+\d{2}:\d{2}:\d{2})')
|
||
if ($startMatch.Success) {
|
||
$startTime = $startMatch.Groups[1].Value.Trim()
|
||
}
|
||
|
||
$endMatch = [regex]::Match($Body, 'Fine:\s*(\d{2}-\d{2}-\d{4}\s+\d{2}:\d{2}:\d{2})')
|
||
if ($endMatch.Success) {
|
||
$endTime = $endMatch.Groups[1].Value.Trim()
|
||
}
|
||
|
||
# Se "Fine:" non è ancora presente nel log passato, uso l’istante attuale
|
||
if (-not $endTime) {
|
||
$endTime = (Get-Date).ToString('dd-MM-yyyy HH:mm:ss')
|
||
}
|
||
|
||
# Data/ora del report: uso l’ora di fine se disponibile, altrimenti quella di inizio
|
||
$backupDateTime = if ($endTime) {
|
||
$endTime
|
||
} elseif ($startTime) {
|
||
$startTime
|
||
} else {
|
||
(Get-Date).ToString('dd/MM/yyyy HH:mm:ss')
|
||
}
|
||
|
||
# --- Calcolo durata: Fine - Inizio ---
|
||
$durationStr = ''
|
||
try {
|
||
if ($startTime -and $endTime) {
|
||
$fmt = 'dd-MM-yyyy HH:mm:ss'
|
||
$culture = [System.Globalization.CultureInfo]::InvariantCulture
|
||
$startDt = [datetime]::ParseExact($startTime, $fmt, $culture)
|
||
$endDt = [datetime]::ParseExact($endTime, $fmt, $culture)
|
||
$span = $endDt - $startDt
|
||
$durationStr = Format-Duration -Span $span
|
||
}
|
||
} catch {
|
||
Write-Log WARN "[MAIL] Impossibile calcolare la durata: $_"
|
||
$durationStr = 'N/D'
|
||
}
|
||
|
||
# --- Dimensioni archivi SQL / FILES ---
|
||
$archives = @()
|
||
|
||
# 1) Prova con la variabile $moved (se il main script la imposta a percorsi)
|
||
try {
|
||
$movedVar = Get-Variable -Name moved -ErrorAction Stop
|
||
$movedVal = $movedVar.Value
|
||
if ($movedVal) {
|
||
$archives = @($movedVal) | Where-Object { $_ -and (Test-Path -LiteralPath $_) }
|
||
}
|
||
} catch {
|
||
Write-Log INFO "[MAIL] Variabile 'moved' non disponibile, provo a recuperare i .7z dal log."
|
||
}
|
||
|
||
# 2) Fallback: cerco *.7z dentro il testo del log (percorso completo)
|
||
if (-not $archives -or $archives.Count -eq 0) {
|
||
$tokens = $Body -split '\s+'
|
||
foreach ($tok in $tokens) {
|
||
if ($tok -like '*.7z') {
|
||
$path = $tok.Trim("`";',.")
|
||
if ($path -and (Test-Path -LiteralPath $path)) {
|
||
if (-not ($archives -contains $path)) {
|
||
$archives += $path
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
# 3) Fallback avanzato: usa solo il NOME FILE e cerca in BackupRoot\Files / BackupRoot\Databases / BackupRoot\out
|
||
if (-not $archives -or $archives.Count -eq 0) {
|
||
try {
|
||
$brVar = Get-Variable -Name BackupRoot -ErrorAction SilentlyContinue
|
||
if ($brVar -and $brVar.Value) {
|
||
$br = $brVar.Value
|
||
|
||
$dirs = @(
|
||
(Join-Path $br 'Files'),
|
||
(Join-Path $br 'Databases'),
|
||
(Join-Path $br 'out')
|
||
)
|
||
|
||
$tokens = $Body -split '\s+'
|
||
$names = @()
|
||
|
||
foreach ($tok in $tokens) {
|
||
if ($tok -like '*.7z') {
|
||
$clean = $tok.Trim("`";',.")
|
||
if ($clean) {
|
||
$name = [System.IO.Path]::GetFileName($clean)
|
||
if ($name) { $names += $name }
|
||
}
|
||
}
|
||
}
|
||
|
||
$names = $names | Select-Object -Unique
|
||
|
||
foreach ($name in $names) {
|
||
foreach ($dir in $dirs) {
|
||
if (-not $dir) { continue }
|
||
$candidate = Join-Path $dir $name
|
||
if (Test-Path -LiteralPath $candidate) {
|
||
if (-not ($archives -contains $candidate)) {
|
||
$archives += $candidate
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch {
|
||
Write-Log WARN "[MAIL] Errore nel tentativo di individuare gli archivi in Files/Databases/out: $_"
|
||
}
|
||
}
|
||
|
||
[long]$totalBytes = 0
|
||
[long]$sqlBytes = 0
|
||
[long]$fileBytes = 0
|
||
|
||
foreach ($p in $archives) {
|
||
try {
|
||
$fi = Get-Item -LiteralPath $p -ErrorAction Stop
|
||
$size = [long]$fi.Length
|
||
$totalBytes += $size
|
||
|
||
if ($fi.Name -match '^SQL_') {
|
||
$sqlBytes += $size
|
||
} elseif ($fi.Name -match '^FILES_') {
|
||
$fileBytes += $size
|
||
}
|
||
} catch {
|
||
Write-Log WARN ("[MAIL] Impossibile leggere dimensione archivio {0}: {1}" -f $p, $_)
|
||
}
|
||
}
|
||
|
||
$sizeTotalStr = if ($totalBytes -gt 0) { Format-Bytes -Bytes $totalBytes } else { 'N/D' }
|
||
$sizeSqlStr = if ($sqlBytes -gt 0) { Format-Bytes -Bytes $sqlBytes } else { 'N/D' }
|
||
$sizeFilesStr = if ($fileBytes -gt 0) { Format-Bytes -Bytes $fileBytes } else { 'N/D' }
|
||
|
||
# Info disco (unità di BackupRoot o del primo archivio)
|
||
$diskSize = 'N/D'
|
||
$diskUsed = 'N/D'
|
||
$diskAvail = 'N/D'
|
||
$diskUsePct = 'N/D'
|
||
$diskFs = 'N/D'
|
||
$diskRoot = $null
|
||
|
||
try {
|
||
$brVar = Get-Variable -Name BackupRoot -ErrorAction SilentlyContinue
|
||
if ($brVar -and $brVar.Value) {
|
||
$diskRoot = $brVar.Value
|
||
} elseif ($archives.Count -gt 0) {
|
||
$diskRoot = $archives[0]
|
||
}
|
||
|
||
if ($diskRoot) {
|
||
$driveRoot = [System.IO.Path]::GetPathRoot($diskRoot)
|
||
if ($driveRoot) {
|
||
$di = New-Object System.IO.DriveInfo($driveRoot)
|
||
$diskFs = $di.DriveFormat
|
||
$diskSize = Format-Bytes -Bytes $di.TotalSize
|
||
$diskAvail = Format-Bytes -Bytes $di.AvailableFreeSpace
|
||
$used = $di.TotalSize - $di.AvailableFreeSpace
|
||
$diskUsed = Format-Bytes -Bytes $used
|
||
if ($di.TotalSize -gt 0) {
|
||
$diskUsePct = ('{0:N1}%%' -f (($used / $di.TotalSize) * 100))
|
||
}
|
||
}
|
||
}
|
||
} catch {
|
||
Write-Log WARN "[MAIL] Impossibile calcolare spazio disco: $_"
|
||
}
|
||
|
||
# Info destinazione (rclone) + login/dominio + percorso / FS locale
|
||
$target = ''
|
||
$fst = ''
|
||
$localPathStr = if ($diskRoot) { $diskRoot } else { 'N/D' }
|
||
$localFsStr = $diskFs
|
||
|
||
# Login/Dominio dell'utente Windows che esegue il backup
|
||
$login = if ($env:USERNAME) { $env:USERNAME } else { 'N/D' }
|
||
$domain = if ($env:USERDOMAIN) { $env:USERDOMAIN }
|
||
elseif ($env:COMPUTERNAME) { $env:COMPUTERNAME }
|
||
else { 'N/D' }
|
||
|
||
try {
|
||
# 1) Preferisci RcloneRemoteDest (come da backup.conf)
|
||
$rdVar = Get-Variable -Name RcloneRemoteDest -ErrorAction SilentlyContinue
|
||
if ($rdVar -and $rdVar.Value) {
|
||
$target = $rdVar.Value
|
||
$fst = 'rclone'
|
||
} else {
|
||
# 2) Fallback: RcloneRemote/RcloneRemotePath se esistono
|
||
$rrVar = Get-Variable -Name RcloneRemote -ErrorAction SilentlyContinue
|
||
$rpVar = Get-Variable -Name RcloneRemotePath -ErrorAction SilentlyContinue
|
||
$rrVal = if ($rrVar) { $rrVar.Value } else { $null }
|
||
$rpVal = if ($rpVar) { $rpVar.Value } else { $null }
|
||
|
||
if ($rrVal -or $rpVal) {
|
||
if ($rpVal) {
|
||
$target = "$rrVal`:$rpVal"
|
||
} else {
|
||
$target = "$rrVal`:"
|
||
}
|
||
$fst = 'rclone'
|
||
}
|
||
}
|
||
} catch {
|
||
Write-Log WARN "[MAIL] Impossibile leggere info rclone: $_"
|
||
}
|
||
|
||
# Arricchisco i campi per la riga "Destinazione backup / File system / tipo"
|
||
if ($target -and $localPathStr -and $localFsStr) {
|
||
# Esempio:
|
||
# Destinazione backup: remote:bucket/path (locale: D:\Backup\out)
|
||
# File system / tipo: rclone / NTFS (D:)
|
||
$htmlTarget = "$target (locale: $localPathStr)"
|
||
if ($diskRoot) {
|
||
$driveRoot = [System.IO.Path]::GetPathRoot($diskRoot)
|
||
$fst = "$fst / $localFsStr ($driveRoot)"
|
||
} else {
|
||
$fst = "$fst / $localFsStr"
|
||
}
|
||
$target = $htmlTarget
|
||
}
|
||
|
||
# Dettagli log in HTML
|
||
$details = $Body
|
||
$details = $details -replace '&','&' -replace '<','<' -replace '>','>'
|
||
$details = $details -replace "`r`n","<br />" -replace "`n","<br />"
|
||
|
||
# Sostituzione placeholder nel template
|
||
$html = $template
|
||
|
||
# Stato e colori (verde / giallo / rosso)
|
||
$html = $html.Replace('XXXBGCOLORXXX', $statusInfo.Color)
|
||
$html = $html.Replace('XXXSTATXXX', $statusInfo.Text)
|
||
$html = $html.Replace('XXXSTATUSXXX', $statusInfo.Text)
|
||
|
||
# Info generali
|
||
$html = $html.Replace('XXXAGENTXXX', $agent)
|
||
$html = $html.Replace('XXXHOSTNAMEXXX', $agent)
|
||
$html = $html.Replace('XXXVERSIONXXX', $scriptVersion)
|
||
|
||
# Date / orari
|
||
$html = $html.Replace('XXXBACKUPDATETIMEXXX', $backupDateTime)
|
||
$html = $html.Replace('XXXSTARTXXX', $startTime)
|
||
$html = $html.Replace('XXXENDXXX', $endTime)
|
||
$html = $html.Replace('XXXDURATIONXXX', $durationStr)
|
||
|
||
# Contatori esito
|
||
$html = $html.Replace('XXXSUCCESSXXX', $statusInfo.Success)
|
||
$html = $html.Replace('XXXWARNINGXXX', $statusInfo.Warning)
|
||
$html = $html.Replace('XXXERRORXXX', $statusInfo.Error)
|
||
|
||
# Dimensioni
|
||
$html = $html.Replace('XXXTOTALSIZEXXX', $sizeTotalStr)
|
||
$html = $html.Replace('XXXSQLSIZEXXX', $sizeSqlStr)
|
||
$html = $html.Replace('XXXFILESSIZEXXX', $sizeFilesStr)
|
||
|
||
# Destinazione backup + login/dominio + spazio locale
|
||
$html = $html.Replace('XXXTARGETXXX', $target)
|
||
$html = $html.Replace('XXXFSTXXX', $fst)
|
||
$html = $html.Replace('XXXLOGINXXX', $login)
|
||
$html = $html.Replace('XXXDOMAINXXX', $domain)
|
||
|
||
# Info disco
|
||
$html = $html.Replace('XXXDISKSIZEXXX', $diskSize)
|
||
$html = $html.Replace('XXXDISKUSEDXXX', $diskUsed)
|
||
$html = $html.Replace('XXXDISKAVAILXXX', $diskAvail)
|
||
$html = $html.Replace('XXXDISKUSEPXXX', $diskUsePct)
|
||
|
||
# Dettaglio log
|
||
$html = $html.Replace('XXXDETAILSXXX', $details)
|
||
|
||
return $html
|
||
}
|
||
|
||
function Send-ReportMail {
|
||
param(
|
||
[string]$Subject,
|
||
[string]$Body
|
||
)
|
||
|
||
$meVar = Get-Variable -Name MailEnabled -ErrorAction SilentlyContinue
|
||
if (-not $meVar -or -not $meVar.Value) {
|
||
Write-Log INFO "[MAIL] Invio mail disabilitato (MailEnabled=false o non definito)."
|
||
return
|
||
}
|
||
|
||
$htmlBody = Build-ReportHtml -Subject $Subject -Body $Body
|
||
|
||
try {
|
||
$shVar = Get-Variable -Name MailSmtpHost -ErrorAction Stop
|
||
$spVar = Get-Variable -Name MailSmtpPort -ErrorAction Stop
|
||
$maVar = Get-Variable -Name MailUseAuth -ErrorAction SilentlyContinue
|
||
$muVar = Get-Variable -Name MailUser -ErrorAction SilentlyContinue
|
||
$mpVar = Get-Variable -Name MailPassword -ErrorAction SilentlyContinue
|
||
$mfVar = Get-Variable -Name MailFrom -ErrorAction Stop
|
||
$mtVar = Get-Variable -Name MailTo -ErrorAction Stop
|
||
|
||
$MailSmtpHost = $shVar.Value
|
||
$MailSmtpPort = $spVar.Value
|
||
$MailUseAuth = if ($maVar) { $maVar.Value } else { $false }
|
||
$MailUser = if ($muVar) { $muVar.Value } else { $null }
|
||
$MailPassword = if ($mpVar) { $mpVar.Value } else { $null }
|
||
$MailFrom = $mfVar.Value
|
||
$MailTo = $mtVar.Value
|
||
|
||
$smtp = New-Object System.Net.Mail.SmtpClient($MailSmtpHost, $MailSmtpPort)
|
||
$smtp.EnableSsl = $true
|
||
$smtp.DeliveryMethod = [System.Net.Mail.SmtpDeliveryMethod]::Network
|
||
$smtp.UseDefaultCredentials = $false
|
||
|
||
if ($MailUseAuth -and $MailUser -and $MailPassword) {
|
||
$smtp.Credentials = New-Object System.Net.NetworkCredential($MailUser, $MailPassword)
|
||
}
|
||
|
||
$msg = New-Object System.Net.Mail.MailMessage
|
||
$msg.From = $MailFrom
|
||
|
||
foreach ($rcpt in @($MailTo)) {
|
||
if ($rcpt) { $msg.To.Add($rcpt) }
|
||
}
|
||
|
||
$msg.Subject = $Subject
|
||
$msg.Body = $htmlBody
|
||
$msg.IsBodyHtml = $true
|
||
$msg.BodyEncoding = [System.Text.Encoding]::UTF8
|
||
$msg.SubjectEncoding = [System.Text.Encoding]::UTF8
|
||
|
||
$smtp.Send($msg)
|
||
Write-Log INFO "[MAIL] Inviata correttamente (HTML con template)."
|
||
} catch {
|
||
Write-Log ERROR "[MAIL] Errore invio: $_"
|
||
}
|
||
}
|
||
|
||
Send-ReportMail -Subject $Subject -Body $Body |