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 } } 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 {} # Parsing del body $lines = $Body -split "(`r`n|`n)" $startLine = $lines | Where-Object { $_ -match 'Start:' } | Select-Object -First 1 $endLine = $lines | Where-Object { $_ -match 'Fine:' } | Select-Object -First 1 $startTime = if ($startLine) { ($startLine -replace '.*Start:\s*','').Trim() } else { '' } $endTime = if ($endLine) { ($endLine -replace '.*Fine:\s*','').Trim() } else { '' } $backupDateTime = if ($endTime) { $endTime } elseif ($startTime) { $startTime } else { (Get-Date).ToString('dd/MM/yyyy HH:mm:ss') } # Durata - provo sia con dd/MM/yyyy che dd-MM-yyyy $durationStr = '' try { $culture = [System.Globalization.CultureInfo]::GetCultureInfo('it-IT') $formats = @('dd/MM/yyyy HH:mm:ss','dd-MM-yyyy HH:mm:ss') [datetime]$startDt = $null [datetime]$endDt = $null if ($startTime) { foreach ($fmt in $formats) { if ([datetime]::TryParseExact($startTime, $fmt, $culture, [System.Globalization.DateTimeStyles]::None, [ref]$startDt)) { break } } } if ($endTime) { foreach ($fmt in $formats) { if ([datetime]::TryParseExact($endTime, $fmt, $culture, [System.Globalization.DateTimeStyles]::None, [ref]$endDt)) { break } } } if ($startDt -and $endDt) { $span = $endDt - $startDt $durationStr = Format-Duration -Span $span } } catch {} # --- Dimensioni archivi SQL / FILES, usando in modo sicuro la variabile $moved --- $archives = @() 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." } # Fallback: cerco *.7z dentro il testo del log 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 } } } } } [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' try { $diskRoot = $null $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) $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) $target = '' $fst = '' $login = '' $domain = '' try { $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 {} # 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: # - Dimensione totale -> totale (SQL + FILES) # - Dimensione backup -> solo FILES # - Dati letti -> solo SQL # - Dettagli / Dimensione -> totale # - Dettagli / Letti -> solo SQL # - Dettagli / Trasferiti -> solo FILES $html = $html.Replace('XXXTOTALSIZEXXX', $sizeTotalStr) $html = $html.Replace('XXXBACKUPSIZEXXX', $sizeFilesStr) $html = $html.Replace('XXXDATAREADXXX', $sizeSqlStr) $html = $html.Replace('XXXREADXXX', $sizeSqlStr) $html = $html.Replace('XXXTRANSFERREDXXX', $sizeFilesStr) # Destinazione backup $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