|
|
|
@@ -109,3 +109,113 @@ function Load-SmtpPassword { |
|
|
|
$connection.Dispose() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function Get-MessageMimeBytes { |
|
|
|
param([System.Net.Mail.MailMessage]$Message) |
|
|
|
# Rewind inline image streams that the SMTP send already consumed. |
|
|
|
foreach ($view in $Message.AlternateViews) { |
|
|
|
$view.ContentStream.Position = 0 |
|
|
|
foreach ($resource in $view.LinkedResources) { $resource.ContentStream.Position = 0 } |
|
|
|
} |
|
|
|
$directory = Join-Path ([System.IO.Path]::GetTempPath()) ([guid]::NewGuid().ToString('N')) |
|
|
|
$null = New-Item -ItemType Directory -Path $directory |
|
|
|
$client = New-Object System.Net.Mail.SmtpClient |
|
|
|
try { |
|
|
|
$client.DeliveryMethod = [System.Net.Mail.SmtpDeliveryMethod]::SpecifiedPickupDirectory |
|
|
|
$client.PickupDirectoryLocation = $directory |
|
|
|
$client.Send($Message) |
|
|
|
$file = Get-ChildItem -LiteralPath $directory -Filter '*.eml' | Select-Object -First 1 |
|
|
|
return [System.IO.File]::ReadAllBytes($file.FullName) |
|
|
|
} |
|
|
|
finally { |
|
|
|
$client.Dispose() |
|
|
|
Remove-Item -LiteralPath $directory -Recurse -Force -ErrorAction SilentlyContinue |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function Read-ImapLine { |
|
|
|
param([System.IO.Stream]$Stream) |
|
|
|
$bytes = New-Object System.Collections.Generic.List[byte] |
|
|
|
while ($true) { |
|
|
|
$value = $Stream.ReadByte() |
|
|
|
if ($value -lt 0) { throw 'IMAP connection closed' } |
|
|
|
if ($value -eq 10) { break } |
|
|
|
if ($value -ne 13) { $bytes.Add([byte]$value) } |
|
|
|
} |
|
|
|
return [System.Text.Encoding]::ASCII.GetString($bytes.ToArray()) |
|
|
|
} |
|
|
|
|
|
|
|
function Read-ImapResponse { |
|
|
|
param([System.IO.Stream]$Stream, [string]$Tag) |
|
|
|
while ($true) { |
|
|
|
$line = Read-ImapLine $Stream |
|
|
|
if ($line.StartsWith("$Tag ")) { |
|
|
|
if ($line -notmatch "^$Tag OK") { throw "IMAP command failed: $line" } |
|
|
|
return |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function ConvertTo-ImapQuoted { |
|
|
|
param([string]$Value) |
|
|
|
if ($Value -match "[\r\n]") { throw 'Invalid IMAP value' } |
|
|
|
return '"' + $Value.Replace('\', '\').Replace('"', '\"') + '"' |
|
|
|
} |
|
|
|
|
|
|
|
# Appends a copy of an already-sent message to the Sent folder over IMAP. |
|
|
|
# Does nothing unless IMAP_HOST is configured. Callers treat failures as |
|
|
|
# non-fatal because the message itself has already been accepted by SMTP. |
|
|
|
function Save-SentCopy { |
|
|
|
param($Settings, [System.Security.SecureString]$Password, [System.Net.Mail.MailMessage]$Message) |
|
|
|
$imapHost = [string]$Settings['IMAP_HOST'] |
|
|
|
if (-not $imapHost) { return $false } |
|
|
|
$port = 993 |
|
|
|
if ($Settings['IMAP_PORT'] -and -not [int]::TryParse($Settings['IMAP_PORT'], [ref]$port)) { |
|
|
|
throw 'Invalid IMAP_PORT' |
|
|
|
} |
|
|
|
$folder = if ($Settings['IMAP_SENT_FOLDER']) { $Settings['IMAP_SENT_FOLDER'] } else { 'Sent' } |
|
|
|
$mime = Get-MessageMimeBytes $Message |
|
|
|
$tcp = New-Object System.Net.Sockets.TcpClient |
|
|
|
$plain = $null |
|
|
|
try { |
|
|
|
$tcp.ReceiveTimeout = 20000 |
|
|
|
$tcp.SendTimeout = 20000 |
|
|
|
$tcp.Connect($imapHost, $port) |
|
|
|
$ssl = New-Object System.Net.Security.SslStream($tcp.GetStream(), $false) |
|
|
|
$ssl.ReadTimeout = 20000 |
|
|
|
$ssl.WriteTimeout = 20000 |
|
|
|
$ssl.AuthenticateAsClient($imapHost) |
|
|
|
$send = { |
|
|
|
param([string]$Text) |
|
|
|
$data = [System.Text.Encoding]::UTF8.GetBytes($Text + "`r`n") |
|
|
|
$ssl.Write($data, 0, $data.Length) |
|
|
|
$ssl.Flush() |
|
|
|
} |
|
|
|
$null = Read-ImapLine $ssl |
|
|
|
$plain = (New-Object System.Net.NetworkCredential('', $Password)).Password |
|
|
|
& $send ('a1 LOGIN ' + (ConvertTo-ImapQuoted $Settings['SMTP_USER']) + ' ' + (ConvertTo-ImapQuoted $plain)) |
|
|
|
Read-ImapResponse $ssl 'a1' |
|
|
|
& $send ('a2 APPEND ' + (ConvertTo-ImapQuoted $folder) + ' (\Seen) {' + $mime.Length + '}') |
|
|
|
$line = Read-ImapLine $ssl |
|
|
|
if (-not $line.StartsWith('+')) { throw "IMAP APPEND rejected: $line" } |
|
|
|
$ssl.Write($mime, 0, $mime.Length) |
|
|
|
$ssl.Write([byte[]](13, 10), 0, 2) |
|
|
|
$ssl.Flush() |
|
|
|
Read-ImapResponse $ssl 'a2' |
|
|
|
& $send 'a3 LOGOUT' |
|
|
|
return $true |
|
|
|
} |
|
|
|
finally { |
|
|
|
$plain = $null |
|
|
|
$tcp.Close() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function Save-SentCopySafely { |
|
|
|
param($Settings, $Password, $Message) |
|
|
|
try { |
|
|
|
if (Save-SentCopy $Settings $Password $Message) { return 'Copy saved to Sent folder.' } |
|
|
|
return $null |
|
|
|
} |
|
|
|
catch { return "Warning: could not save a Sent copy ($($_.Exception.Message))." } |
|
|
|
} |