JT.SendMail.psm1

# Module-scope flag to avoid loading assemblies more than once
$script:MailKitAssembliesLoaded = $false

function Import-MailKitAssemblies {
    [CmdletBinding()]
    param()

    if ($script:MailKitAssembliesLoaded) {
        return
    }

    $libPath = Join-Path $PSScriptRoot 'lib'

    $mimeKitDll = Join-Path $libPath 'MimeKit.dll'
    $mailKitDll = Join-Path $libPath 'MailKit.dll'

    if (-not (Test-Path $mimeKitDll)) {
        throw "MimeKit.dll not found: $mimeKitDll"
    }

    if (-not (Test-Path $mailKitDll)) {
        throw "MailKit.dll not found: $mailKitDll"
    }

    # Optional dependency DLLs
    $optionalDeps = @(
        'BouncyCastle.Cryptography.dll',
        'BouncyCastle.Crypto.dll',
        'System.Buffers.dll',
        'System.Memory.dll',
        'System.Runtime.CompilerServices.Unsafe.dll',
        'System.Numerics.Vectors.dll',
        'System.Formats.Asn1.dll'
    )

    foreach ($dep in $optionalDeps) {
        $depPath = Join-Path $libPath $dep
        if (Test-Path $depPath) {
            [void][System.Reflection.Assembly]::LoadFrom($depPath)
        }
    }

    # Load MimeKit first, then MailKit
    [void][System.Reflection.Assembly]::LoadFrom($mimeKitDll)
    [void][System.Reflection.Assembly]::LoadFrom($mailKitDll)

    $script:MailKitAssembliesLoaded = $true
}

function Convert-HtmlToPlainText {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Html
    )

    $text = $Html -replace '<style[\s\S]*?</style>', ' '
    $text = $text -replace '<script[\s\S]*?</script>', ' '
    $text = $text -replace '<[^>]+>', ' '
    $text = $text -replace '&nbsp;', ' '
    $text = $text -replace '&amp;', '&'
    $text = $text -replace '&lt;', '<'
    $text = $text -replace '&gt;', '>'
    $text = $text -replace '\s+', ' '
    return $text.Trim()
}

function Add-AddressesToCollection {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [object]$Collection,

        [Parameter(Mandatory = $true)]
        [string[]]$Addresses
    )

    foreach ($addr in $Addresses) {
        if ([string]::IsNullOrWhiteSpace($addr)) {
            continue
        }

        $Collection.Add([MimeKit.MailboxAddress]::Parse($addr))
    }
}

function Set-MimeMessagePriority {
    param (
        [Parameter(Mandatory = $true)]
        [MimeKit.MimeMessage]$Message,
        
        [Parameter(Mandatory = $true)]
        [ValidateSet('High', 'Normal', 'Low')]
        [string]$PriorityLevel
    )

    switch ($PriorityLevel) {
        'High' {
            $Message.Importance = [MimeKit.MessageImportance]::High
            $Message.Priority   = [MimeKit.MessagePriority]::Urgent
            $Message.XPriority  = [MimeKit.XMessagePriority]::High
        }
        'Low' {
            $Message.Importance = [MimeKit.MessageImportance]::Low
            $Message.Priority   = [MimeKit.MessagePriority]::NonUrgent
            $Message.XPriority  = [MimeKit.XMessagePriority]::Low
        }
        'Normal' {
            $Message.Importance = [MimeKit.MessageImportance]::Normal
            $Message.Priority   = [MimeKit.MessagePriority]::Normal
            $Message.XPriority  = [MimeKit.XMessagePriority]::Normal
        }
    }
}

