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