From b172a66ee545f05196af8152ae237df08536bf56 Mon Sep 17 00:00:00 2001 From: Mattia Tadini Date: Wed, 19 Nov 2025 11:27:59 +0000 Subject: [PATCH] Add send-mail.ps1 --- send-mail.ps1 | 432 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 432 insertions(+) create mode 100644 send-mail.ps1 diff --git a/send-mail.ps1 b/send-mail.ps1 new file mode 100644 index 0000000..64dbe46 --- /dev/null +++ b/send-mail.ps1 @@ -0,0 +1,432 @@ +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