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