Skip to content

Commit 2f388fd

Browse files
Fix the defects found by adversarial review of the .NET 8 migration
- MboxParser now normalizes line endings to CRLF when writing message bytes: the server's message pipeline requires CRLF, so an LF-only (Unix) mbox previously produced stored .eml files the server could not parse. Verified end to end: an LF-only mbox imports with correct subjects, senders, dates and bodies. - ImportTool writes each message as {account}\XY\{GUID}.eml (the server's on-disk convention) so ImportMessageFromFileToIMAPFolder never relocates the file; previously a failure after the server's internal move orphaned the message at a path the cleanup could not see. Cleanup also no longer aborts the import on non-IO errors. - ucWizard.ShowPage restores the pre-validation button states instead of force-enabling all three; force-enabling let a failed validation on the first page enable Previous, and clicking it crashed with ShowPage(-1). Latent for DBSetup (its first page never fails); ImportTool's first pages validate, making it reachable. - ucMboxSelect skips mail-client index files (.msf/.dat) so a Thunderbird profile folder imports cleanly (Inbox.msf previously collided with the Inbox mbox and imported garbage). - ImportTool refuses /silent - the shared wizard would otherwise auto-run all pages with empty selections. - InstallDotNetRuntime checks the runtime bundle's exit code (0/3010 = success); Exec only reports launch failures, so a failed install previously passed silently and the DB tools then could not start. MinVersion raised to Windows 10 1607, the .NET 8 runtime's floor - on older Windows the runtime install fails and the server would be left without a database. - build-tools.ps1 cleans each publish folder before publishing; dotnet publish never removes orphans, and the installer wildcards the folder's entire contents into {app}\Bin. Findings produced by a five-lens adversarial review (12 raised, 9 confirmed after refutation passes, all fixed).
1 parent 3f9b2ca commit 2f388fd

8 files changed

Lines changed: 81 additions & 15 deletions

File tree

