Backup-AdHoc/send-mail.ps1

525 lines
17 KiB
PowerShell
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 '&','&amp;' -replace '<','&lt;' -replace '>','&gt;'
$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 '&','&amp;' -replace '<','&lt;' -replace '>','&gt;'
$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 listante attuale
if (-not $endTime) {
$endTime = (Get-Date).ToString('dd-MM-yyyy HH:mm:ss')
}
# Data/ora del report: uso lora 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 '&','&amp;' -replace '<','&lt;' -replace '>','&gt;'
$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