Pārlūkot izejas kodu

Add hidden tool runner, helper tests, and HTA/README updates

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
master
Daniel Covington pirms 1 dienas
vecāks
revīzija
9ea2920169
5 mainītis faili ar 239 papildinājumiem un 50 dzēšanām
  1. +96
    -50
      MassMailer.hta
  2. +1
    -0
      README.md
  3. +46
    -0
      tools/run-hidden-tool.ps1
  4. +41
    -0
      tools/test-hidden-helper.ps1
  5. +55
    -0
      tools/test-hidden-hta-isolated.ps1

+ 96
- 50
MassMailer.hta Parādīt failu

@@ -274,6 +274,60 @@
return root + "\\" + folder + "\\WindowsPowerShell\\v1.0\\powershell.exe";
}
function quoted(value) { return '"' + value + '"'; }
function writeJsonFile(path, value) {
var stream = new ActiveXObject("ADODB.Stream");
try {
stream.Type = 2;
stream.Charset = "utf-8";
stream.Open();
stream.WriteText(JSON.stringify(value));
stream.SaveToFile(path, 2);
} finally { if (stream.State === 1) stream.Close(); }
}
function readJsonFile(path) {
var stream = new ActiveXObject("ADODB.Stream");
try {
stream.Type = 2;
stream.Charset = "utf-8";
stream.Open();
stream.LoadFromFile(path);
return JSON.parse(stream.ReadText().replace(/^\uFEFF/, ""));
} finally { if (stream.State === 1) stream.Close(); }
}
function finishHiddenTool(job) {
var fs = new ActiveXObject("Scripting.FileSystemObject");
var result;
try {
if (!fs.FileExists(job.resultPath)) throw new Error("Helper exited without a result");
result = readJsonFile(job.resultPath);
} finally {
if (fs.FileExists(job.requestPath)) fs.DeleteFile(job.requestPath);
if (fs.FileExists(job.resultPath)) fs.DeleteFile(job.resultPath);
}
if (!result.Success) throw new Error(result.Error || "Helper failed");
return result.Output || "";
}
function startHiddenTool(scriptName, parameters, wait) {
var fs = new ActiveXObject("Scripting.FileSystemObject");
var folder = fs.BuildPath(projectRoot(), "temp");
if (!fs.FolderExists(folder)) fs.CreateFolder(folder);
var requestPath = fs.BuildPath(folder, fs.GetTempName() + ".request.json");
var job = { requestPath: requestPath, resultPath: requestPath + ".result.json",
startedAt: new Date().getTime(), warned: false };
var bridge = fs.BuildPath(projectRoot(), "tools\\run-hidden-tool.ps1");
var command = quoted(powerShellHost()) + " -NoProfile -ExecutionPolicy Bypass -File " +
quoted(bridge) + " -RequestPath " + quoted(job.requestPath) +
" -ResultPath " + quoted(job.resultPath);
writeJsonFile(job.requestPath, { ScriptName: scriptName,
ConfigPath: fs.BuildPath(projectRoot(), "config.ini"), Parameters: parameters || {} });
try {
new ActiveXObject("WScript.Shell").Run(command, 0, wait);
return wait ? finishHiddenTool(job) : job;
} catch (error) {
if (fs.FileExists(job.requestPath)) fs.DeleteFile(job.requestPath);
throw error;
}
}
function configurePassword() {
try {
var fs = new ActiveXObject("Scripting.FileSystemObject");
@@ -289,18 +343,11 @@
function sendInternalTest() {
try {
var fs = new ActiveXObject("Scripting.FileSystemObject");
var root = projectRoot();
var settings = readSettings(fs.BuildPath(root, "config.ini"));
var settings = readSettings(fs.BuildPath(projectRoot(), "config.ini"));
if (!settings.SMTP_TEST_TO) throw new Error("Set SMTP_TEST_TO in config.ini first");
if (!window.confirm("Send one internal SMTP test to " + settings.SMTP_TEST_TO + "?")) return;
var script = fs.BuildPath(root, "tools\\test-smtp-message.ps1");
var command = quoted(powerShellHost()) + " -NoProfile -ExecutionPolicy Bypass -File " +
quoted(script) + " -Send";
var process = new ActiveXObject("WScript.Shell").Exec(command);
var output = process.StdOut.ReadAll();
var errors = process.StdErr.ReadAll();
document.getElementById("transportMessage").innerText =
(process.ExitCode === 0 ? "" : "Test failed. ") + output + errors;
runTemplateTool("test-smtp-message.ps1", { Send: true });
} catch (error) {
document.getElementById("transportMessage").innerText = "Test failed: " + error.message;
}
@@ -336,23 +383,14 @@
if (db !== null && db.State === 1) db.Close();
}
}
function runTemplateTool(scriptName, args) {
var fs = new ActiveXObject("Scripting.FileSystemObject");
var script = fs.BuildPath(projectRoot(), "tools\\" + scriptName);
var configPath = fs.BuildPath(projectRoot(), "config.ini");
var command = quoted(powerShellHost()) + " -NoProfile -ExecutionPolicy Bypass -File " +
quoted(script) + " -ConfigPath " + quoted(configPath) + " " + args;
var process = new ActiveXObject("WScript.Shell").Exec(command);
var output = process.StdOut.ReadAll();
var errors = process.StdErr.ReadAll();
if (process.ExitCode !== 0) throw new Error((output + errors).replace(/^\s+|\s+$/g, ""));
return output.replace(/^\s+|\s+$/g, "");
function runTemplateTool(scriptName, parameters) {
return startHiddenTool(scriptName, parameters, true);
}
function importTemplate() {
try {
var path = document.getElementById("templatePath").value;
if (!path || path.indexOf('"') !== -1) throw new Error("Select a template file");
var result = runTemplateTool("import-template.ps1", "-Path " + quoted(path));
var result = runTemplateTool("import-template.ps1", { Path: path });
document.getElementById("templateMessage").innerText = result;
refreshTemplates();
refreshStatus();
@@ -364,7 +402,7 @@
try {
var id = document.getElementById("templateVersion").value;
if (!/^[1-9][0-9]*$/.test(id)) throw new Error("Select an imported template");
var file = runTemplateTool("preview-template.ps1", "-TemplateId " + id);
var file = runTemplateTool("preview-template.ps1", { TemplateId: Number(id) });
document.getElementById("templatePreview").src = file;
document.getElementById("templateMessage").innerText =
"Showing the imported snapshot. This preview does not include the campaign footer yet.";
@@ -441,7 +479,7 @@
TestReceived: document.getElementById("confirmReceived").checked
};
}
function withCampaignRequest(scriptName, extraArgs) {
function withCampaignRequest(scriptName, extraParameters) {
var fs = new ActiveXObject("Scripting.FileSystemObject");
var folder = fs.BuildPath(projectRoot(), "temp");
if (!fs.FolderExists(folder)) fs.CreateFolder(folder);
@@ -454,7 +492,9 @@
stream.WriteText(JSON.stringify(campaignRequest()));
stream.SaveToFile(path, 2);
stream.Close();
return runTemplateTool(scriptName, "-RequestPath " + quoted(path) + (extraArgs || ""));
var parameters = extraParameters || {};
parameters.RequestPath = path;
return runTemplateTool(scriptName, parameters);
} finally {
if (stream.State === 1) stream.Close();
if (fs.FileExists(path)) fs.DeleteFile(path);
@@ -463,7 +503,7 @@
function reviewCampaign() {
invalidateCampaignReview();
try {
var data = JSON.parse(withCampaignRequest("review-campaign.ps1", ""));
var data = JSON.parse(withCampaignRequest("review-campaign.ps1"));
pendingCampaignReview = data;
document.getElementById("campaignReview").innerText =
data.Count + " eligible recipients. From: " + data.From + ". Reply-to: " +
@@ -482,7 +522,7 @@
if (!pendingCampaignReview) return;
if (!window.confirm("Send this campaign test to " + pendingCampaignReview.TestTo + "?")) return;
try {
var result = withCampaignRequest("send-campaign-test.ps1", " -Send");
var result = withCampaignRequest("send-campaign-test.ps1", { Send: true });
campaignTestAccepted = true;
document.getElementById("confirmReceived").disabled = false;
document.getElementById("campaignMessage").innerText = result;
@@ -501,7 +541,7 @@
if (!window.confirm("Approve " + pendingCampaignReview.Count +
" recipients? The campaign will remain paused.")) return;
try {
var result = withCampaignRequest("approve-campaign.ps1", "");
var result = withCampaignRequest("approve-campaign.ps1");
document.getElementById("campaignMessage").innerText = result;
invalidateCampaignReview();
refreshStatus();
@@ -566,7 +606,7 @@
var folder = fs.BuildPath(projectRoot(), "temp");
if (!fs.FolderExists(folder)) fs.CreateFolder(folder);
document.getElementById("dispatchMessage").innerText =
runTemplateTool("dispatch-control.ps1", "-Action Recover");
runTemplateTool("dispatch-control.ps1", { Action: "Recover" });
dispatchSessionPid = htaProcessId();
dispatchSessionToken = "";
for (var i = 0; i < 32; i++)
@@ -596,13 +636,13 @@
var fs = new ActiveXObject("Scripting.FileSystemObject");
if (dispatchSessionPath && fs.FileExists(dispatchSessionPath))
fs.DeleteFile(dispatchSessionPath);
runTemplateTool("dispatch-control.ps1", "-Action PauseAll");
runTemplateTool("dispatch-control.ps1", { Action: "PauseAll" });
} catch (ignored) { }
}
function refreshDispatchStatus() {
if (!dispatchReady || (dispatchProcess && dispatchProcess.Status === 0)) return;
if (!dispatchReady || dispatchProcess) return;
try {
var status = JSON.parse(runTemplateTool("dispatch-control.ps1", "-Action Status"));
var status = JSON.parse(runTemplateTool("dispatch-control.ps1", { Action: "Status" }));
document.getElementById("sendState").innerText = status.Paused ?
"Paused" : "Running " + status.RunningCampaigns + " campaign(s)";
document.getElementById("dispatchSummary").innerText =
@@ -619,14 +659,25 @@
if (!dispatchReady) return;
try {
writeHeartbeat();
var fs = new ActiveXObject("Scripting.FileSystemObject");
if (dispatchProcess) {
if (dispatchProcess.Status === 0) return;
var output = dispatchProcess.StdOut.ReadAll();
var errors = dispatchProcess.StdErr.ReadAll();
var exitCode = dispatchProcess.ExitCode;
if (!fs.FileExists(dispatchProcess.resultPath)) {
if (!dispatchProcess.warned &&
new Date().getTime() - dispatchProcess.startedAt > 60000) {
dispatchProcess.warned = true;
document.getElementById("dispatchMessage").innerText =
"Sending helper has not returned. No further attempts will start until it finishes; pause and review the campaign.";
}
return;
}
var finished = dispatchProcess;
dispatchProcess = null;
document.getElementById("dispatchMessage").innerText =
(exitCode === 0 ? "" : "Dispatch error: ") + output + errors;
try {
document.getElementById("dispatchMessage").innerText = finishHiddenTool(finished);
} catch (error) {
document.getElementById("dispatchMessage").innerText =
"Dispatch error: " + error.message;
}
refreshStatus();
refreshCampaigns();
refreshDispatchStatus();
@@ -640,13 +691,8 @@
rows.Close();
} finally { db.Close(); }
if (!running) return;
var fs = new ActiveXObject("Scripting.FileSystemObject");
var script = fs.BuildPath(projectRoot(), "tools\\dispatch-once.ps1");
var command = quoted(powerShellHost()) + " -NoProfile -ExecutionPolicy Bypass -File " +
quoted(script) + " -ConfigPath " + quoted(fs.BuildPath(projectRoot(), "config.ini")) +
" -Send -SessionPid " + dispatchSessionPid +
" -SessionToken " + dispatchSessionToken;
dispatchProcess = new ActiveXObject("WScript.Shell").Exec(command);
dispatchProcess = startHiddenTool("dispatch-once.ps1", { Send: true,
SessionPid: dispatchSessionPid, SessionToken: dispatchSessionToken }, false);
} catch (error) {
document.getElementById("dispatchMessage").innerText =
"Dispatch tick failed: " + error.message;
@@ -654,10 +700,10 @@
}
function dispatchControl(action, campaignId, deliveryId) {
try {
var args = "-Action " + action;
if (campaignId) args += " -CampaignId " + campaignId;
if (deliveryId) args += " -DeliveryId " + deliveryId;
var result = runTemplateTool("dispatch-control.ps1", args);
var parameters = { Action: action };
if (campaignId) parameters.CampaignId = Number(campaignId);
if (deliveryId) parameters.DeliveryId = Number(deliveryId);
var result = runTemplateTool("dispatch-control.ps1", parameters);
document.getElementById("dispatchMessage").innerText = result;
refreshCampaigns();
refreshUncertain();
@@ -693,7 +739,7 @@
if (!window.confirm("Send a test of approved campaign #" + id +
" to " + settings.SMTP_TEST_TO + " only?")) return;
document.getElementById("dispatchMessage").innerText =
runTemplateTool("send-approved-campaign-test.ps1", "-CampaignId " + id + " -Send");
runTemplateTool("send-approved-campaign-test.ps1", { CampaignId: Number(id), Send: true });
} catch (error) {
document.getElementById("dispatchMessage").innerText =
"Approved campaign test failed: " + error.message;
@@ -750,7 +796,7 @@
var id = document.getElementById("reportCampaign").value;
if (!/^[1-9][0-9]*$/.test(id)) throw new Error("Select a campaign to report");
document.getElementById("reportMessage").innerText =
runTemplateTool("export-report.ps1", "-CampaignId " + id);
runTemplateTool("export-report.ps1", { CampaignId: Number(id) });
} catch (error) {
document.getElementById("reportMessage").innerText =
"Report export failed: " + error.message;


+ 1
- 0
README.md Parādīt failu

@@ -14,6 +14,7 @@ Windows HTA application for individual marketing emails for a 9 x 12 postcard bu
- Uses tabs for Overview, SMTP, Contacts, Templates, Campaigns, Sending, and Reports. Contacts includes import and global suppression.
- Sends approved campaigns only while the HTA is open, with a configurable account-wide gap between attempts. It pauses on restart and uncertain outcomes, rechecks suppression before each send, and requires a fresh mailbox opt-out review before resuming.
- Sends a test of an approved campaign to the internal inbox without advancing delivery counts, and exports delivery and attempt CSV reports to `REPORT_DIR`.
- Runs routine PowerShell helpers without opening console windows. The password setup action opens a console so the password can be entered privately.

Live marketing dispatch has passed disposable-database pickup tests. It still needs an owner-run internal pilot and inbox review before use with business contacts. Closing the HTA stops new send attempts; an SMTP attempt already in progress may finish.



+ 46
- 0
tools/run-hidden-tool.ps1 Parādīt failu

@@ -0,0 +1,46 @@
param(
[Parameter(Mandatory = $true)][string]$RequestPath,
[Parameter(Mandatory = $true)][string]$ResultPath
)
$ErrorActionPreference = 'Stop'
$result = @{ Success = $false; Output = ''; Error = '' }
try {
$projectRoot = Split-Path -Parent $PSScriptRoot
$tempRoot = [System.IO.Path]::GetFullPath((Join-Path $projectRoot 'temp'))
foreach ($path in @($RequestPath, $ResultPath)) {
$resolved = [System.IO.Path]::GetFullPath($path)
if (-not $resolved.StartsWith($tempRoot + [System.IO.Path]::DirectorySeparatorChar,
[StringComparison]::OrdinalIgnoreCase)) {
throw 'Hidden helper files must be under the project temp folder'
}
}
$request = Get-Content -LiteralPath $RequestPath -Raw -Encoding UTF8 | ConvertFrom-Json
$allowed = @('test-smtp-message.ps1', 'import-template.ps1', 'preview-template.ps1',
'review-campaign.ps1', 'send-campaign-test.ps1', 'approve-campaign.ps1',
'dispatch-control.ps1', 'dispatch-once.ps1', 'send-approved-campaign-test.ps1',
'export-report.ps1')
if ($request.ScriptName -notin $allowed) { throw 'Unsupported hidden helper script' }
$scriptPath = Join-Path $PSScriptRoot $request.ScriptName
if (-not (Test-Path -LiteralPath $scriptPath -PathType Leaf)) {
throw 'Hidden helper script is missing'
}
$parameters = @{ ConfigPath = [string]$request.ConfigPath }
foreach ($property in $request.Parameters.PSObject.Properties) {
if ($property.Name -notmatch '^[A-Za-z][A-Za-z0-9]*$' -or
$property.Name -eq 'ConfigPath') { throw 'Invalid hidden helper parameter' }
$parameters[$property.Name] = $property.Value
}
$result.Output = ((& $scriptPath @parameters | Out-String).Trim())
$result.Success = $true
}
catch {
$result.Error = $_.Exception.Message
}
finally {
$json = ConvertTo-Json -InputObject $result -Compress -Depth 4
$temporaryPath = $ResultPath + '.writing'
[System.IO.File]::WriteAllText($temporaryPath, $json,
(New-Object System.Text.UTF8Encoding($false)))
[System.IO.File]::Move($temporaryPath, $ResultPath)
}
if (-not $result.Success) { exit 1 }

+ 41
- 0
tools/test-hidden-helper.ps1 Parādīt failu

@@ -0,0 +1,41 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$tempFolder = Join-Path $root 'temp'
$null = New-Item -ItemType Directory -Path $tempFolder -Force
$powerShellX86 = Join-Path $env:SystemRoot 'SysWOW64\WindowsPowerShell\v1.0\powershell.exe'
$bridge = Join-Path $PSScriptRoot 'run-hidden-tool.ps1'
$shell = New-Object -ComObject WScript.Shell
foreach ($wait in @($true, $false)) {
$token = [guid]::NewGuid().ToString('N')
$requestPath = Join-Path $tempFolder ($token + '.request.json')
$resultPath = Join-Path $tempFolder ($token + '.result.json')
try {
@{ ScriptName = 'dispatch-control.ps1'; ConfigPath = (Join-Path $root 'config.ini');
Parameters = @{ Action = 'Status' } } |
ConvertTo-Json -Compress -Depth 4 |
Set-Content -LiteralPath $requestPath -Encoding UTF8
$command = '"' + $powerShellX86 + '" -NoProfile -ExecutionPolicy Bypass -File "' +
$bridge + '" -RequestPath "' + $requestPath + '" -ResultPath "' + $resultPath + '"'
$exitCode = $shell.Run($command, 0, $wait)
if (-not $wait) {
for ($attempt = 0; $attempt -lt 160 -and
-not (Test-Path -LiteralPath $resultPath -PathType Leaf); $attempt++) {
Start-Sleep -Milliseconds 250
}
}
if (-not (Test-Path -LiteralPath $resultPath -PathType Leaf)) {
throw 'Hidden runner produced no result'
}
$response = Get-Content -LiteralPath $resultPath -Raw -Encoding UTF8 | ConvertFrom-Json
if (($wait -and $exitCode -ne 0) -or -not $response.Success) {
throw ('Hidden runner failed: ' + $response.Error)
}
$status = $response.Output | ConvertFrom-Json
if ($null -eq $status.Paused) { throw 'Hidden runner status response is incomplete' }
}
finally {
if (Test-Path -LiteralPath $requestPath) { Remove-Item -LiteralPath $requestPath -Force }
if (Test-Path -LiteralPath $resultPath) { Remove-Item -LiteralPath $resultPath -Force }
}
}
Write-Output 'Synchronous and asynchronous hidden helpers returned dispatch status'

+ 55
- 0
tools/test-hidden-hta-isolated.ps1 Parādīt failu

@@ -0,0 +1,55 @@
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$tempRoot = [System.IO.Path]::GetFullPath((Join-Path $root 'temp'))
$work = Join-Path $tempRoot ('hta-hidden-' + [guid]::NewGuid().ToString('N'))
$hta = Join-Path $work 'MassMailer.hta'
$started = $null
try {
$null = New-Item -ItemType Directory -Path $work -Force
Copy-Item -LiteralPath (Join-Path $root 'tools') -Destination (Join-Path $work 'tools') -Recurse
Copy-Item -LiteralPath (Join-Path $root 'src') -Destination (Join-Path $work 'src') -Recurse
$null = New-Item -ItemType Directory -Path (Join-Path $work 'templates') -Force
$app = Get-Content -LiteralPath (Join-Path $root 'MassMailer.hta') -Raw
$app.Replace('APPLICATIONNAME="MassMailer"', 'APPLICATIONNAME="MassMailerSmoke"') |
Set-Content -LiteralPath $hta -Encoding UTF8
$dbPath = Join-Path $work 'data\test.mdb'
$null = New-Item -ItemType Directory -Path (Split-Path -Parent $dbPath) -Force
$settings = Get-Content -LiteralPath (Join-Path $root 'config.example.ini') -Raw
$settings.Replace('DB_PATH=data/MassMailer.mdb', ('DB_PATH=' + $dbPath)) |
Set-Content -LiteralPath (Join-Path $work 'config.ini') -Encoding UTF8
$cscript = Join-Path $env:SystemRoot 'SysWOW64\cscript.exe'
& $cscript //nologo (Join-Path $work 'tools\init-db.vbs') (Join-Path $work 'config.ini')
if ($LASTEXITCODE -ne 0) { throw 'Isolated MDB initialization failed' }
$mshta = Join-Path $env:SystemRoot 'SysWOW64\mshta.exe'
$started = Start-Process -FilePath $mshta -ArgumentList ('"' + $hta + '"') -PassThru -WindowStyle Hidden
$session = $null
for ($wait = 0; $wait -lt 30; $wait++) {
Start-Sleep -Seconds 1
$matches = @(Get-CimInstance Win32_Process -Filter "Name='mshta.exe'" |
Where-Object { $_.CommandLine -and $_.CommandLine.IndexOf($hta,
[StringComparison]::OrdinalIgnoreCase) -ge 0 })
if ($matches.Count -eq 1) {
$session = Join-Path $work ('temp\session-' + $matches[0].ProcessId + '.txt')
if (Test-Path -LiteralPath $session -PathType Leaf) { break }
}
}
if ($matches.Count -ne 1 -or -not (Test-Path -LiteralPath $session -PathType Leaf)) {
throw 'Isolated HTA did not initialize its hidden dispatcher session'
}
Start-Sleep -Seconds 3
Write-Output 'Isolated HTA startup passed while the main app remained open'
}
finally {
$matches = @(Get-CimInstance Win32_Process -Filter "Name='mshta.exe'" |
Where-Object { $_.CommandLine -and $_.CommandLine.IndexOf($hta,
[StringComparison]::OrdinalIgnoreCase) -ge 0 })
foreach ($process in $matches) {
Stop-Process -Id $process.ProcessId -Force -ErrorAction SilentlyContinue
}
$resolvedWork = [System.IO.Path]::GetFullPath($work)
if (-not $resolvedWork.StartsWith($tempRoot + [System.IO.Path]::DirectorySeparatorChar,
[StringComparison]::OrdinalIgnoreCase)) { throw 'Unsafe isolated test cleanup path' }
if (Test-Path -LiteralPath $resolvedWork) {
Remove-Item -LiteralPath $resolvedWork -Recurse -Force
}
}

Notiek ielāde…
Atcelt
Saglabāt

Powered by TurnKey Linux.