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","
" -replace "`n","
" @" $Subject $encoded "@ 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","
" -replace "`n","
" @" $Subject $encoded "@ 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","
" -replace "`n","
" # 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