v1.0.0 - Version, Ordner-Tab, Duplex, Dienst-Buttons, Passwort-Maskierung, README

This commit is contained in:
administrator 2026-04-19 22:41:54 +02:00
parent a35b4ada20
commit aa38762021
6 changed files with 208 additions and 57 deletions

View file

@ -8,6 +8,9 @@
<RootNamespace>MailPrint</RootNamespace> <RootNamespace>MailPrint</RootNamespace>
<AssemblyName>MailPrint</AssemblyName> <AssemblyName>MailPrint</AssemblyName>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<Version>1.0.0</Version>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View file

@ -32,7 +32,8 @@ public class PrinterProfile
public string PrinterName { get; set; } = ""; public string PrinterName { get; set; } = "";
public string PaperSource { get; set; } = ""; public string PaperSource { get; set; } = "";
public int Copies { get; set; } = 1; public int Copies { get; set; } = 1;
/// <summary>Leer = globale Liste verwenden. Gesetzt = überschreibt globale Liste.</summary> /// <summary>none | long | short</summary>
public string Duplex { get; set; } = "none";
public List<string> AllowedSenders { get; set; } = new(); public List<string> AllowedSenders { get; set; } = new();
public List<string> BlockedSenders { get; set; } = new(); public List<string> BlockedSenders { get; set; } = new();
} }

View file

@ -40,7 +40,7 @@ public class PrintService
try try
{ {
for (int i = 0; i < copies; i++) for (int i = 0; i < copies; i++)
PrintOnce(job.FilePath, printerName, paperSource); PrintOnce(job.FilePath, printerName, paperSource, profile.Duplex);
_logger.LogInformation("Druck OK: {File}", job.FilePath); _logger.LogInformation("Druck OK: {File}", job.FilePath);
} }
catch (Exception ex) catch (Exception ex)
@ -65,22 +65,14 @@ public class PrintService
return _options.PrinterProfiles.FirstOrDefault() ?? new PrinterProfile(); return _options.PrinterProfiles.FirstOrDefault() ?? new PrinterProfile();
} }
private void PrintOnce(string pdfPath, string printerName, string paperSource) private void PrintOnce(string pdfPath, string printerName, string paperSource, string duplex = "none")
{ {
if (string.IsNullOrEmpty(paperSource)) var sumatra = ResolveSumatra();
if (sumatra != null)
{ {
if (TryPrintViaSumatra(pdfPath, printerName)) return; var settings = BuildPrintSettings(paperSource, duplex);
} RunAndWait(sumatra, $"-print-to \"{printerName}\" -print-settings \"{settings}\" -silent \"{pdfPath}\"");
else return;
{
// SumatraPDF mit bin=<FachName>
var sumatra = ResolveSumatra();
if (sumatra != null)
{
RunAndWait(sumatra,
$"-print-to \"{printerName}\" -print-settings \"bin={paperSource},noscale\" -silent \"{pdfPath}\"");
return;
}
} }
// Fallback Shell-Print // Fallback Shell-Print
@ -90,12 +82,12 @@ public class PrintService
p.WaitForExit(30_000); p.WaitForExit(30_000);
} }
private bool TryPrintViaSumatra(string pdfPath, string printerName) private static string BuildPrintSettings(string paperSource, string duplex)
{ {
var s = ResolveSumatra(); var parts = new List<string> { "noscale" };
if (s == null) return false; if (!string.IsNullOrEmpty(paperSource)) parts.Add($"bin={paperSource}");
RunAndWait(s, $"-print-to \"{printerName}\" -print-settings \"noscale\" -silent \"{pdfPath}\""); if (!string.IsNullOrEmpty(duplex) && duplex != "none") parts.Add($"duplex{duplex}");
return true; return string.Join(",", parts);
} }
private string? ResolveSumatra() private string? ResolveSumatra()
@ -110,7 +102,7 @@ public class PrintService
_logger.LogDebug("Exec: {Exe} {Args}", exe, args); _logger.LogDebug("Exec: {Exe} {Args}", exe, args);
var psi = new ProcessStartInfo(exe, args) { UseShellExecute = false, CreateNoWindow = true }; var psi = new ProcessStartInfo(exe, args) { UseShellExecute = false, CreateNoWindow = true };
using var p = Process.Start(psi) ?? throw new InvalidOperationException($"Nicht startbar: {exe}"); using var p = Process.Start(psi) ?? throw new InvalidOperationException($"Nicht startbar: {exe}");
if (!p.WaitForExit(60_000)) { p.Kill(); throw new TimeoutException($"Timeout: {exe}"); } if (!p.WaitForExit(300_000)) { p.Kill(); throw new TimeoutException($"Timeout: {exe}"); }
if (p.ExitCode != 0) _logger.LogWarning("ExitCode {Code}: {Exe}", p.ExitCode, exe); if (p.ExitCode != 0) _logger.LogWarning("ExitCode {Code}: {Exe}", p.ExitCode, exe);
} }

