v1.0.0 - Version, Ordner-Tab, Duplex, Dienst-Buttons, Passwort-Maskierung, README
This commit is contained in:
parent
a35b4ada20
commit
aa38762021
6 changed files with 208 additions and 57 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
10
README.md
10
README.md
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue