Преглед на файлове

Save a copy of live-sent mail to the IMAP Sent folder

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
master
Daniel Covington преди 1 ден
родител
ревизия
88b998b7ad
променени са 6 файла, в които са добавени 120 реда и са изтрити 0 реда
  1. +6
    -0
      config.example.ini
  2. +1
    -0
      tools/common.vbs
  3. +1
    -0
      tools/dispatch-once.ps1
  4. +110
    -0
      tools/mail-common.ps1
  5. +1
    -0
      tools/send-approved-campaign-test.ps1
  6. +1
    -0
      tools/send-campaign-test.ps1

+ 6
- 0
config.example.ini Целия файл

@@ -10,6 +10,9 @@ SMTP_FROM=danielcovington@postcardmojo.com
SMTP_REPLY_TO=danielcovington@postcardmojo.com
SMTP_TEST_TO=
SMTP_SECRET_ID=primary_smtp
IMAP_HOST=
IMAP_PORT=993
IMAP_SENT_FOLDER=Sent
DB_PATH=data/MassMailer.mdb
DB_PROVIDER=Microsoft.ACE.OLEDB.12.0
DB_BITNESS=x86
@@ -29,6 +32,9 @@ BACKUP_DIR=backups
# QUEUE_INTERVAL is the minimum number of seconds between attempts across
# all campaigns using this SMTP account. TLS=on plus STARTTLS=on means the
# sender must upgrade to TLS and fail closed if the server cannot do so.
# IMAP_HOST enables saving a copy of each live-sent message to the Sent folder
# over implicit-TLS IMAP, using SMTP_USER and the stored SMTP password. Leave it
# empty to disable. A failed save is reported but never affects delivery status.
# SMTP_REPLY_TO is the monitored mailbox confirmed by the owner.
# Set SMTP_TEST_TO to an owner-controlled address before the internal transport test.
# DB_PROVIDER and DB_BITNESS are checked before the HTA opens. On this PC,


+ 1
- 0
tools/common.vbs Целия файл

@@ -63,6 +63,7 @@ Function IsAllowedKey(key)
IsAllowedKey = False
For Each name In Array("SMTP_HOST", "SMTP_PORT", "SMTP_USER", "SMTP_TLS", _
"SMTP_STARTTLS", "SMTP_FROM", "SMTP_REPLY_TO", "SMTP_TEST_TO", "SMTP_SECRET_ID", _
"IMAP_HOST", "IMAP_PORT", "IMAP_SENT_FOLDER", _
"DB_PATH", "DB_PROVIDER", "DB_BITNESS", "QUEUE_INTERVAL", _
"SEND_MODE", "DISPLAY_TIME_ZONE", "TEMPLATE_ROOT", _
"TEMPLATE_IMAGE_MODE", "TEMPLATE_IMAGE_TYPES", _


+ 1
- 0
tools/dispatch-once.ps1 Целия файл

@@ -163,6 +163,7 @@ try {
Complete-DeliveryAttempt $db $reservedDelivery $reservedAttempt 'accepted' 'accepted'
Complete-FinishedCampaigns $db
Write-Output "SMTP accepted delivery $reservedDelivery. Next slot at $($next.ToString('o'))."
if ($Send) { $sentNote = Save-SentCopySafely $settings $password $message; if ($sentNote) { Write-Output $sentNote } }
}
catch {
$code = if ($_.Exception -is [System.Net.Mail.SmtpException]) {


+ 110
- 0
tools/mail-common.ps1 Целия файл

@@ -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))." }
}

+ 1
- 0
tools/send-approved-campaign-test.ps1 Целия файл

@@ -63,6 +63,7 @@ try {
$client.Send($message)
Write-CampaignAudit $db 'approved_campaign_test_accepted' ([string]$CampaignId) ("Internal test to $testTo")
Write-Output "Approved campaign $CampaignId test accepted for $testTo. Live delivery counts did not advance."
if ($Send) { $sentNote = Save-SentCopySafely $settings $password $message; if ($sentNote) { Write-Output $sentNote } }
}
finally {
if ($client) { $client.Dispose() }


+ 1
- 0
tools/send-campaign-test.ps1 Целия файл

@@ -58,6 +58,7 @@ try {
$client.Send($message)
Write-CampaignAudit $db 'campaign_test_accepted' $review.Token ("Internal test to $($review.Identity.TestTo)")
Write-Output "Campaign test accepted for $($review.Identity.TestTo). Check the received message before approval."
if ($Send) { $sentNote = Save-SentCopySafely $settings $password $message; if ($sentNote) { Write-Output $sentNote } }
}
finally {
if ($client) { $client.Dispose() }


Loading…
Отказ
Запис

Powered by TurnKey Linux.