View file

@ -8,6 +8,9 @@
<UseWindowsForms>true</UseWindowsForms> <UseWindowsForms>true</UseWindowsForms>
<RootNamespace>MailPrintConfig</RootNamespace> <RootNamespace>MailPrintConfig</RootNamespace>
<AssemblyName>MailPrintConfig</AssemblyName> <AssemblyName>MailPrintConfig</AssemblyName>
<Version>1.0.0</Version>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View file

@ -27,7 +27,7 @@ public class MainForm : Form
// ── Config / Steuerung ──────────────────────────────────────── // ── Config / Steuerung ────────────────────────────────────────
private TextBox txtConfigPath = null!; private TextBox txtConfigPath = null!;
private Button btnLoad = null!, btnSave = null!, btnStartStop = null!; private Button btnLoad = null!, btnSave = null!, btnStartStop = null!, btnInstall = null!, btnUninstall = null!, btnSvcStart = null!, btnSvcStop = null!;
private Label lblStatus = null!; private Label lblStatus = null!;
private System.Diagnostics.Process? _proc; private System.Diagnostics.Process? _proc;
private System.Windows.Forms.Timer _timer = null!; private System.Windows.Forms.Timer _timer = null!;
@ -66,19 +66,34 @@ public class MainForm : Form
var bottom = new Panel { Dock = DockStyle.Bottom, Height = 84 }; var bottom = new Panel { Dock = DockStyle.Bottom, Height = 84 };
int x = Pad; int x = Pad;
btnLoad = Btn("Laden", x, 10, 84); x += 90;
btnSave = Btn("Speichern", x, 10, 84); x += 90;
btnStartStop = Btn("▶ Starten", x, 10, 110, Color.LightGreen); x += 116;
var btnBrowse = Btn("Pfad…", x, 10, 64); x += 70;
txtConfigPath = new TextBox { Left = x, Top = 12, Width = 340, Anchor = AnchorStyles.Left | AnchorStyles.Top };
lblStatus = new Label { Left = Pad, Top = 46, Width = 820, Height = 30, AutoSize = false, ForeColor = Color.DarkGreen };
btnLoad.Click += (_, _) => LoadConfig(); // Zeile 1: Laden | Speichern | [Pfad…] [____path____]
btnSave.Click += (_, _) => SaveConfig(); btnLoad = Btn("Laden", x, 10, 80); x += 86;
btnStartStop.Click += (_, _) => _ = ToggleServiceAsync(); btnSave = Btn("Speichern", x, 10, 84); x += 90;
btnBrowse.Click += (_, _) => BrowseConfig(); var btnBrowse = Btn("Pfad…", x, 10, 60); x += 66;
txtConfigPath = new TextBox { Left = x, Top = 12, Width = 320, Anchor = AnchorStyles.Left | AnchorStyles.Top };
bottom.Controls.AddRange([btnLoad, btnSave, btnStartStop, btnBrowse, txtConfigPath, lblStatus]); // Zeile 2: EXE starten | EXE stoppen | Dienst installieren | Dienst deinstallieren | Dienst starten | Dienst beenden
int x2 = Pad;
btnStartStop = Btn("▶ EXE starten", x2, 44, 120, Color.LightGreen); x2 += 126;
btnInstall = Btn("⚙ Dienst installieren", x2, 44, 145, Color.LightBlue); x2 += 151;
btnUninstall = Btn("✖ Dienst deinstall.", x2, 44, 140, Color.LightSalmon); x2 += 146;
btnSvcStart = Btn("▶ Dienst starten", x2, 44, 125, Color.PaleGreen); x2 += 131;
btnSvcStop = Btn("⏹ Dienst beenden", x2, 44, 120, Color.LightCoral);
lblStatus = new Label { Left = Pad, Top = 68, Width = 820, Height = 16, AutoSize = false, ForeColor = Color.DarkGreen };
btnLoad.Click += (_, _) => LoadConfig();
btnSave.Click += (_, _) => SaveConfig();
btnStartStop.Click += (_, _) => _ = ToggleExeAsync();
btnBrowse.Click += (_, _) => BrowseConfig();
btnInstall.Click += (_, _) => _ = ServiceActionAsync("install");
btnUninstall.Click += (_, _) => _ = ServiceActionAsync("uninstall");
btnSvcStart.Click += (_, _) => _ = ServiceActionAsync("start");
btnSvcStop.Click += (_, _) => _ = ServiceActionAsync("stop");
bottom.Controls.AddRange([btnLoad, btnSave, btnBrowse, txtConfigPath,
btnStartStop, btnInstall, btnUninstall, btnSvcStart, btnSvcStop, lblStatus]);
Controls.Add(bottom); Controls.Add(bottom);
} }
@ -116,8 +131,13 @@ public class MainForm : Form
gridProfiles.Columns.Add(colSource); gridProfiles.Columns.Add(colSource);
gridProfiles.Columns.Add(new DataGridViewTextBoxColumn { Name = "Copies", HeaderText = "Kopien", Width = 55 }); gridProfiles.Columns.Add(new DataGridViewTextBoxColumn { Name = "Copies", HeaderText = "Kopien", Width = 55 });
gridProfiles.Columns.Add(new DataGridViewTextBoxColumn { Name = "Allowed", HeaderText = "Whitelist (Komma)", Width = 200 });
gridProfiles.Columns.Add(new DataGridViewTextBoxColumn { Name = "Blocked", HeaderText = "Blacklist (Komma)", Width = 200 }); var colDuplex = new DataGridViewComboBoxColumn { Name = "Duplex", HeaderText = "Duplex", FlatStyle = FlatStyle.Flat, Width = 110 };
colDuplex.Items.AddRange(["Aus", "Lange Seite", "Kurze Seite"]);
gridProfiles.Columns.Add(colDuplex);
gridProfiles.Columns.Add(new DataGridViewTextBoxColumn { Name = "Allowed", HeaderText = "E-Mail-Whitelist (Komma)", Width = 220 });
gridProfiles.Columns.Add(new DataGridViewTextBoxColumn { Name = "Blocked", HeaderText = "E-Mail-Blacklist (Komma)", Width = 220 });
gridProfiles.DataError += (_, e) => e.ThrowException = false; gridProfiles.DataError += (_, e) => e.ThrowException = false;
gridProfiles.CellValueChanged += GridProfiles_CellValueChanged; gridProfiles.CellValueChanged += GridProfiles_CellValueChanged;
@ -163,7 +183,10 @@ public class MainForm : Form
gridAccounts.Columns.Add(new DataGridViewTextBoxColumn { Name = "Port", HeaderText = "Port", Width = 52 }); gridAccounts.Columns.Add(new DataGridViewTextBoxColumn { Name = "Port", HeaderText = "Port", Width = 52 });
gridAccounts.Columns.Add(new DataGridViewCheckBoxColumn { Name = "Ssl", HeaderText = "SSL", Width = 38 }); gridAccounts.Columns.Add(new DataGridViewCheckBoxColumn { Name = "Ssl", HeaderText = "SSL", Width = 38 });
gridAccounts.Columns.Add(new DataGridViewTextBoxColumn { Name = "User", HeaderText = "Benutzername", Width = 180 }); gridAccounts.Columns.Add(new DataGridViewTextBoxColumn { Name = "User", HeaderText = "Benutzername", Width = 180 });
gridAccounts.Columns.Add(new DataGridViewTextBoxColumn { Name = "Pass", HeaderText = "Passwort", Width = 180 }); var colPass = new DataGridViewTextBoxColumn { Name = "Pass", HeaderText = "Passwort", Width = 180 };
colPass.DefaultCellStyle.NullValue = null;
colPass.DefaultCellStyle.Font = new Font("Courier New", 9f);
gridAccounts.Columns.Add(colPass);
gridAccounts.Columns.Add(new DataGridViewTextBoxColumn { Name = "Folder", HeaderText = "Ordner", Width = 80 }); gridAccounts.Columns.Add(new DataGridViewTextBoxColumn { Name = "Folder", HeaderText = "Ordner", Width = 80 });
var colProfile = new DataGridViewComboBoxColumn { Name = "Profile", HeaderText = "Drucker-Profil", FlatStyle = FlatStyle.Flat, Width = 150 }; var colProfile = new DataGridViewComboBoxColumn { Name = "Profile", HeaderText = "Drucker-Profil", FlatStyle = FlatStyle.Flat, Width = 150 };
@ -171,6 +194,20 @@ public class MainForm : Form
gridAccounts.Columns.Add(colProfile); gridAccounts.Columns.Add(colProfile);
gridAccounts.DataError += (_, e) => e.ThrowException = false; gridAccounts.DataError += (_, e) => e.ThrowException = false;
// Passwort maskieren
gridAccounts.CellFormatting += (_, e) =>
{
if (e.RowIndex >= 0 && gridAccounts.Columns[e.ColumnIndex].Name == "Pass" && e.Value is string pw)
e.Value = new string('●', pw.Length);
};
gridAccounts.CellBeginEdit += (_, e) =>
{
if (e.RowIndex >= 0 && gridAccounts.Columns[e.ColumnIndex].Name == "Pass")
{
var cell = gridAccounts.Rows[e.RowIndex].Cells["Pass"];
// beim Bearbeiten Klartext zeigen Value ist bereits der echte Wert
}
};
gridProfiles.CellValueChanged += (_, _) => RefreshProfileDropdowns(); gridProfiles.CellValueChanged += (_, _) => RefreshProfileDropdowns();
AttachContextMenu(gridAccounts); AttachContextMenu(gridAccounts);
@ -202,7 +239,7 @@ public class MainForm : Form
gridFolders.Columns.Add(new DataGridViewTextBoxColumn { Name = "FName", HeaderText = "Name", Width = 120 }); gridFolders.Columns.Add(new DataGridViewTextBoxColumn { Name = "FName", HeaderText = "Name", Width = 120 });
gridFolders.Columns.Add(new DataGridViewTextBoxColumn { Name = "FPath", HeaderText = "Pfad", Width = 400 }); gridFolders.Columns.Add(new DataGridViewTextBoxColumn { Name = "FPath", HeaderText = "Pfad", Width = 400 });
gridFolders.Columns.Add(new DataGridViewButtonColumn { Name = "FBrowse", HeaderText = "", Text = "…", Width = 30, UseColumnTextForButtonValue = true }); gridFolders.Columns.Add(new DataGridViewButtonColumn { Name = "FBrowse", HeaderText = "", Text = "…", Width = 30 });
gridFolders.Columns.Add(new DataGridViewCheckBoxColumn { Name = "FSubfolders",HeaderText = "Unterordner", Width = 90 }); gridFolders.Columns.Add(new DataGridViewCheckBoxColumn { Name = "FSubfolders",HeaderText = "Unterordner", Width = 90 });
gridFolders.Columns.Add(new DataGridViewCheckBoxColumn { Name = "FDelete", HeaderText = "Nach Druck löschen",Width = 120 }); gridFolders.Columns.Add(new DataGridViewCheckBoxColumn { Name = "FDelete", HeaderText = "Nach Druck löschen",Width = 120 });
@ -220,11 +257,8 @@ public class MainForm : Form
gridFolders.CellClick += (_, e) => gridFolders.CellClick += (_, e) =>
{ {
if (e.RowIndex < 0 || gridFolders.Columns[e.ColumnIndex].Name != "FBrowse") return; if (e.RowIndex < 0 || gridFolders.Columns[e.ColumnIndex].Name != "FBrowse") return;
using var d = new FolderBrowserDialog { Description = "Ordner wählen" }; var rowIndex = e.RowIndex;
var current = gridFolders.Rows[e.RowIndex].Cells["FPath"].Value?.ToString(); BeginInvoke(() => BrowseFolder(rowIndex));
if (!string.IsNullOrEmpty(current) && Directory.Exists(current)) d.SelectedPath = current;
if (d.ShowDialog() == DialogResult.OK)
gridFolders.Rows[e.RowIndex].Cells["FPath"].Value = d.SelectedPath;
}; };
gridProfiles.CellValueChanged += (_, _) => RefreshProfileDropdowns(); gridProfiles.CellValueChanged += (_, _) => RefreshProfileDropdowns();
@ -236,7 +270,7 @@ public class MainForm : Form
// ── Tab: Filter ─────────────────────────────────────────────── // ── Tab: Filter ───────────────────────────────────────────────
private TabPage BuildFilterTab() private TabPage BuildFilterTab()
{ {
var tab = new TabPage("Filter (Global)"); var tab = new TabPage("E-Mail-Filter (Global)");
int y = Pad; int y = Pad;
tab.Controls.Add(new Label tab.Controls.Add(new Label
@ -247,8 +281,8 @@ public class MainForm : Form
}); });
y += 40; y += 40;
tab.Controls.Add(new Label { Text = "Whitelist (nur diese Absender drucken):", Left = Pad, Top = y, Width = 380, AutoSize = false }); tab.Controls.Add(new Label { Text = "E-Mail-Whitelist (nur diese Absender drucken):", Left = Pad, Top = y, Width = 390, AutoSize = false });
tab.Controls.Add(new Label { Text = "Blacklist (diese Absender blockieren):", Left = Pad + 420, Top = y, Width = 380, AutoSize = false }); tab.Controls.Add(new Label { Text = "E-Mail-Blacklist (diese Absender blockieren):", Left = Pad + 420, Top = y, Width = 390, AutoSize = false });
y += 20; y += 20;
txtGlobalAllowed = new TextBox txtGlobalAllowed = new TextBox
@ -344,8 +378,12 @@ public class MainForm : Form
y += 28; y += 28;
} }
var version = System.Reflection.Assembly.GetExecutingAssembly()
.GetName().Version?.ToString(3) ?? "?";
AddLine("MailPrint"); AddLine("MailPrint");
y += 4; y += 4;
AddLine($"Version {version}");
AddLine("Automatischer PDF-Druck per E-Mail und REST API."); AddLine("Automatischer PDF-Druck per E-Mail und REST API.");
AddLine("Kostenlos und quelloffen (MIT-Lizenz)."); AddLine("Kostenlos und quelloffen (MIT-Lizenz).");
y += 16; y += 16;
@ -453,6 +491,31 @@ public class MainForm : Form
?? Path.Combine(AppContext.BaseDirectory, "appsettings.json"); ?? Path.Combine(AppContext.BaseDirectory, "appsettings.json");
} }
private void BrowseFolder(int rowIndex)
{
string? result = null;
var current = gridFolders.Rows[rowIndex].Cells["FPath"].Value?.ToString() ?? "";
// Eigener STA-Thread verhindert Blockierung durch COM-Dialog
var t = new Thread(() =>
{
using var d = new FolderBrowserDialog
{
Description = "Ordner wählen",
UseDescriptionForTitle = true,
ShowNewFolderButton = true
};
if (Directory.Exists(current)) d.SelectedPath = current;
result = d.ShowDialog() == DialogResult.OK ? d.SelectedPath : null;
});
t.SetApartmentState(ApartmentState.STA);
t.Start();
t.Join();
if (result != null)
gridFolders.Rows[rowIndex].Cells["FPath"].Value = result;
}
private void BrowseConfig() private void BrowseConfig()
{ {
using var d = new OpenFileDialog { Filter = "appsettings.json|appsettings.json|JSON|*.json" }; using var d = new OpenFileDialog { Filter = "appsettings.json|appsettings.json|JSON|*.json" };
@ -499,7 +562,8 @@ public class MainForm : Form
printerCol.Items.Add(printer); printerCol.Items.Add(printer);
int ri = gridProfiles.Rows.Add( int ri = gridProfiles.Rows.Add(
p["Name"]?.ToString() ?? "", printer, "", p["Copies"]?.ToString() ?? "1", allowed, blocked); p["Name"]?.ToString() ?? "", printer, "", p["Copies"]?.ToString() ?? "1",
DuplexToDisplay(p["Duplex"]?.ToString()), allowed, blocked);
var sc = (DataGridViewComboBoxCell)gridProfiles.Rows[ri].Cells["Source"]; var sc = (DataGridViewComboBoxCell)gridProfiles.Rows[ri].Cells["Source"];
sc.Items.Clear(); sc.Items.Add(""); sc.Items.Clear(); sc.Items.Add("");
@ -582,6 +646,7 @@ public class MainForm : Form
["PrinterName"] = r.Cells["Printer"].Value?.ToString() ?? "", ["PrinterName"] = r.Cells["Printer"].Value?.ToString() ?? "",
["PaperSource"] = r.Cells["Source"].Value?.ToString() ?? "", ["PaperSource"] = r.Cells["Source"].Value?.ToString() ?? "",
["Copies"] = int.TryParse(r.Cells["Copies"].Value?.ToString(), out int c) ? c : 1, ["Copies"] = int.TryParse(r.Cells["Copies"].Value?.ToString(), out int c) ? c : 1,
["Duplex"] = DuplexToJson(r.Cells["Duplex"].Value?.ToString()),
["AllowedSenders"] = ToJArray(r.Cells["Allowed"].Value?.ToString()), ["AllowedSenders"] = ToJArray(r.Cells["Allowed"].Value?.ToString()),
["BlockedSenders"] = ToJArray(r.Cells["Blocked"].Value?.ToString()) ["BlockedSenders"] = ToJArray(r.Cells["Blocked"].Value?.ToString())
}); });
@ -653,6 +718,20 @@ public class MainForm : Form
catch (Exception ex) { SetStatus($"Fehler: {ex.Message}", Color.Red); } catch (Exception ex) { SetStatus($"Fehler: {ex.Message}", Color.Red); }
} }
private static string DuplexToDisplay(string? json) => json switch
{
"long" => "Lange Seite",
"short" => "Kurze Seite",
_ => "Aus"
};
private static string DuplexToJson(string? display) => display switch
{
"Lange Seite" => "long",
"Kurze Seite" => "short",
_ => "none"
};
private static JArray ToJArray(string? input, bool multiline = false) private static JArray ToJArray(string? input, bool multiline = false)
{ {
if (string.IsNullOrWhiteSpace(input)) return new JArray(); if (string.IsNullOrWhiteSpace(input)) return new JArray();
@ -662,9 +741,9 @@ public class MainForm : Form
} }
// ══════════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════════
// Start / Stop // EXE starten / stoppen
// ══════════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════════
private async Task ToggleServiceAsync() private async Task ToggleExeAsync()
{ {
btnStartStop.Enabled = false; btnStartStop.Enabled = false;
try try
@ -674,14 +753,13 @@ public class MainForm : Form
_proc.Kill(entireProcessTree: true); _proc.Kill(entireProcessTree: true);
await _proc.WaitForExitAsync(); await _proc.WaitForExitAsync();
_proc = null; _proc = null;
SetStatus("MailPrint gestoppt.", Color.DarkOrange); SetStatus("MailPrint EXE gestoppt.", Color.DarkOrange);
} }
else else
{ {
var exePath = Path.Combine( var exePath = Path.Combine(
Path.GetDirectoryName(txtConfigPath.Text) ?? AppContext.BaseDirectory, Path.GetDirectoryName(txtConfigPath.Text) ?? AppContext.BaseDirectory,
"MailPrint.exe"); "MailPrint.exe");
if (!File.Exists(exePath)) if (!File.Exists(exePath))
{ SetStatus($"MailPrint.exe nicht gefunden: {exePath}", Color.Red); return; } { SetStatus($"MailPrint.exe nicht gefunden: {exePath}", Color.Red); return; }
@ -696,17 +774,89 @@ public class MainForm : Form
}; };
_proc.Exited += (_, _) => BeginInvoke(RefreshStartStop); _proc.Exited += (_, _) => BeginInvoke(RefreshStartStop);
_proc.Start(); _proc.Start();
SetStatus($"MailPrint gestartet (PID {_proc.Id})", Color.DarkGreen); SetStatus($"MailPrint EXE gestartet (PID {_proc.Id})", Color.DarkGreen);
} }
} }
finally { btnStartStop.Enabled = true; RefreshStartStop(); } finally { btnStartStop.Enabled = true; RefreshStartStop(); }
} }
// ── Dienst-Aktionen ───────────────────────────────────────────
private const string ServiceName = "MailPrint";
private async Task ServiceActionAsync(string action)
{
var publishDir = Path.GetDirectoryName(txtConfigPath.Text) ?? AppContext.BaseDirectory;
var exePath = Path.Combine(publishDir, "MailPrint.exe");
var installPs = Path.Combine(publishDir, "..", "install-service.ps1");
var uninstallPs= Path.Combine(publishDir, "..", "uninstall-service.ps1");
// Bestätigung nur bei Install/Deinstall
if (action is "install" or "uninstall")
{
var msg = action == "install"
? "Dienst 'MailPrint' jetzt installieren?"
: "Dienst 'MailPrint' wirklich deinstallieren?";
if (MessageBox.Show(msg, "Bestätigung", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes)
return;
}
SetStatus($"Führe aus: {action}…", Color.DarkBlue);
DisableServiceButtons(true);
try
{
string cmd = action switch
{
"install" => $"-ExecutionPolicy Bypass -File \"{Path.GetFullPath(installPs)}\"",
"uninstall" => $"-ExecutionPolicy Bypass -File \"{Path.GetFullPath(uninstallPs)}\"",
"start" => $"-Command Start-Service -Name '{ServiceName}'",
"stop" => $"-Command Stop-Service -Name '{ServiceName}' -Force",
_ => throw new ArgumentException(action)
};
var psi = new System.Diagnostics.ProcessStartInfo
{
FileName = "powershell",
Arguments = $"-NoProfile -NonInteractive {cmd}",
UseShellExecute = true,
Verb = "runas",
WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden
};
using var p = System.Diagnostics.Process.Start(psi)!;
await p.WaitForExitAsync();
if (p.ExitCode == 0)
{
var successMsg = action switch
{
"install" => "Dienst 'MailPrint' wurde erfolgreich installiert.",
"uninstall" => "Dienst 'MailPrint' wurde erfolgreich deinstalliert.",
"start" => "Dienst 'MailPrint' wurde gestartet.",
"stop" => "Dienst 'MailPrint' wurde beendet.",
_ => "Aktion erfolgreich."
};
SetStatus(successMsg, Color.DarkGreen);
MessageBox.Show(successMsg, "Erfolgreich", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
else
SetStatus($"'{action}' fehlgeschlagen (Code {p.ExitCode})", Color.Red);
}
catch (Exception ex) { SetStatus($"Fehler: {ex.Message}", Color.Red); }
finally { DisableServiceButtons(false); RefreshStartStop(); }
}
private void DisableServiceButtons(bool disable)
{
if (InvokeRequired) { BeginInvoke(() => DisableServiceButtons(disable)); return; }
btnInstall.Enabled = btnUninstall.Enabled = btnSvcStart.Enabled = btnSvcStop.Enabled = !disable;
}
private void RefreshStartStop() private void RefreshStartStop()
{ {
if (InvokeRequired) { BeginInvoke(RefreshStartStop); return; } if (InvokeRequired) { BeginInvoke(RefreshStartStop); return; }
bool running = _proc is { HasExited: false }; bool running = _proc is { HasExited: false };
btnStartStop.Text = running ? "⏹ Stoppen" : "▶ Starten"; btnStartStop.Text = running ? "⏹ EXE stoppen" : "▶ EXE starten";
btnStartStop.BackColor = running ? Color.LightCoral : Color.LightGreen; btnStartStop.BackColor = running ? Color.LightCoral : Color.LightGreen;
} }

View file

@ -1,20 +1,22 @@
# MailPrint # MailPrint
**Windows-Dienst zum automatischen Drucken von PDF-Anhängen aus E-Mails und per REST API.** **Windows-Dienst zum automatischen Drucken von PDF-Anhängen aus E-Mails, per REST API und per Ordner-Überwachung.**
Kostenlos und quelloffen. Kostenlos und quelloffen. Version 1.0.0
--- ---
## Features ## Features
- 📧 **IMAP / POP3** Postfächer werden automatisch abgerufen, PDF-Anhänge sofort gedruckt - 📧 **IMAP / POP3** Postfächer werden automatisch abgerufen, PDF-Anhänge sofort gedruckt
- 🖨️ **Mehrere Drucker-Profile** je Profil eigener Drucker, Papierfach und Kopienanzahl - 📂 **Ordner-Überwachung** PDFs in überwachten Ordnern werden automatisch gedruckt
- 🖨️ **Mehrere Drucker-Profile** je Profil eigener Drucker, Papierfach, Duplex und Kopienanzahl
- 📬 **Mehrere Postfächer** jedes Postfach zeigt auf ein Drucker-Profil - 📬 **Mehrere Postfächer** jedes Postfach zeigt auf ein Drucker-Profil
- 🌐 **REST API** PDF per HTTP-Upload oder URL drucken (z.B. aus einem Webshop) - 🌐 **REST API** PDF per HTTP-Upload oder URL drucken (z.B. aus einem Webshop)
- 🔒 **API-Key Absicherung** optionaler Schutz für den HTTP-Endpunkt - 🔒 **API-Key Absicherung** optionaler Schutz für den HTTP-Endpunkt
- 🗂️ **Papierfach-Steuerung** SumatraPDF-basiert, stiller Druck ohne Fenster - 🗂️ **Papierfach-Steuerung** SumatraPDF-basiert, stiller Druck ohne Fenster
- ✉️ **Whitelist / Blacklist** global und pro Drucker-Profil - ↔️ **Duplex-Druck** einseitig, lange Seite oder kurze Seite
- ✉️ **E-Mail-Whitelist / Blacklist** global und pro Drucker-Profil
- ⚙️ **Config-Tool** WinForms GUI zum Konfigurieren ohne JSON-Bearbeitung - ⚙️ **Config-Tool** WinForms GUI zum Konfigurieren ohne JSON-Bearbeitung
- 🔄 **Windows Service** läuft ohne Anmeldung im Hintergrund - 🔄 **Windows Service** läuft ohne Anmeldung im Hintergrund