build/build-tools.ps1

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ foreach ($project in $publishProjects) {
1818
$csproj = Join-Path $toolsDir "$project\$project.csproj"
1919
$output = Join-Path $toolsDir "$project\publish"
2020

21+
# Start from a clean folder: the installer wildcards its entire contents,
22+
# and dotnet publish never removes files a previous publish left behind.
23+
if (Test-Path $output) { Remove-Item -Recurse -Force $output }
24+
2125
Write-Host "Publishing $project -> $output"
2226
dotnet publish $csproj -c $Configuration -o $output
2327
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

hmailserver/installation/hMailServerInnoExtension.iss

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,17 @@ begin
297297
begin
298298
MsgBox('The .NET 8 Desktop Runtime could not be installed. The database setup tools will not work until it is installed.' + #13#10 +
299299
SysErrorMessage(ResultCode), mbError, MB_OK);
300+
Exit;
301+
end;
302+
303+
// Exec returns True whenever the process launched; the actual install
304+
// result is the exit code. 0 = success, 3010 = success, reboot required.
305+
if (ResultCode <> 0) and (ResultCode <> 3010) then
306+
begin
307+
MsgBox('The .NET 8 Desktop Runtime installation failed with exit code ' + IntToStr(ResultCode) + '.' + #13#10 +
308+
'The database setup tools will not work until the runtime is installed. ' +
309+
'Install it manually from https://dotnet.microsoft.com/download/dotnet/8.0 and then run ' +
310+
'DBSetupQuick.exe from the hMailServer Bin folder.', mbError, MB_OK);
300311
end;
301312
end;
302313

hmailserver/installation/section_setup.iss

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ AllowNoIcons=yes
1212
Uninstallable=true
1313
DirExistsWarning=no
1414
CreateAppDir=true
15-
; Windows 7 SP1. Inno Setup 6 refuses anything below 6.1, and the old Vista SP1
16-
; floor was fiction anyway: this is an x64-only build whose Control Panel and
17-
; setup tools need the .NET 8 Desktop Runtime (bundled, installed when
18-
; missing) and whose binaries link the VC++ v145 runtime.
19-
MinVersion=6.1sp1
15+
; Windows 10 1607 (build 14393): the floor of the .NET 8 Desktop Runtime,
16+
; which the Control Panel and the setup tools require (bundled, installed
17+
; when missing - its installer refuses anything older, which would leave the
18+
; server without a database). The binaries link the VC++ v145 runtime.
19+
MinVersion=10.0.14393

hmailserver/source/Tools/ImportTool/MboxImport/MboxParser.cs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ namespace ImportTool.MboxImport
1414
/// the start of the file or directly after a blank line, which covers the
1515
/// classic mbox variants as well as Thunderbird's "From - <date>" form. Both
1616
/// LF and CRLF line endings are handled, the final message is emitted, and
17-
/// mboxrd-style ">From " quoting is reversed. Message bytes are otherwise
18-
/// preserved exactly; no dot-stuffing or trailing separators are added.
17+
/// mboxrd-style ">From " quoting is reversed. Line endings are normalized
18+
/// to CRLF - the server's message pipeline requires it - but message bytes
19+
/// are otherwise preserved; no dot-stuffing or trailing separators are
20+
/// added.
1921
/// </summary>
2022
internal static class MboxParser
2123
{
@@ -129,11 +131,31 @@ private static void WriteBodyLine(MemoryStream message, byte[] line)
129131

130132
if (quoteCount > 0 && StartsWithFromMarkerAt(line, quoteCount))
131133
{
132-
message.Write(line, 1, line.Length - 1);
134+
WriteNormalizedLine(message, line, 1);
133135
return;
134136
}
135137

136-
message.Write(line, 0, line.Length);
138+
WriteNormalizedLine(message, line, 0);
139+
}
140+
141+
/// <summary>
142+
/// Writes the line starting at <paramref name="offset"/>, rewriting a
143+
/// bare LF terminator to CRLF.
144+
/// </summary>
145+
private static void WriteNormalizedLine(MemoryStream message, byte[] line, int offset)
146+
{
147+
int length = line.Length - offset;
148+
149+
if (length >= 1 && line[line.Length - 1] == '\n' &&
150+
(length < 2 || line[line.Length - 2] != '\r'))
151+
{
152+
message.Write(line, offset, length - 1);
153+
message.WriteByte((byte)'\r');
154+
message.WriteByte((byte)'\n');
155+
return;
156+
}
157+
158+
message.Write(line, offset, length);
137159
}
138160

139161
private static bool StartsWithFromMarkerAt(byte[] line, int offset)

hmailserver/source/Tools/ImportTool/MboxImport/ucMboxProgress.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,15 @@ private void RunImport()
115115

116116
private bool ImportMessage(hMailServer.Utilities utilities, string destinationDirectory, string folderName, byte[] messageBytes)
117117
{
118-
var fileName = Path.Combine(destinationDirectory, "{" + Guid.NewGuid().ToString().ToUpperInvariant() + "}.eml");
118+
// Use the server's on-disk convention, {account}\XY\{GUID}.eml where
119+
// XY is the GUID's first two characters. A file placed anywhere else
120+
// is moved (and renamed) by the import, after which a failure would
121+
// leave it orphaned at a path we no longer know.
122+
var guid = Guid.NewGuid().ToString().ToUpperInvariant();
123+
var subDirectory = Path.Combine(destinationDirectory, guid.Substring(0, 2));
124+
Directory.CreateDirectory(subDirectory);
125+
126+
var fileName = Path.Combine(subDirectory, "{" + guid + "}.eml");
119127

120128
File.WriteAllBytes(fileName, messageBytes);
121129

@@ -129,12 +137,13 @@ private bool ImportMessage(hMailServer.Utilities utilities, string destinationDi
129137
AddToLog("A message failed to import: " + ex.Message);
130138
}
131139

132-
// The server did not take ownership of the file; do not leave it behind.
140+
// The server did not take ownership of the file; do not leave it
141+
// behind. Cleanup failure must not abort the remaining import.
133142
try
134143
{
135144
File.Delete(fileName);
136145
}
137-
catch (IOException)
146+
catch (Exception)
138147
{
139148
}
140149

hmailserver/source/Tools/ImportTool/MboxImport/ucMboxSelect.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ private void buttonSelectDirectory_Click(object sender, EventArgs e)
5555
listFiles.Items.Clear();
5656
foreach (var file in Directory.GetFiles(dialog.SelectedPath))
5757
{
58+
// Skip mail-client index files that live next to the mboxes
59+
// (e.g. Thunderbird's Inbox.msf next to Inbox).
60+
var extension = Path.GetExtension(file);
61+
if (string.Equals(extension, ".msf", StringComparison.OrdinalIgnoreCase) ||
62+
string.Equals(extension, ".dat", StringComparison.OrdinalIgnoreCase))
63+
continue;
64+
5865
var item = listFiles.Items.Add(file);
5966
item.SubItems.Add(ucMboxProgress.GetFolderNameForFile(file));
6067
}

hmailserver/source/Tools/ImportTool/Program.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ static void Main()
1515

1616
CommandLineParser.Parse();
1717

18+
// The shared wizard auto-runs all pages when /silent is passed. The
19+
// import wizards have no scripted-argument support, so a silent run
20+
// could only fail (or import with empty selections); refuse it.
21+
if (CommandLineParser.IsSilent())
22+
{
23+
MessageBox.Show("The hMailServer Import Tool does not support silent mode.", "hMailServer Import Tool");
24+
return;
25+
}
26+
1827
hMailServer.Application application = new hMailServer.Application();
1928
if (!Authenticator.AuthenticateUser(application))
2029
return;

hmailserver/source/Tools/Shared/Wizard/ucWizard.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,13 @@ public bool ShowPage(int pageNo)
9191
}
9292
finally
9393
{
94-
buttonNext.Enabled = true;
95-
buttonPrevious.Enabled = true;
96-
buttonCancel.Enabled = true;
94+
// Restore the states from before OnLeavePage; enabling all
95+
// three unconditionally would enable Previous on the first
96+
// page after a failed validation, and clicking it would call
97+
// ShowPage(-1).
98+
buttonNext.Enabled = nextEnabled;
99+
buttonPrevious.Enabled = previousEnabled;
100+
buttonCancel.Enabled = cancelEnabled;
97101
}
98102
}
99103

0 commit comments

Comments
 (0)