diff --git a/MailPrint/MailPrint.csproj b/MailPrint/MailPrint.csproj
index 5496211..0406fa1 100644
--- a/MailPrint/MailPrint.csproj
+++ b/MailPrint/MailPrint.csproj
@@ -8,6 +8,9 @@
MailPrint
MailPrint
Exe
+ 1.0.0
+ 1.0.0.0
+ 1.0.0.0
diff --git a/MailPrint/MailPrintOptions.cs b/MailPrint/MailPrintOptions.cs
index 543b094..9288f01 100644
--- a/MailPrint/MailPrintOptions.cs
+++ b/MailPrint/MailPrintOptions.cs
@@ -32,7 +32,8 @@ public class PrinterProfile
public string PrinterName { get; set; } = "";
public string PaperSource { get; set; } = "";
public int Copies { get; set; } = 1;
- /// Leer = globale Liste verwenden. Gesetzt = überschreibt globale Liste.
+ /// none | long | short
+ public string Duplex { get; set; } = "none";
public List AllowedSenders { get; set; } = new();
public List BlockedSenders { get; set; } = new();
}
diff --git a/MailPrint/Services/PrintService.cs b/MailPrint/Services/PrintService.cs
index ff4cb66..e8b3492 100644
--- a/MailPrint/Services/PrintService.cs
+++ b/MailPrint/Services/PrintService.cs
@@ -40,7 +40,7 @@ public class PrintService
try
{
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);
}
catch (Exception ex)
@@ -65,22 +65,14 @@ public class PrintService
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;
- }
- else
- {
- // SumatraPDF mit bin=
- var sumatra = ResolveSumatra();
- if (sumatra != null)
- {
- RunAndWait(sumatra,
- $"-print-to \"{printerName}\" -print-settings \"bin={paperSource},noscale\" -silent \"{pdfPath}\"");
- return;
- }
+ var settings = BuildPrintSettings(paperSource, duplex);
+ RunAndWait(sumatra, $"-print-to \"{printerName}\" -print-settings \"{settings}\" -silent \"{pdfPath}\"");
+ return;
}
// Fallback Shell-Print
@@ -90,12 +82,12 @@ public class PrintService
p.WaitForExit(30_000);
}
- private bool TryPrintViaSumatra(string pdfPath, string printerName)
+ private static string BuildPrintSettings(string paperSource, string duplex)
{
- var s = ResolveSumatra();
- if (s == null) return false;
- RunAndWait(s, $"-print-to \"{printerName}\" -print-settings \"noscale\" -silent \"{pdfPath}\"");
- return true;
+ var parts = new List { "noscale" };
+ if (!string.IsNullOrEmpty(paperSource)) parts.Add($"bin={paperSource}");
+ if (!string.IsNullOrEmpty(duplex) && duplex != "none") parts.Add($"duplex{duplex}");
+ return string.Join(",", parts);
}
private string? ResolveSumatra()
@@ -110,7 +102,7 @@ public class PrintService
_logger.LogDebug("Exec: {Exe} {Args}", exe, args);
var psi = new ProcessStartInfo(exe, args) { UseShellExecute = false, CreateNoWindow = true };
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);
}
diff --git a/MailPrintConfig/MailPrintConfig.csproj b/MailPrintConfig/MailPrintConfig.csproj
index 51a25ce..f78cb16 100644
--- a/MailPrintConfig/MailPrintConfig.csproj
+++ b/MailPrintConfig/MailPrintConfig.csproj
@@ -8,6 +8,9 @@
true
MailPrintConfig
MailPrintConfig
+ 1.0.0
+ 1.0.0.0
+ 1.0.0.0
diff --git a/MailPrintConfig/MainForm.cs b/MailPrintConfig/MainForm.cs
index 59d75cb..aa4d363 100644
--- a/MailPrintConfig/MainForm.cs
+++ b/MailPrintConfig/MainForm.cs
@@ -27,7 +27,7 @@ public class MainForm : Form
// ── Config / Steuerung ────────────────────────────────────────
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 System.Diagnostics.Process? _proc;
private System.Windows.Forms.Timer _timer = null!;
@@ -66,19 +66,34 @@ public class MainForm : Form
var bottom = new Panel { Dock = DockStyle.Bottom, Height = 84 };
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();
- btnSave.Click += (_, _) => SaveConfig();
- btnStartStop.Click += (_, _) => _ = ToggleServiceAsync();
- btnBrowse.Click += (_, _) => BrowseConfig();
+ // Zeile 1: Laden | Speichern | [Pfad…] [____path____]
+ btnLoad = Btn("Laden", x, 10, 80); x += 86;
+ btnSave = Btn("Speichern", x, 10, 84); x += 90;
+ 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);
}
@@ -116,8 +131,13 @@ public class MainForm : Form
gridProfiles.Columns.Add(colSource);
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.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 DataGridViewCheckBoxColumn { Name = "Ssl", HeaderText = "SSL", Width = 38 });
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 });
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.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();
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 = "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 = "FDelete", HeaderText = "Nach Druck löschen",Width = 120 });
@@ -220,11 +257,8 @@ public class MainForm : Form
gridFolders.CellClick += (_, e) =>
{
if (e.RowIndex < 0 || gridFolders.Columns[e.ColumnIndex].Name != "FBrowse") return;
- using var d = new FolderBrowserDialog { Description = "Ordner wählen" };
- var current = gridFolders.Rows[e.RowIndex].Cells["FPath"].Value?.ToString();
- if (!string.IsNullOrEmpty(current) && Directory.Exists(current)) d.SelectedPath = current;
- if (d.ShowDialog() == DialogResult.OK)
- gridFolders.Rows[e.RowIndex].Cells["FPath"].Value = d.SelectedPath;
+ var rowIndex = e.RowIndex;
+ BeginInvoke(() => BrowseFolder(rowIndex));
};
gridProfiles.CellValueChanged += (_, _) => RefreshProfileDropdowns();
@@ -236,7 +270,7 @@ public class MainForm : Form
// ── Tab: Filter ───────────────────────────────────────────────
private TabPage BuildFilterTab()
{
- var tab = new TabPage("Filter (Global)");
+ var tab = new TabPage("E-Mail-Filter (Global)");
int y = Pad;
tab.Controls.Add(new Label
@@ -247,8 +281,8 @@ public class MainForm : Form
});
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 = "Blacklist (diese Absender blockieren):", Left = Pad + 420, 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 = "E-Mail-Blacklist (diese Absender blockieren):", Left = Pad + 420, Top = y, Width = 390, AutoSize = false });
y += 20;
txtGlobalAllowed = new TextBox
@@ -344,8 +378,12 @@ public class MainForm : Form
y += 28;
}
+ var version = System.Reflection.Assembly.GetExecutingAssembly()
+ .GetName().Version?.ToString(3) ?? "?";
+
AddLine("MailPrint");
y += 4;
+ AddLine($"Version {version}");
AddLine("Automatischer PDF-Druck per E-Mail und REST API.");
AddLine("Kostenlos und quelloffen (MIT-Lizenz).");
y += 16;
@@ -453,6 +491,31 @@ public class MainForm : Form
?? 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()
{
using var d = new OpenFileDialog { Filter = "appsettings.json|appsettings.json|JSON|*.json" };
@@ -499,7 +562,8 @@ public class MainForm : Form
printerCol.Items.Add(printer);
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"];
sc.Items.Clear(); sc.Items.Add("");
@@ -582,6 +646,7 @@ public class MainForm : Form
["PrinterName"] = r.Cells["Printer"].Value?.ToString() ?? "",
["PaperSource"] = r.Cells["Source"].Value?.ToString() ?? "",
["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()),
["BlockedSenders"] = ToJArray(r.Cells["Blocked"].Value?.ToString())
});
@@ -653,6 +718,20 @@ public class MainForm : Form
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)
{
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;
try
@@ -674,14 +753,13 @@ public class MainForm : Form
_proc.Kill(entireProcessTree: true);
await _proc.WaitForExitAsync();
_proc = null;
- SetStatus("MailPrint gestoppt.", Color.DarkOrange);
+ SetStatus("MailPrint EXE gestoppt.", Color.DarkOrange);
}
else
{
var exePath = Path.Combine(
Path.GetDirectoryName(txtConfigPath.Text) ?? AppContext.BaseDirectory,
"MailPrint.exe");
-
if (!File.Exists(exePath))
{ SetStatus($"MailPrint.exe nicht gefunden: {exePath}", Color.Red); return; }
@@ -696,17 +774,89 @@ public class MainForm : Form
};
_proc.Exited += (_, _) => BeginInvoke(RefreshStartStop);
_proc.Start();
- SetStatus($"MailPrint gestartet (PID {_proc.Id})", Color.DarkGreen);
+ SetStatus($"MailPrint EXE gestartet (PID {_proc.Id})", Color.DarkGreen);
}
}
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()
{
if (InvokeRequired) { BeginInvoke(RefreshStartStop); return; }
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;
}
diff --git a/README.md b/README.md
index b60869b..b0ddd4b 100644
--- a/README.md
+++ b/README.md
@@ -1,20 +1,22 @@
# 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
- 📧 **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
- 🌐 **REST API** – PDF per HTTP-Upload oder URL drucken (z.B. aus einem Webshop)
- 🔒 **API-Key Absicherung** – optionaler Schutz für den HTTP-Endpunkt
- 🗂️ **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
- 🔄 **Windows Service** – läuft ohne Anmeldung im Hintergrund