function Send-MailKitMessage {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$From,

        [Parameter(Mandatory = $true)]
        [string[]]$To,

        [Parameter(Mandatory = $false)]
        [string[]]$Cc,

        [Parameter(Mandatory = $false)]
        [string[]]$Bcc,

        [Parameter(Mandatory = $false)]
        [string[]]$ReplyTo,

        [Parameter(Mandatory = $true)]
        [string]$Subject,

        [Parameter(Mandatory = $false)]
        [string]$Body,

        [Parameter(Mandatory = $false)]
        [switch]$BodyAsHtml,

        [Parameter(Mandatory = $false)]
        [string]$TextBody,

        [Parameter(Mandatory = $false)]
        [string]$HtmlBody,

        [Parameter(Mandatory = $false)]
        [string[]]$Attachments,

        [Parameter(Mandatory = $true)]
        [string]$SmtpServer,

        [Parameter(Mandatory = $false)]
        [int]$Port = 25,

        [Parameter(Mandatory = $false)]
        [ValidateSet('None','Auto','SslOnConnect','StartTls','StartTlsWhenAvailable')]
        [string]$SecureSocketOption = 'Auto',

        [Parameter(Mandatory = $false)]
        [pscredential]$Credential,

        [Parameter(Mandatory = $false)]
        [switch]$SkipCertificateValidation,

        [Parameter(Mandatory = $false)]
        [ValidateSet('High', 'Normal', 'Low')]
        [string]$PriorityLevel = 'Normal'
    )

    Import-MailKitAssemblies

    $message = [MimeKit.MimeMessage]::new()

    # From
    $message.From.Add([MimeKit.MailboxAddress]::Parse($From))

    # Recipients
    Add-AddressesToCollection -Collection $message.To -Addresses $To

    if ($Cc) {
        Add-AddressesToCollection -Collection $message.Cc -Addresses $Cc
    }

    if ($Bcc) {
        Add-AddressesToCollection -Collection $message.Bcc -Addresses $Bcc
    }

    if ($ReplyTo) {
        Add-AddressesToCollection -Collection $message.ReplyTo -Addresses $ReplyTo
    }

    Set-MimeMessagePriority -Message $message -PriorityLevel $PriorityLevel

    $message.Subject = $Subject

    $builder = [MimeKit.BodyBuilder]::new()

    # Body handling priority:
    # 1. Explicit TextBody / HtmlBody
    # 2. Fallback to Body + BodyAsHtml
    if ($PSBoundParameters.ContainsKey('TextBody')) {
        $builder.TextBody = $TextBody
    }

    if ($PSBoundParameters.ContainsKey('HtmlBody')) {
        $builder.HtmlBody = $HtmlBody

        if (-not $PSBoundParameters.ContainsKey('TextBody') -and -not [string]::IsNullOrWhiteSpace($HtmlBody)) {
            $builder.TextBody = Convert-HtmlToPlainText -Html $HtmlBody
        }
    }

    if (-not $PSBoundParameters.ContainsKey('TextBody') -and -not $PSBoundParameters.ContainsKey('HtmlBody')) {
        if ($BodyAsHtml) {
            $builder.HtmlBody = $Body
            $builder.TextBody = Convert-HtmlToPlainText -Html $Body
        }
        else {
            $builder.TextBody = $Body
        }
    }

    # Attachments
    if ($Attachments) {
        foreach ($file in $Attachments) {
            if (-not (Test-Path -LiteralPath $file)) {
                throw "Attachment not found: $file"
            }

            $null = $builder.Attachments.Add($file)
        }
    }

    $message.Body = $builder.ToMessageBody()

    $socketOption = [MailKit.Security.SecureSocketOptions]::$SecureSocketOption
    $client = [MailKit.Net.Smtp.SmtpClient]::new()

    try {
        if ($SkipCertificateValidation) {
            $client.ServerCertificateValidationCallback = { $true }
        }

        $client.Connect($SmtpServer, $Port, $socketOption)

        # Authenticate only if credential is provided
        if ($Credential) {
            $plainPassword = [System.Net.NetworkCredential]::new('', $Credential.Password).Password
            $client.Authenticate($Credential.UserName, $plainPassword)
        }

        $serverResponse = $client.Send($message)

        [pscustomobject]@{
            Success        = $true
            SmtpServer     = $SmtpServer
            Port           = $Port
            From           = $From
            To             = ($To -join '; ')
            Subject        = $Subject
            MessageId      = $message.MessageId
            ServerResponse = $serverResponse
        }
    }
    catch {
        throw "Failed to send email via '$($SmtpServer):$($Port)'. $($_.Exception.Message)"
    }
    finally {
        if ($client) {
            if ($client.IsConnected) {
                $client.Disconnect($true)
            }
            $client.Dispose()
        }
    }
}

Export-ModuleMember -Function Send-MailKitMessage