Public/Send-Mail.ps1

<#
.SYNOPSIS
Sends emails using an SMTP server.
 
.DESCRIPTION
The `Send-Mail` function sends emails with HTML support, configured via environment variables
or explicit parameters. Ideal for automation scripts, CI/CD pipelines, and scheduled tasks.
 
.PARAMETER Subject
The email subject. This parameter is required.
 
.PARAMETER Body
The email body content. This parameter is required.
 
.PARAMETER IsHtml
Indicates whether the body is HTML formatted. Default is $False.
 
.PARAMETER FromName
The sender display name. If not provided, uses the machine name.
 
.PARAMETER To
The recipient email address. If not provided, uses the SMTP_TO environment variable.
 
.PARAMETER Attachments
Array of file paths to attach to the email. Files must exist on the filesystem.
 
.NOTES
SMTP credentials and settings must be defined via environment variables:
- SMTP_USER (required)
- SMTP_PASS (required)
- SMTP_SERVER (optional, default: smtp.gmail.com)
- SMTP_PORT (optional, default: 587)
- SMTP_FROM (optional, default: SMTP_USER)
 
.EXAMPLE
Send-Mail -Subject "Test" -Body "Test content" -FromName "MyApp" -To "recipient@example.com"
 
Sends a simple email to the recipient.
 
.EXAMPLE
Send-Mail -Subject "Alert" -Body "<h1>Alert</h1><p>Check the logs.</p>" -IsHtml $true
 
Sends an HTML formatted email with subject "Alert".
 
.EXAMPLE
Send-Mail -Subject "Report" -Body "Please find the report attached." -To "recipient@example.com" -Attachments @("./report.pdf", "./data.xlsx")
 
Sends an email with two file attachments.
#>

Function Send-Mail {
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$Subject,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$Body,

        [bool]$IsHtml = $False,
        [string]$FromName = $null,
        [string]$To = $null,
        [string[]]$Attachments = @()
    )

    # Load environment configuration
    Write-Debug "Call: Subject='$Subject', Body='$Body', IsHtml='$IsHtml', FromName='$FromName', To='$To'."
    Write-Debug "Environment: SMTP_SERVER='$env:SMTP_SERVER', SMTP_PORT='$env:SMTP_PORT', SMTP_FROM='$env:SMTP_FROM', SMTP_TO='$env:SMTP_TO', SMTP_USER='$env:SMTP_USER', SMTP_PASS='$(if($env:SMTP_PASS){"[SET]"}else{"[NOT SET]"})'"

    $smtpUser = $env:SMTP_USER
    $smtpPass = $env:SMTP_PASS

    if (-not $smtpUser -or -not $smtpPass) {
        Write-Error "Environment variables SMTP_USER and/or SMTP_PASS are not configured."
        return
    }

    $smtpServer = if ($env:SMTP_SERVER) { $env:SMTP_SERVER } else { "smtp.gmail.com" }
    $smtpPort = if ($env:SMTP_PORT) { $env:SMTP_PORT } else { 587 }
    $smtpFrom = if ($env:SMTP_FROM) { $env:SMTP_FROM } else { $smtpUser }
    if ([string]::IsNullOrWhiteSpace($FromName)) { $FromName = $env:COMPUTERNAME }

    # Optional To parameter - PowerShell converts null strings to "" automatically
    if ([string]::IsNullOrWhiteSpace($To)) { $To = $env:SMTP_TO }
    if ([string]::IsNullOrWhiteSpace($To)) {
        Write-Error "The 'To' parameter is required, either in the function call or via the SMTP_TO environment variable."
        return
    }

    Write-Debug "Configuration: SMTP_SERVER='$smtpServer', SMTP_PORT='$smtpPort', SMTP_FROM='$smtpFrom', SMTP_USER='$smtpUser', TO='$To'."
    Write-Verbose "Sending email to '$To' with subject '$Subject'..."

    # SMTP client configuration
    $smtpClient = New-Object Net.Mail.SmtpClient($smtpServer, $smtpPort)
    $smtpClient.EnableSsl = $true
    $smtpClient.Credentials = New-Object System.Net.NetworkCredential($smtpUser, $smtpPass)

    # Email message creation
    $mailMessage = New-Object Net.Mail.MailMessage
    $mailMessage.From = "$FromName <$smtpFrom>"
    $mailMessage.To.Add($To)
    $mailMessage.Subject = $Subject
    $mailMessage.Body = $Body
    $mailMessage.IsBodyHtml = $IsHtml
    $mailMessage.Headers.Add("X-Mailer", "ElektoMailPosh/0.2.0")

    # Validate and add attachments
    foreach ($attachmentPath in $Attachments) {
        if (-not (Test-Path -Path $attachmentPath -PathType Leaf)) {
            # Clean up resources before exit (Dispose releases already added attachments)
            $mailMessage.Dispose()
            $smtpClient.Dispose()
            Write-Error "Attachment file not found: '$attachmentPath'"
            return
        }
        $fullPath = (Resolve-Path -Path $attachmentPath).Path
        $attachment = New-Object System.Net.Mail.Attachment($fullPath)
        $mailMessage.Attachments.Add($attachment)
        Write-Verbose "Attachment added: $fullPath"
    }

    # Send email with retry and exponential backoff
    $maxRetries = 5
    $retryCount = 0
    $delay = 1

    try {
        while ($retryCount -lt $maxRetries) {
            try {
                $smtpClient.Send($mailMessage)
                Write-Verbose "Email sent successfully."
                return
            } catch {
                $retryCount++
                if ($retryCount -ge $maxRetries) {
                    Write-Error "Failed to send email after $maxRetries attempts: $_"
                    return
                }
                Write-Warning "Failed to send email: $_. Retrying in $delay seconds..."
                Start-Sleep -Seconds $delay
                $delay *= 2
            }
        }
    } finally {
        # Release resources (MailMessage.Dispose() also releases Attachments)
        $mailMessage.Dispose()
        $smtpClient.Dispose()
    }
}