Initial commit clean history
This commit is contained in:
commit
c963c4d9e9
19 changed files with 2096 additions and 0 deletions
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Build-Ausgaben
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
publish/
|
||||||
|
|
||||||
|
# Skripte aus publish/ im Root behalten
|
||||||
|
!install-service.ps1
|
||||||
|
!uninstall-service.ps1
|
||||||
|
|
||||||
|
# Visual Studio
|
||||||
|
.vs/
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docobj
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Secrets / Config mit Zugangsdaten
|
||||||
|
**/appsettings.json
|
||||||
|
**/appsettings.*.json
|
||||||
|
|
||||||
|
# Temp
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
20
Directory.Build.props
Normal file
20
Directory.Build.props
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<!-- Version aus Git-Tag lesen: git tag v1.2.3 → Version 1.2.3 -->
|
||||||
|
<GitVersion Condition="'$(GitVersion)' == ''">$([System.String]::Copy('1.0.0'))</GitVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<Target Name="SetVersionFromGit" BeforeTargets="GetAssemblyVersion;Build;Publish">
|
||||||
|
<Exec Command="git describe --tags --abbrev=0 2>nul" ConsoleToMSBuild="true" IgnoreExitCode="true">
|
||||||
|
<Output TaskParameter="ConsoleOutput" PropertyName="GitTagRaw" />
|
||||||
|
</Exec>
|
||||||
|
<PropertyGroup>
|
||||||
|
<!-- v1.2.3 → 1.2.3 -->
|
||||||
|
<GitTagClean>$(GitTagRaw.TrimStart('v').Trim())</GitTagClean>
|
||||||
|
<Version Condition="'$(GitTagClean)' != ''">$(GitTagClean)</Version>
|
||||||
|
<AssemblyVersion Condition="'$(GitTagClean)' != ''">$(GitTagClean).0</AssemblyVersion>
|
||||||
|
<FileVersion Condition="'$(GitTagClean)' != ''">$(GitTagClean).0</FileVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Message Text="Build-Version: $(Version)" Importance="high" />
|
||||||
|
</Target>
|
||||||
|
</Project>
|
||||||
25
MailPrint.sln
Normal file
25
MailPrint.sln
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.0.31903.59
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MailPrint", "MailPrint\MailPrint.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MailPrintConfig", "MailPrintConfig\MailPrintConfig.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
51
MailPrint/ApiKeyMiddleware.cs
Normal file
51
MailPrint/ApiKeyMiddleware.cs
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace MailPrint;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Schützt alle /api/* Routen mit einem statischen API-Key.
|
||||||
|
/// Header: X-Api-Key: <key>
|
||||||
|
/// Wenn kein Key konfiguriert ist, wird die Middleware übersprungen.
|
||||||
|
/// </summary>
|
||||||
|
public class ApiKeyMiddleware
|
||||||
|
{
|
||||||
|
private const string HeaderName = "X-Api-Key";
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
private readonly string _apiKey;
|
||||||
|
private readonly ILogger<ApiKeyMiddleware> _logger;
|
||||||
|
|
||||||
|
public ApiKeyMiddleware(RequestDelegate next, IOptions<MailPrintOptions> options, ILogger<ApiKeyMiddleware> logger)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
_apiKey = options.Value.WebApi.ApiKey;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
// Kein API-Key konfiguriert → durchlassen (Entwicklung / lokales Netz)
|
||||||
|
if (string.IsNullOrEmpty(_apiKey))
|
||||||
|
{
|
||||||
|
await _next(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swagger UI immer erlauben
|
||||||
|
if (context.Request.Path.StartsWithSegments("/swagger"))
|
||||||
|
{
|
||||||
|
await _next(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.Request.Headers.TryGetValue(HeaderName, out var provided) ||
|
||||||
|
provided != _apiKey)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Ungültiger API-Key von {IP}", context.Connection.RemoteIpAddress);
|
||||||
|
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||||
|
await context.Response.WriteAsJsonAsync(new { error = "Ungültiger oder fehlender API-Key." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _next(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
131
MailPrint/Controllers/PrintController.cs
Normal file
131
MailPrint/Controllers/PrintController.cs
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
using MailPrint.Services;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace MailPrint.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class PrintController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly PrintService _printService;
|
||||||
|
private readonly MailPrintOptions _options;
|
||||||
|
private readonly ILogger<PrintController> _logger;
|
||||||
|
|
||||||
|
public PrintController(PrintService printService, IOptions<MailPrintOptions> options, ILogger<PrintController> logger)
|
||||||
|
{
|
||||||
|
_printService = printService;
|
||||||
|
_options = options.Value;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PDF hochladen und drucken.
|
||||||
|
/// POST /api/print/upload
|
||||||
|
/// Form-Data: file, printer (opt), paperSource (opt), copies (opt)
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("upload")]
|
||||||
|
[Consumes("multipart/form-data")]
|
||||||
|
public async Task<IActionResult> PrintUpload(
|
||||||
|
IFormFile file,
|
||||||
|
[FromForm] string? printer = null,
|
||||||
|
[FromForm] string? paperSource = null,
|
||||||
|
[FromForm] int copies = 0)
|
||||||
|
{
|
||||||
|
if (file is null || file.Length == 0)
|
||||||
|
return BadRequest(new { error = "Keine Datei." });
|
||||||
|
|
||||||
|
var ext = Path.GetExtension(file.FileName).ToLower();
|
||||||
|
if (!_options.AllowedExtensions.Contains(ext))
|
||||||
|
return BadRequest(new { error = $"Dateityp '{ext}' nicht erlaubt." });
|
||||||
|
|
||||||
|
Directory.CreateDirectory(_options.TempDirectory);
|
||||||
|
var tempFile = Path.Combine(_options.TempDirectory, $"{Guid.NewGuid()}{ext}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using (var fs = System.IO.File.Create(tempFile))
|
||||||
|
await file.CopyToAsync(fs);
|
||||||
|
|
||||||
|
_printService.PrintPdf(new PrintJob
|
||||||
|
{
|
||||||
|
FilePath = tempFile,
|
||||||
|
MailSubject = file.FileName,
|
||||||
|
MailFrom = "WebAPI",
|
||||||
|
PrinterOverride = printer,
|
||||||
|
PaperSourceOverride = paperSource,
|
||||||
|
CopiesOverride = copies > 0 ? copies : null
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(new { success = true, file = file.FileName, printer, paperSource });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "WebAPI Druckfehler: {File}", file.FileName);
|
||||||
|
return StatusCode(500, new { error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PDF per URL drucken.
|
||||||
|
/// POST /api/print/url
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("url")]
|
||||||
|
public async Task<IActionResult> PrintUrl([FromBody] PrintUrlRequest request)
|
||||||
|
{
|
||||||
|
if (!Uri.TryCreate(request.Url, UriKind.Absolute, out var uri))
|
||||||
|
return BadRequest(new { error = "Ungültige URL." });
|
||||||
|
|
||||||
|
Directory.CreateDirectory(_options.TempDirectory);
|
||||||
|
var tempFile = Path.Combine(_options.TempDirectory, $"{Guid.NewGuid()}.pdf");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
|
||||||
|
await System.IO.File.WriteAllBytesAsync(tempFile, await http.GetByteArrayAsync(uri));
|
||||||
|
|
||||||
|
_printService.PrintPdf(new PrintJob
|
||||||
|
{
|
||||||
|
FilePath = tempFile,
|
||||||
|
MailSubject = uri.Segments.LastOrDefault() ?? "download.pdf",
|
||||||
|
MailFrom = "WebAPI/URL",
|
||||||
|
PrinterOverride = request.Printer,
|
||||||
|
PaperSourceOverride = request.PaperSource,
|
||||||
|
CopiesOverride = request.Copies > 0 ? request.Copies : null
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(new { success = true, url = request.Url, printer = request.Printer, paperSource = request.PaperSource });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "WebAPI URL-Druckfehler: {Url}", request.Url);
|
||||||
|
return StatusCode(500, new { error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Alle Drucker mit Papierfächern.
|
||||||
|
/// GET /api/print/printers
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("printers")]
|
||||||
|
public IActionResult GetPrinters()
|
||||||
|
{
|
||||||
|
var infos = _printService.GetPrinterInfos();
|
||||||
|
var defaultPrinter = _options.PrinterProfiles.FirstOrDefault()?.PrinterName
|
||||||
|
?? infos.FirstOrDefault()?.Name;
|
||||||
|
return Ok(new { defaultPrinter, profiles = _options.PrinterProfiles, printers = infos });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("health")]
|
||||||
|
public IActionResult Health() =>
|
||||||
|
Ok(new { status = "ok", service = "MailPrint", timestamp = DateTime.UtcNow });
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PrintUrlRequest
|
||||||
|
{
|
||||||
|
[Required] public string Url { get; set; } = "";
|
||||||
|
public string? Printer { get; set; }
|
||||||
|
public string? PaperSource { get; set; }
|
||||||
|
public int Copies { get; set; } = 0;
|
||||||
|
}
|
||||||
25
MailPrint/MailPrint.csproj
Normal file
25
MailPrint/MailPrint.csproj
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0-windows</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<UserSecretsId>mailprint-service</UserSecretsId>
|
||||||
|
<RootNamespace>MailPrint</RootNamespace>
|
||||||
|
<AssemblyName>MailPrint</AssemblyName>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="MailKit" Version="4.7.1.1" />
|
||||||
|
<PackageReference Include="MimeKit" Version="4.7.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.0-preview.3.25171.5" />
|
||||||
|
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.EventLog" Version="4.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.3.1" />
|
||||||
|
<PackageReference Include="System.Drawing.Common" Version="9.0.4" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
72
MailPrint/MailPrintOptions.cs
Normal file
72
MailPrint/MailPrintOptions.cs
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
namespace MailPrint;
|
||||||
|
|
||||||
|
public class MailPrintOptions
|
||||||
|
{
|
||||||
|
public int PollIntervalSeconds { get; set; } = 60;
|
||||||
|
public string SubjectFilter { get; set; } = "";
|
||||||
|
public bool DeleteAfterPrint { get; set; } = true;
|
||||||
|
public bool MarkAsRead { get; set; } = true;
|
||||||
|
public string SumatraPath { get; set; } = "";
|
||||||
|
public string TempDirectory { get; set; } = Path.Combine(Path.GetTempPath(), "MailPrint");
|
||||||
|
public List<string> AllowedExtensions { get; set; } = new() { ".pdf" };
|
||||||
|
|
||||||
|
/// <summary>Drucker-Profile: Name → Drucker + Fach + Kopien</summary>
|
||||||
|
public List<PrinterProfile> PrinterProfiles { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>Postfächer: jedes hat eigene Credentials + zeigt auf ein PrinterProfile</summary>
|
||||||
|
public List<MailAccount> Accounts { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>Global – wird verwendet wenn das Profil keine eigene Liste hat.</summary>
|
||||||
|
public List<string> AllowedSenders { get; set; } = new();
|
||||||
|
public List<string> BlockedSenders { get; set; } = new();
|
||||||
|
|
||||||
|
public WebApiOptions WebApi { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>Ordner-Überwachung: PDFs in diesen Ordnern werden automatisch gedruckt.</summary>
|
||||||
|
public List<FolderWatcher> FolderWatchers { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PrinterProfile
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public string PrinterName { get; set; } = "";
|
||||||
|
public string PaperSource { get; set; } = "";
|
||||||
|
public int Copies { get; set; } = 1;
|
||||||
|
/// <summary>none | long | short</summary>
|
||||||
|
public string Duplex { get; set; } = "none";
|
||||||
|
public List<string> AllowedSenders { get; set; } = new();
|
||||||
|
public List<string> BlockedSenders { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MailAccount
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public string Protocol { get; set; } = "IMAP";
|
||||||
|
public string Host { get; set; } = "";
|
||||||
|
public int Port { get; set; } = 993;
|
||||||
|
public bool UseSsl { get; set; } = true;
|
||||||
|
public string Username { get; set; } = "";
|
||||||
|
public string Password { get; set; } = "";
|
||||||
|
public string Folder { get; set; } = "INBOX";
|
||||||
|
/// <summary>Name eines PrinterProfile – leer = erstes Profil</summary>
|
||||||
|
public string PrinterProfileName { get; set; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WebApiOptions
|
||||||
|
{
|
||||||
|
public int Port { get; set; } = 5100;
|
||||||
|
public bool BindAllInterfaces { get; set; } = false;
|
||||||
|
public string ApiKey { get; set; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FolderWatcher
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
/// <summary>Zu überwachender Ordner (vollständiger Pfad)</summary>
|
||||||
|
public string Path { get; set; } = "";
|
||||||
|
/// <summary>Auch Unterordner überwachen</summary>
|
||||||
|
public bool IncludeSubfolders { get; set; } = false;
|
||||||
|
/// <summary>Datei nach erfolgreichem Druck löschen</summary>
|
||||||
|
public bool DeleteAfterPrint { get; set; } = true;
|
||||||
|
public string PrinterProfileName { get; set; } = "";
|
||||||
|
}
|
||||||
70
MailPrint/MailPrintWorker.cs
Normal file
70
MailPrint/MailPrintWorker.cs
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
using MailPrint.Services;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace MailPrint;
|
||||||
|
|
||||||
|
public class MailPrintWorker : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly ILogger<MailPrintWorker> _logger;
|
||||||
|
private readonly MailFetchService _mailService;
|
||||||
|
private readonly PrintService _printService;
|
||||||
|
private readonly FolderWatcherService _folderService;
|
||||||
|
private readonly MailPrintOptions _options;
|
||||||
|
|
||||||
|
public MailPrintWorker(
|
||||||
|
ILogger<MailPrintWorker> logger,
|
||||||
|
MailFetchService mailService,
|
||||||
|
PrintService printService,
|
||||||
|
FolderWatcherService folderService,
|
||||||
|
IOptions<MailPrintOptions> options)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_mailService = mailService;
|
||||||
|
_printService = printService;
|
||||||
|
_folderService = folderService;
|
||||||
|
_options = options.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
// Ordner-Überwachung starten (event-basiert, läuft dauerhaft)
|
||||||
|
_folderService.Start();
|
||||||
|
|
||||||
|
if (_options.Accounts.Count == 0)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Keine Postfächer konfiguriert – Mail-Polling deaktiviert.");
|
||||||
|
await Task.Delay(Timeout.Infinite, ct).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("{Count} Postfach/Postfächer konfiguriert, Intervall: {Interval}s",
|
||||||
|
_options.Accounts.Count, _options.PollIntervalSeconds);
|
||||||
|
|
||||||
|
var tasks = _options.Accounts.Select(account => PollAccountAsync(account, ct));
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PollAccountAsync(MailAccount account, CancellationToken ct)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Starte Polling für [{Account}] ({Protocol}://{Host})",
|
||||||
|
account.Name, account.Protocol, account.Host);
|
||||||
|
|
||||||
|
while (!ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var jobs = await _mailService.FetchJobsForAccountAsync(account, ct);
|
||||||
|
foreach (var job in jobs)
|
||||||
|
{
|
||||||
|
if (ct.IsCancellationRequested) break;
|
||||||
|
try { _printService.PrintPdf(job); }
|
||||||
|
catch (Exception ex) { _logger.LogError(ex, "[{Account}] Druckfehler", account.Name); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { break; }
|
||||||
|
catch (Exception ex) { _logger.LogError(ex, "[{Account}] Fehler beim Mail-Abruf", account.Name); }
|
||||||
|
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(_options.PollIntervalSeconds), ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
74
MailPrint/Program.cs
Normal file
74
MailPrint/Program.cs
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
using MailPrint;
|
||||||
|
using MailPrint.Services;
|
||||||
|
using Serilog;
|
||||||
|
using Serilog.Events;
|
||||||
|
|
||||||
|
// Working directory auf EXE-Verzeichnis setzen
|
||||||
|
Directory.SetCurrentDirectory(AppContext.BaseDirectory);
|
||||||
|
|
||||||
|
Log.Logger = new LoggerConfiguration()
|
||||||
|
.MinimumLevel.Information()
|
||||||
|
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
|
||||||
|
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
|
||||||
|
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
|
||||||
|
.WriteTo.File(
|
||||||
|
Path.Combine(AppContext.BaseDirectory, "logs", "mailprint-.log"),
|
||||||
|
rollingInterval: RollingInterval.Day,
|
||||||
|
retainedFileCountLimit: 30)
|
||||||
|
.CreateLogger();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Log.Information("MailPrint gestartet. Beenden mit Ctrl+C.");
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Als Windows Service UND als Konsole nutzbar
|
||||||
|
builder.Host.UseWindowsService(options => options.ServiceName = "MailPrint");
|
||||||
|
|
||||||
|
builder.Host.UseSerilog();
|
||||||
|
|
||||||
|
builder.Configuration
|
||||||
|
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
|
||||||
|
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true)
|
||||||
|
.AddEnvironmentVariables();
|
||||||
|
|
||||||
|
builder.WebHost.ConfigureKestrel(kestrel =>
|
||||||
|
{
|
||||||
|
var port = builder.Configuration.GetValue<int>("MailPrint:WebApi:Port", 5100);
|
||||||
|
var bindAll = builder.Configuration.GetValue<bool>("MailPrint:WebApi:BindAllInterfaces", false);
|
||||||
|
if (bindAll)
|
||||||
|
kestrel.ListenAnyIP(port);
|
||||||
|
else
|
||||||
|
kestrel.ListenLocalhost(port);
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.Configure<MailPrintOptions>(builder.Configuration.GetSection("MailPrint"));
|
||||||
|
builder.Services.AddSingleton<PrintService>();
|
||||||
|
builder.Services.AddSingleton<MailFetchService>();
|
||||||
|
builder.Services.AddSingleton<FolderWatcherService>();
|
||||||
|
builder.Services.AddHostedService<MailPrintWorker>();
|
||||||
|
builder.Services.AddControllers();
|
||||||
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
|
builder.Services.AddSwaggerGen(c =>
|
||||||
|
c.SwaggerDoc("v1", new() { Title = "MailPrint API", Version = "v1" }));
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI();
|
||||||
|
app.UseMiddleware<ApiKeyMiddleware>();
|
||||||
|
app.MapControllers();
|
||||||
|
|
||||||
|
await app.RunAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||||
|
{
|
||||||
|
Log.Fatal(ex, "Unbehandelter Fehler");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Log.CloseAndFlush();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
12
MailPrint/Properties/launchSettings.json
Normal file
12
MailPrint/Properties/launchSettings.json
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"profiles": {
|
||||||
|
"MailPrint": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
},
|
||||||
|
"applicationUrl": "https://localhost:56813;http://localhost:56814"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
175
MailPrint/Services/FolderWatcherService.cs
Normal file
175
MailPrint/Services/FolderWatcherService.cs
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace MailPrint.Services;
|
||||||
|
|
||||||
|
public class FolderWatcherService : IDisposable
|
||||||
|
{
|
||||||
|
private readonly ILogger<FolderWatcherService> _logger;
|
||||||
|
private readonly PrintService _printService;
|
||||||
|
private readonly MailPrintOptions _options;
|
||||||
|
private readonly List<FileSystemWatcher> _watchers = new();
|
||||||
|
private readonly HashSet<string> _printed = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public FolderWatcherService(
|
||||||
|
ILogger<FolderWatcherService> logger,
|
||||||
|
PrintService printService,
|
||||||
|
IOptions<MailPrintOptions> options)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_printService = printService;
|
||||||
|
_options = options.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
if (_options.FolderWatchers.Count == 0)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Keine Ordner-Überwachung konfiguriert.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gedruckte Dateien laden (damit nach Neustart nicht nochmal gedruckt wird)
|
||||||
|
LoadPrintedLog();
|
||||||
|
|
||||||
|
foreach (var config in _options.FolderWatchers)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(config.Path) || !Directory.Exists(config.Path))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("[{Name}] Ordner nicht gefunden: {Path}", config.Name, config.Path);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Beim Start bereits vorhandene PDFs drucken
|
||||||
|
PrintExistingFiles(config);
|
||||||
|
|
||||||
|
var watcher = new FileSystemWatcher(config.Path)
|
||||||
|
{
|
||||||
|
Filter = "*.pdf",
|
||||||
|
IncludeSubdirectories = config.IncludeSubfolders,
|
||||||
|
NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite,
|
||||||
|
EnableRaisingEvents = true
|
||||||
|
};
|
||||||
|
|
||||||
|
watcher.Created += (_, e) => OnFileDetected(e.FullPath, config);
|
||||||
|
watcher.Renamed += (_, e) =>
|
||||||
|
{
|
||||||
|
if (e.Name?.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase) == true)
|
||||||
|
OnFileDetected(e.FullPath, config);
|
||||||
|
};
|
||||||
|
|
||||||
|
_watchers.Add(watcher);
|
||||||
|
_logger.LogInformation("[{Name}] Überwache: {Path} (Unterordner: {Sub})",
|
||||||
|
config.Name, config.Path, config.IncludeSubfolders);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PrintExistingFiles(FolderWatcher config)
|
||||||
|
{
|
||||||
|
var files = Directory.GetFiles(config.Path, "*.pdf",
|
||||||
|
config.IncludeSubfolders ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
|
||||||
|
|
||||||
|
if (files.Length == 0) return;
|
||||||
|
|
||||||
|
_logger.LogInformation("[{Name}] {Count} vorhandene PDF(s) gefunden – werden gedruckt.", config.Name, files.Length);
|
||||||
|
foreach (var file in files)
|
||||||
|
OnFileDetected(file, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFileDetected(string filePath, FolderWatcher config)
|
||||||
|
{
|
||||||
|
// Bereits gedruckte Dateien überspringen (nur relevant wenn DeleteAfterPrint=false)
|
||||||
|
if (_printed.Contains(filePath))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("[{Name}] Bereits gedruckt, überspringe: {File}", config.Name, filePath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kurz warten bis Datei vollständig geschrieben ist
|
||||||
|
if (!WaitForFile(filePath))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("[{Name}] Datei nicht lesbar (gesperrt?): {File}", config.Name, filePath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("[{Name}] Neue Datei: {File}", config.Name, filePath);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_printService.PrintPdf(new PrintJob
|
||||||
|
{
|
||||||
|
FilePath = filePath,
|
||||||
|
MailSubject = Path.GetFileName(filePath),
|
||||||
|
MailFrom = $"Ordner:{config.Name}",
|
||||||
|
PrinterProfileName = config.PrinterProfileName
|
||||||
|
});
|
||||||
|
|
||||||
|
if (config.DeleteAfterPrint)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(filePath);
|
||||||
|
_logger.LogInformation("[{Name}] Gelöscht: {File}", config.Name, filePath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "[{Name}] Löschen fehlgeschlagen: {File}", config.Name, filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Datei bleibt – merken damit sie nicht erneut gedruckt wird
|
||||||
|
_printed.Add(filePath);
|
||||||
|
SavePrintedLog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "[{Name}] Druckfehler: {File}", config.Name, filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool WaitForFile(string path, int timeoutMs = 5000)
|
||||||
|
{
|
||||||
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
while (sw.ElapsedMilliseconds < timeoutMs)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var fs = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.None);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
Thread.Sleep(200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string PrintedLogPath =>
|
||||||
|
Path.Combine(AppContext.BaseDirectory, "folder_printed.log");
|
||||||
|
|
||||||
|
private void LoadPrintedLog()
|
||||||
|
{
|
||||||
|
if (!File.Exists(PrintedLogPath)) return;
|
||||||
|
foreach (var line in File.ReadAllLines(PrintedLogPath))
|
||||||
|
if (!string.IsNullOrWhiteSpace(line)) _printed.Add(line.Trim());
|
||||||
|
_logger.LogInformation("Gedruckte-Dateien-Liste geladen: {Count} Einträge", _printed.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SavePrintedLog()
|
||||||
|
{
|
||||||
|
try { File.WriteAllLines(PrintedLogPath, _printed); }
|
||||||
|
catch (Exception ex) { _logger.LogWarning(ex, "Gedruckte-Dateien-Liste konnte nicht gespeichert werden"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
foreach (var w in _watchers)
|
||||||
|
{
|
||||||
|
w.EnableRaisingEvents = false;
|
||||||
|
w.Dispose();
|
||||||
|
}
|
||||||
|
_watchers.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
159
MailPrint/Services/MailFetchService.cs
Normal file
159
MailPrint/Services/MailFetchService.cs
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
using MailKit;
|
||||||
|
using MailKit.Net.Imap;
|
||||||
|
using MailKit.Net.Pop3;
|
||||||
|
using MailKit.Search;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using MimeKit;
|
||||||
|
|
||||||
|
namespace MailPrint.Services;
|
||||||
|
|
||||||
|
public class MailFetchService
|
||||||
|
{
|
||||||
|
private readonly ILogger<MailFetchService> _logger;
|
||||||
|
private readonly MailPrintOptions _options;
|
||||||
|
|
||||||
|
public MailFetchService(ILogger<MailFetchService> logger, IOptions<MailPrintOptions> options)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_options = options.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<PrintJob>> FetchJobsForAccountAsync(MailAccount account, CancellationToken ct)
|
||||||
|
{
|
||||||
|
return account.Protocol.ToUpper() == "POP3"
|
||||||
|
? await FetchViaPop3Async(account, ct)
|
||||||
|
: await FetchViaImapAsync(account, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<PrintJob>> FetchViaImapAsync(MailAccount account, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var jobs = new List<PrintJob>();
|
||||||
|
using var client = new ImapClient();
|
||||||
|
await client.ConnectAsync(account.Host, account.Port, account.UseSsl, ct);
|
||||||
|
await client.AuthenticateAsync(account.Username, account.Password, ct);
|
||||||
|
|
||||||
|
var folder = await client.GetFolderAsync(account.Folder, ct);
|
||||||
|
await folder.OpenAsync(FolderAccess.ReadWrite, ct);
|
||||||
|
|
||||||
|
var uids = await folder.SearchAsync(SearchQuery.NotSeen, ct);
|
||||||
|
_logger.LogInformation("[{Account}] IMAP: {Count} ungelesene Nachrichten", account.Name, uids.Count);
|
||||||
|
|
||||||
|
foreach (var uid in uids)
|
||||||
|
{
|
||||||
|
if (ct.IsCancellationRequested) break;
|
||||||
|
var message = await folder.GetMessageAsync(uid, ct);
|
||||||
|
var extracted = ExtractJobs(message, account);
|
||||||
|
if (extracted.Count > 0)
|
||||||
|
{
|
||||||
|
jobs.AddRange(extracted);
|
||||||
|
if (_options.MarkAsRead)
|
||||||
|
await folder.AddFlagsAsync(uid, MessageFlags.Seen, true, ct);
|
||||||
|
if (_options.DeleteAfterPrint)
|
||||||
|
await folder.AddFlagsAsync(uid, MessageFlags.Deleted, true, ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_options.DeleteAfterPrint) await folder.ExpungeAsync(ct);
|
||||||
|
await client.DisconnectAsync(true, ct);
|
||||||
|
return jobs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<PrintJob>> FetchViaPop3Async(MailAccount account, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var jobs = new List<PrintJob>();
|
||||||
|
using var client = new Pop3Client();
|
||||||
|
await client.ConnectAsync(account.Host, account.Port, account.UseSsl, ct);
|
||||||
|
await client.AuthenticateAsync(account.Username, account.Password, ct);
|
||||||
|
|
||||||
|
int count = await client.GetMessageCountAsync(ct);
|
||||||
|
_logger.LogInformation("[{Account}] POP3: {Count} Nachrichten", account.Name, count);
|
||||||
|
|
||||||
|
var toDelete = new List<int>();
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
if (ct.IsCancellationRequested) break;
|
||||||
|
var message = await client.GetMessageAsync(i, ct);
|
||||||
|
var extracted = ExtractJobs(message, account);
|
||||||
|
if (extracted.Count > 0)
|
||||||
|
{
|
||||||
|
jobs.AddRange(extracted);
|
||||||
|
if (_options.DeleteAfterPrint) toDelete.Add(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var idx in toDelete) await client.DeleteMessageAsync(idx, ct);
|
||||||
|
await client.DisconnectAsync(true, ct);
|
||||||
|
return jobs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<PrintJob> ExtractJobs(MimeMessage message, MailAccount account)
|
||||||
|
{
|
||||||
|
var jobs = new List<PrintJob>();
|
||||||
|
|
||||||
|
// Profil für Filter-Lookup
|
||||||
|
var profile = _options.PrinterProfiles.FirstOrDefault(p =>
|
||||||
|
p.Name.Equals(account.PrinterProfileName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
var from = message.From.Mailboxes.Select(m => m.Address.ToLower()).ToList();
|
||||||
|
|
||||||
|
// Whitelist: Profil-Liste hat Vorrang, Fallback global
|
||||||
|
var allowed = profile?.AllowedSenders.Count > 0
|
||||||
|
? profile.AllowedSenders
|
||||||
|
: _options.AllowedSenders;
|
||||||
|
if (allowed.Count > 0 && !from.Any(f => allowed.Any(a => a.ToLower() == f)))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("[{Account}] Absender {From} nicht in Whitelist", account.Name, string.Join(",", from));
|
||||||
|
return jobs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blacklist: Profil-Liste hat Vorrang, Fallback global
|
||||||
|
var blocked = profile?.BlockedSenders.Count > 0
|
||||||
|
? profile.BlockedSenders
|
||||||
|
: _options.BlockedSenders;
|
||||||
|
if (blocked.Count > 0 && from.Any(f => blocked.Any(b => b.ToLower() == f)))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("[{Account}] Absender {From} in Blacklist", account.Name, string.Join(",", from));
|
||||||
|
return jobs;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(_options.SubjectFilter) &&
|
||||||
|
!message.Subject.Contains(_options.SubjectFilter, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return jobs;
|
||||||
|
|
||||||
|
Directory.CreateDirectory(_options.TempDirectory);
|
||||||
|
|
||||||
|
foreach (var attachment in message.Attachments.OfType<MimePart>())
|
||||||
|
{
|
||||||
|
var ext = Path.GetExtension(attachment.FileName ?? "").ToLower();
|
||||||
|
if (!_options.AllowedExtensions.Contains(ext)) continue;
|
||||||
|
|
||||||
|
var tempFile = Path.Combine(_options.TempDirectory, $"{Guid.NewGuid()}{ext}");
|
||||||
|
using (var stream = File.Create(tempFile))
|
||||||
|
attachment.Content.DecodeTo(stream);
|
||||||
|
|
||||||
|
jobs.Add(new PrintJob
|
||||||
|
{
|
||||||
|
FilePath = tempFile,
|
||||||
|
MailSubject = message.Subject,
|
||||||
|
MailFrom = message.From.ToString(),
|
||||||
|
PrinterProfileName = account.PrinterProfileName
|
||||||
|
});
|
||||||
|
|
||||||
|
_logger.LogInformation("[{Account}] Job: {File}", account.Name, tempFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record PrintJob
|
||||||
|
{
|
||||||
|
public string FilePath { get; init; } = "";
|
||||||
|
public string MailSubject { get; init; } = "";
|
||||||
|
public string MailFrom { get; init; } = "";
|
||||||
|
public string PrinterProfileName { get; init; } = "";
|
||||||
|
// Direkte Overrides für WebAPI
|
||||||
|
public string? PrinterOverride { get; init; }
|
||||||
|
public string? PaperSourceOverride { get; init; }
|
||||||
|
public int? CopiesOverride { get; init; }
|
||||||
|
}
|
||||||
132
MailPrint/Services/PrintService.cs
Normal file
132
MailPrint/Services/PrintService.cs
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Drawing.Printing;
|
||||||
|
|
||||||
|
namespace MailPrint.Services;
|
||||||
|
|
||||||
|
public class PrintService
|
||||||
|
{
|
||||||
|
private readonly ILogger<PrintService> _logger;
|
||||||
|
private readonly MailPrintOptions _options;
|
||||||
|
|
||||||
|
private static readonly string[] SumatraCandidates =
|
||||||
|
[
|
||||||
|
@"C:\Program Files\SumatraPDF\SumatraPDF.exe",
|
||||||
|
@"C:\Program Files (x86)\SumatraPDF\SumatraPDF.exe",
|
||||||
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "SumatraPDF", "SumatraPDF.exe"),
|
||||||
|
Path.Combine(AppContext.BaseDirectory, "SumatraPDF.exe"),
|
||||||
|
];
|
||||||
|
|
||||||
|
public PrintService(ILogger<PrintService> logger, IOptions<MailPrintOptions> options)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_options = options.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PrintPdf(PrintJob job)
|
||||||
|
{
|
||||||
|
// Profil auflösen: direkte Overrides > benanntes Profil > erstes Profil > Defaults
|
||||||
|
var profile = ResolveProfile(job);
|
||||||
|
var printerName = !string.IsNullOrEmpty(job.PrinterOverride) ? job.PrinterOverride : profile.PrinterName;
|
||||||
|
var paperSource = !string.IsNullOrEmpty(job.PaperSourceOverride) ? job.PaperSourceOverride : profile.PaperSource;
|
||||||
|
var copies = job.CopiesOverride ?? profile.Copies;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(printerName))
|
||||||
|
printerName = GetDefaultPrinter();
|
||||||
|
|
||||||
|
_logger.LogInformation("Drucke '{Subject}' → {Printer} | Fach: {Fach} | {Copies}x",
|
||||||
|
job.MailSubject, printerName, string.IsNullOrEmpty(paperSource) ? "Standard" : paperSource, copies);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (int i = 0; i < copies; i++)
|
||||||
|
PrintOnce(job.FilePath, printerName, paperSource, profile.Duplex);
|
||||||
|
_logger.LogInformation("Druck OK: {File}", job.FilePath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Druckfehler: {File}", job.FilePath);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
TryDelete(job.FilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private PrinterProfile ResolveProfile(PrintJob job)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(job.PrinterProfileName))
|
||||||
|
{
|
||||||
|
var p = _options.PrinterProfiles.FirstOrDefault(x =>
|
||||||
|
x.Name.Equals(job.PrinterProfileName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (p != null) return p;
|
||||||
|
}
|
||||||
|
return _options.PrinterProfiles.FirstOrDefault() ?? new PrinterProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PrintOnce(string pdfPath, string printerName, string paperSource, string duplex = "none")
|
||||||
|
{
|
||||||
|
var sumatra = ResolveSumatra();
|
||||||
|
if (sumatra != null)
|
||||||
|
{
|
||||||
|
var settings = BuildPrintSettings(paperSource, duplex);
|
||||||
|
RunAndWait(sumatra, $"-print-to \"{printerName}\" -print-settings \"{settings}\" -silent \"{pdfPath}\"");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback Shell-Print
|
||||||
|
_logger.LogWarning("Kein SumatraPDF – Shell-Print (kein Papierfach-Support)");
|
||||||
|
var psi = new ProcessStartInfo { FileName = pdfPath, Verb = "print", UseShellExecute = true, WindowStyle = ProcessWindowStyle.Hidden };
|
||||||
|
using var p = Process.Start(psi)!;
|
||||||
|
p.WaitForExit(30_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildPrintSettings(string paperSource, string duplex)
|
||||||
|
{
|
||||||
|
var parts = new List<string> { "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()
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(_options.SumatraPath) && File.Exists(_options.SumatraPath))
|
||||||
|
return _options.SumatraPath;
|
||||||
|
return Array.Find(SumatraCandidates, File.Exists);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RunAndWait(string exe, string args)
|
||||||
|
{
|
||||||
|
_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(300_000)) { p.Kill(); throw new TimeoutException($"Timeout: {exe}"); }
|
||||||
|
if (p.ExitCode != 0) _logger.LogWarning("ExitCode {Code}: {Exe}", p.ExitCode, exe);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<PrinterInfo> GetPrinterInfos()
|
||||||
|
{
|
||||||
|
var result = new List<PrinterInfo>();
|
||||||
|
foreach (string name in PrinterSettings.InstalledPrinters)
|
||||||
|
{
|
||||||
|
var ps = new PrinterSettings { PrinterName = name };
|
||||||
|
var sources = new List<string>();
|
||||||
|
foreach (PaperSource src in ps.PaperSources)
|
||||||
|
sources.Add(src.SourceName);
|
||||||
|
result.Add(new PrinterInfo(name, sources));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<string> GetAvailablePrinters() => GetPrinterInfos().Select(p => p.Name);
|
||||||
|
private static string GetDefaultPrinter() => new PrinterSettings().PrinterName;
|
||||||
|
private void TryDelete(string path)
|
||||||
|
{
|
||||||
|
try { if (File.Exists(path)) File.Delete(path); }
|
||||||
|
catch (Exception ex) { _logger.LogWarning(ex, "Temp nicht löschbar: {Path}", path); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record PrinterInfo(string Name, List<string> PaperSources);
|
||||||
17
MailPrintConfig/MailPrintConfig.csproj
Normal file
17
MailPrintConfig/MailPrintConfig.csproj
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0-windows</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
|
<RootNamespace>MailPrintConfig</RootNamespace>
|
||||||
|
<AssemblyName>MailPrintConfig</AssemblyName>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
900
MailPrintConfig/MainForm.cs
Normal file
900
MailPrintConfig/MainForm.cs
Normal file
|
|
@ -0,0 +1,900 @@
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using System.Drawing.Printing;
|
||||||
|
|
||||||
|
namespace MailPrintConfig;
|
||||||
|
|
||||||
|
public class MainForm : Form
|
||||||
|
{
|
||||||
|
private const int LabelW = 140;
|
||||||
|
private const int CtrlW = 240;
|
||||||
|
private const int RowH = 28;
|
||||||
|
private const int Pad = 12;
|
||||||
|
|
||||||
|
// ── Allgemein ─────────────────────────────────────────────────
|
||||||
|
private TextBox txtInterval = null!, txtSubjectFilter = null!, txtTempDir = null!, txtSumatraPath = null!;
|
||||||
|
private CheckBox chkDelete = null!, chkMarkRead = null!;
|
||||||
|
|
||||||
|
// ── Web API ───────────────────────────────────────────────────
|
||||||
|
private TextBox txtApiPort = null!, txtApiKey = null!;
|
||||||
|
private CheckBox chkBindAll = null!;
|
||||||
|
|
||||||
|
// ── Grids ─────────────────────────────────────────────────────
|
||||||
|
private DataGridView gridProfiles = null!, gridAccounts = null!, gridFolders = null!;
|
||||||
|
|
||||||
|
// ── Filter ────────────────────────────────────────────────────
|
||||||
|
private TextBox txtGlobalAllowed = null!, txtGlobalBlocked = null!;
|
||||||
|
|
||||||
|
// ── Config / Steuerung ────────────────────────────────────────
|
||||||
|
private TextBox txtConfigPath = 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!;
|
||||||
|
|
||||||
|
public MainForm()
|
||||||
|
{
|
||||||
|
Text = "MailPrint Konfiguration";
|
||||||
|
Width = 1100; MinimumSize = new Size(900, 700);
|
||||||
|
Font = new Font("Segoe UI", 9f);
|
||||||
|
StartPosition = FormStartPosition.CenterScreen;
|
||||||
|
|
||||||
|
BuildUI();
|
||||||
|
AutoDetectConfigPath();
|
||||||
|
|
||||||
|
Load += (_, _) => { if (File.Exists(txtConfigPath.Text)) LoadConfig(); };
|
||||||
|
|
||||||
|
_timer = new System.Windows.Forms.Timer { Interval = 1500 };
|
||||||
|
_timer.Tick += (_, _) => RefreshStartStop();
|
||||||
|
_timer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// UI
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
private void BuildUI()
|
||||||
|
{
|
||||||
|
var tabs = new TabControl { Dock = DockStyle.Fill };
|
||||||
|
tabs.TabPages.Add(BuildProfilesTab());
|
||||||
|
tabs.TabPages.Add(BuildAccountsTab());
|
||||||
|
tabs.TabPages.Add(BuildFoldersTab());
|
||||||
|
tabs.TabPages.Add(BuildFilterTab());
|
||||||
|
tabs.TabPages.Add(BuildApiTab());
|
||||||
|
tabs.TabPages.Add(BuildGeneralTab());
|
||||||
|
tabs.TabPages.Add(BuildAboutTab());
|
||||||
|
Controls.Add(tabs);
|
||||||
|
|
||||||
|
var bottom = new Panel { Dock = DockStyle.Bottom, Height = 84 };
|
||||||
|
int x = Pad;
|
||||||
|
|
||||||
|
// 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 };
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tab: Drucker-Profile ──────────────────────────────────────
|
||||||
|
private TabPage BuildProfilesTab()
|
||||||
|
{
|
||||||
|
var tab = new TabPage("Drucker-Profile");
|
||||||
|
tab.Controls.Add(new Label
|
||||||
|
{
|
||||||
|
Text = "Profil = Drucker + Papierfach + optionale Absender-Filter (überschreibt globale Filter).",
|
||||||
|
Left = Pad, Top = Pad, Width = 1060, Height = 18, ForeColor = Color.DimGray
|
||||||
|
});
|
||||||
|
|
||||||
|
gridProfiles = new DataGridView
|
||||||
|
{
|
||||||
|
Left = Pad, Top = 32, Width = 1060, Height = 500,
|
||||||
|
Anchor = AnchorStyles.Left | AnchorStyles.Top | AnchorStyles.Right | AnchorStyles.Bottom,
|
||||||
|
AllowUserToAddRows = true, AllowUserToDeleteRows = false,
|
||||||
|
AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.None,
|
||||||
|
ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize,
|
||||||
|
EditMode = DataGridViewEditMode.EditOnEnter,
|
||||||
|
SelectionMode = DataGridViewSelectionMode.FullRowSelect,
|
||||||
|
ScrollBars = ScrollBars.Both
|
||||||
|
};
|
||||||
|
|
||||||
|
gridProfiles.Columns.Add(new DataGridViewTextBoxColumn { Name = "PName", HeaderText = "Profil-Name", Width = 120 });
|
||||||
|
|
||||||
|
var colPrinter = new DataGridViewComboBoxColumn { Name = "Printer", HeaderText = "Drucker", FlatStyle = FlatStyle.Flat, Width = 280 };
|
||||||
|
colPrinter.Items.Add("");
|
||||||
|
foreach (string p in PrinterSettings.InstalledPrinters) colPrinter.Items.Add(p);
|
||||||
|
gridProfiles.Columns.Add(colPrinter);
|
||||||
|
|
||||||
|
var colSource = new DataGridViewComboBoxColumn { Name = "Source", HeaderText = "Papierfach", FlatStyle = FlatStyle.Flat, Width = 150 };
|
||||||
|
colSource.Items.Add("");
|
||||||
|
gridProfiles.Columns.Add(colSource);
|
||||||
|
|
||||||
|
gridProfiles.Columns.Add(new DataGridViewTextBoxColumn { Name = "Copies", HeaderText = "Kopien", Width = 55 });
|
||||||
|
|
||||||
|
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;
|
||||||
|
gridProfiles.CurrentCellDirtyStateChanged += (_, _) =>
|
||||||
|
{
|
||||||
|
if (gridProfiles.IsCurrentCellDirty) gridProfiles.CommitEdit(DataGridViewDataErrorContexts.Commit);
|
||||||
|
};
|
||||||
|
|
||||||
|
AttachContextMenu(gridProfiles);
|
||||||
|
tab.Controls.Add(gridProfiles);
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tab: Postfächer ───────────────────────────────────────────
|
||||||
|
private TabPage BuildAccountsTab()
|
||||||
|
{
|
||||||
|
var tab = new TabPage("Postfächer");
|
||||||
|
tab.Controls.Add(new Label
|
||||||
|
{
|
||||||
|
Text = "Jedes Postfach wird unabhängig abgerufen und druckt auf das zugeordnete Drucker-Profil.",
|
||||||
|
Left = Pad, Top = Pad, Width = 1060, Height = 18, ForeColor = Color.DimGray
|
||||||
|
});
|
||||||
|
|
||||||
|
gridAccounts = new DataGridView
|
||||||
|
{
|
||||||
|
Left = Pad, Top = 32, Width = 1060, Height = 500,
|
||||||
|
Anchor = AnchorStyles.Left | AnchorStyles.Top | AnchorStyles.Right | AnchorStyles.Bottom,
|
||||||
|
AllowUserToAddRows = true, AllowUserToDeleteRows = false,
|
||||||
|
AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.None,
|
||||||
|
ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize,
|
||||||
|
EditMode = DataGridViewEditMode.EditOnEnter,
|
||||||
|
SelectionMode = DataGridViewSelectionMode.FullRowSelect,
|
||||||
|
ScrollBars = ScrollBars.Both
|
||||||
|
};
|
||||||
|
|
||||||
|
gridAccounts.Columns.Add(new DataGridViewTextBoxColumn { Name = "AName", HeaderText = "Name", Width = 100 });
|
||||||
|
|
||||||
|
var colProto = new DataGridViewComboBoxColumn { Name = "Protocol", HeaderText = "Protokoll", FlatStyle = FlatStyle.Flat, Width = 70 };
|
||||||
|
colProto.Items.AddRange(["IMAP", "POP3"]);
|
||||||
|
gridAccounts.Columns.Add(colProto);
|
||||||
|
|
||||||
|
gridAccounts.Columns.Add(new DataGridViewTextBoxColumn { Name = "Host", HeaderText = "Host", Width = 200 });
|
||||||
|
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 });
|
||||||
|
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 };
|
||||||
|
colProfile.Items.Add("");
|
||||||
|
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);
|
||||||
|
tab.Controls.Add(gridAccounts);
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tab: Ordner-Überwachung ────────────────────────────────────
|
||||||
|
private TabPage BuildFoldersTab()
|
||||||
|
{
|
||||||
|
var tab = new TabPage("Ordner-Überwachung");
|
||||||
|
tab.Controls.Add(new Label
|
||||||
|
{
|
||||||
|
Text = "PDFs in diesen Ordnern werden automatisch erkannt und gedruckt.",
|
||||||
|
Left = Pad, Top = Pad, Width = 1060, Height = 18, ForeColor = Color.DimGray
|
||||||
|
});
|
||||||
|
|
||||||
|
gridFolders = new DataGridView
|
||||||
|
{
|
||||||
|
Left = Pad, Top = 32, Width = 1060, Height = 500,
|
||||||
|
Anchor = AnchorStyles.Left | AnchorStyles.Top | AnchorStyles.Right | AnchorStyles.Bottom,
|
||||||
|
AllowUserToAddRows = true, AllowUserToDeleteRows = false,
|
||||||
|
AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.None,
|
||||||
|
ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize,
|
||||||
|
EditMode = DataGridViewEditMode.EditOnEnter,
|
||||||
|
SelectionMode = DataGridViewSelectionMode.FullRowSelect,
|
||||||
|
ScrollBars = ScrollBars.Both
|
||||||
|
};
|
||||||
|
|
||||||
|
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 });
|
||||||
|
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 });
|
||||||
|
|
||||||
|
var colFProfile = new DataGridViewComboBoxColumn { Name = "FProfile", HeaderText = "Drucker-Profil", FlatStyle = FlatStyle.Flat, Width = 180 };
|
||||||
|
colFProfile.Items.Add("");
|
||||||
|
gridFolders.Columns.Add(colFProfile);
|
||||||
|
|
||||||
|
gridFolders.DataError += (_, e) => e.ThrowException = false;
|
||||||
|
gridFolders.CurrentCellDirtyStateChanged += (_, _) =>
|
||||||
|
{
|
||||||
|
if (gridFolders.IsCurrentCellDirty) gridFolders.CommitEdit(DataGridViewDataErrorContexts.Commit);
|
||||||
|
};
|
||||||
|
|
||||||
|
// "…"-Button → Ordner auswählen
|
||||||
|
gridFolders.CellClick += (_, e) =>
|
||||||
|
{
|
||||||
|
if (e.RowIndex < 0 || gridFolders.Columns[e.ColumnIndex].Name != "FBrowse") return;
|
||||||
|
var rowIndex = e.RowIndex;
|
||||||
|
BeginInvoke(() => BrowseFolder(rowIndex));
|
||||||
|
};
|
||||||
|
|
||||||
|
gridProfiles.CellValueChanged += (_, _) => RefreshProfileDropdowns();
|
||||||
|
AttachContextMenu(gridFolders);
|
||||||
|
tab.Controls.Add(gridFolders);
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tab: Filter ───────────────────────────────────────────────
|
||||||
|
private TabPage BuildFilterTab()
|
||||||
|
{
|
||||||
|
var tab = new TabPage("E-Mail-Filter (Global)");
|
||||||
|
int y = Pad;
|
||||||
|
|
||||||
|
tab.Controls.Add(new Label
|
||||||
|
{
|
||||||
|
Text = "Globale Listen gelten für alle Profile, sofern das Profil keine eigene Liste definiert.\r\n" +
|
||||||
|
"Format: eine E-Mail-Adresse pro Zeile. Leer = kein Filter.",
|
||||||
|
Left = Pad, Top = y, Width = 800, Height = 34, ForeColor = Color.DimGray
|
||||||
|
});
|
||||||
|
y += 40;
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
Left = Pad, Top = y, Width = 390, Height = 400,
|
||||||
|
Multiline = true, ScrollBars = ScrollBars.Vertical,
|
||||||
|
Anchor = AnchorStyles.Left | AnchorStyles.Top | AnchorStyles.Bottom
|
||||||
|
};
|
||||||
|
txtGlobalBlocked = new TextBox
|
||||||
|
{
|
||||||
|
Left = Pad + 420, Top = y, Width = 390, Height = 400,
|
||||||
|
Multiline = true, ScrollBars = ScrollBars.Vertical,
|
||||||
|
Anchor = AnchorStyles.Left | AnchorStyles.Top | AnchorStyles.Bottom
|
||||||
|
};
|
||||||
|
|
||||||
|
tab.Controls.Add(txtGlobalAllowed);
|
||||||
|
tab.Controls.Add(txtGlobalBlocked);
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tab: Web API ──────────────────────────────────────────────
|
||||||
|
private TabPage BuildApiTab()
|
||||||
|
{
|
||||||
|
var tab = new TabPage("Web API");
|
||||||
|
int y = Pad;
|
||||||
|
txtApiPort = AddText(tab, "Port", ref y, "5100");
|
||||||
|
chkBindAll = AddCheck(tab, "Alle Interfaces (0.0.0.0)", ref y, false);
|
||||||
|
txtApiKey = AddText(tab, "API-Key", ref y);
|
||||||
|
|
||||||
|
var btnGen = Btn("🔑 Generieren", LabelW + Pad, y, 130);
|
||||||
|
btnGen.Click += (_, _) => txtApiKey.Text = Guid.NewGuid().ToString("N");
|
||||||
|
tab.Controls.Add(btnGen); y += RowH + 4;
|
||||||
|
|
||||||
|
tab.Controls.Add(new Label
|
||||||
|
{
|
||||||
|
Text = "Header: X-Api-Key: <key> | Leer = kein Schutz\r\n\r\n" +
|
||||||
|
"POST /api/print/upload (multipart: file, printer, paperSource, copies)\r\n" +
|
||||||
|
"POST /api/print/url { url, printer, paperSource, copies }\r\n" +
|
||||||
|
"GET /api/print/printers\r\nGET /api/print/health\r\nGET /swagger",
|
||||||
|
Left = Pad, Top = y, Width = 700, Height = 110, ForeColor = Color.DimGray
|
||||||
|
});
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tab: Allgemein ────────────────────────────────────────────
|
||||||
|
private TabPage BuildGeneralTab()
|
||||||
|
{
|
||||||
|
var tab = new TabPage("Allgemein");
|
||||||
|
int y = Pad;
|
||||||
|
txtInterval = AddText(tab, "Intervall (Sek.)", ref y, "60");
|
||||||
|
txtSubjectFilter = AddText(tab, "Betreff-Filter", ref y);
|
||||||
|
chkDelete = AddCheck(tab, "Nach Druck löschen", ref y, true);
|
||||||
|
chkMarkRead = AddCheck(tab, "Als gelesen markieren", ref y, true);
|
||||||
|
txtSumatraPath = AddText(tab, "SumatraPDF Pfad", ref y);
|
||||||
|
|
||||||
|
var btnS = Btn("…", LabelW + Pad + CtrlW + 4, y - RowH - 2, 28);
|
||||||
|
btnS.Click += (_, _) =>
|
||||||
|
{
|
||||||
|
using var d = new OpenFileDialog { Filter = "SumatraPDF.exe|SumatraPDF.exe|Alle|*.*" };
|
||||||
|
if (d.ShowDialog() == DialogResult.OK) txtSumatraPath.Text = d.FileName;
|
||||||
|
};
|
||||||
|
tab.Controls.Add(btnS);
|
||||||
|
txtTempDir = AddText(tab, "Temp-Verzeichnis", ref y, Path.Combine(Path.GetTempPath(), "MailPrint"));
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tab: Über ─────────────────────────────────────────────────
|
||||||
|
private TabPage BuildAboutTab()
|
||||||
|
{
|
||||||
|
var tab = new TabPage("Über");
|
||||||
|
int y = Pad + 10;
|
||||||
|
|
||||||
|
void AddLine(string text, bool link = false, string url = "")
|
||||||
|
{
|
||||||
|
if (link)
|
||||||
|
{
|
||||||
|
var lbl = new LinkLabel
|
||||||
|
{
|
||||||
|
Text = text, Left = Pad, Top = y, AutoSize = true,
|
||||||
|
Font = new Font("Segoe UI", 10f)
|
||||||
|
};
|
||||||
|
lbl.LinkClicked += (_, _) => System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(url) { UseShellExecute = true });
|
||||||
|
tab.Controls.Add(lbl);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tab.Controls.Add(new Label
|
||||||
|
{
|
||||||
|
Text = text, Left = Pad, Top = y, AutoSize = true,
|
||||||
|
Font = new Font("Segoe UI", 10f)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
|
||||||
|
AddLine("Quellcode & Dokumentation:");
|
||||||
|
AddLine("https://www.dimedtec.net/dimedtec/MailPrint", link: true, url: "https://www.dimedtec.net/dimedtec/MailPrint");
|
||||||
|
y += 16;
|
||||||
|
|
||||||
|
AddLine("Verwendet:");
|
||||||
|
AddLine("SumatraPDF – freier PDF-Viewer (GPLv3)");
|
||||||
|
AddLine("https://www.sumatrapdfreader.org", link: true, url: "https://www.sumatrapdfreader.org");
|
||||||
|
y += 8;
|
||||||
|
AddLine("MailKit – IMAP/POP3 Bibliothek (MIT)");
|
||||||
|
AddLine("https://github.com/jstedfast/MailKit", link: true, url: "https://github.com/jstedfast/MailKit");
|
||||||
|
y += 8;
|
||||||
|
AddLine(".NET – Microsoft (MIT)");
|
||||||
|
AddLine("https://dotnet.microsoft.com", link: true, url: "https://dotnet.microsoft.com");
|
||||||
|
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Kontextmenü für Grids ──────────────────────────────────────
|
||||||
|
private static void AttachContextMenu(DataGridView grid)
|
||||||
|
{
|
||||||
|
var menu = new ContextMenuStrip();
|
||||||
|
var itemDelete = new ToolStripMenuItem("🗑 Zeile löschen");
|
||||||
|
itemDelete.Click += (_, _) =>
|
||||||
|
{
|
||||||
|
foreach (DataGridViewRow row in grid.SelectedRows)
|
||||||
|
if (!row.IsNewRow) grid.Rows.Remove(row);
|
||||||
|
};
|
||||||
|
menu.Items.Add(itemDelete);
|
||||||
|
|
||||||
|
grid.MouseDown += (_, e) =>
|
||||||
|
{
|
||||||
|
if (e.Button != MouseButtons.Right) return;
|
||||||
|
var hit = grid.HitTest(e.X, e.Y);
|
||||||
|
if (hit.RowIndex >= 0)
|
||||||
|
{
|
||||||
|
grid.ClearSelection();
|
||||||
|
grid.Rows[hit.RowIndex].Selected = true;
|
||||||
|
menu.Show(grid, e.Location);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// Papierfächer + Profil-Dropdown sync
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
private void GridProfiles_CellValueChanged(object? sender, DataGridViewCellEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.RowIndex < 0 || gridProfiles.Columns[e.ColumnIndex].Name != "Printer") return;
|
||||||
|
var printerName = gridProfiles.Rows[e.RowIndex].Cells["Printer"].Value?.ToString() ?? "";
|
||||||
|
var sc = (DataGridViewComboBoxCell)gridProfiles.Rows[e.RowIndex].Cells["Source"];
|
||||||
|
sc.Items.Clear(); sc.Items.Add("");
|
||||||
|
if (!string.IsNullOrEmpty(printerName))
|
||||||
|
{
|
||||||
|
var ps = new PrinterSettings { PrinterName = printerName };
|
||||||
|
foreach (PaperSource src in ps.PaperSources) sc.Items.Add(src.SourceName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshProfileDropdowns()
|
||||||
|
{
|
||||||
|
var names = gridProfiles.Rows.Cast<DataGridViewRow>()
|
||||||
|
.Where(r => !r.IsNewRow)
|
||||||
|
.Select(r => r.Cells["PName"].Value?.ToString() ?? "")
|
||||||
|
.Where(n => n.Length > 0).ToList();
|
||||||
|
|
||||||
|
// Postfächer
|
||||||
|
var colA = (DataGridViewComboBoxColumn)gridAccounts.Columns["Profile"];
|
||||||
|
colA.Items.Clear(); colA.Items.Add("");
|
||||||
|
foreach (var n in names) colA.Items.Add(n);
|
||||||
|
foreach (DataGridViewRow row in gridAccounts.Rows)
|
||||||
|
{
|
||||||
|
if (row.IsNewRow) continue;
|
||||||
|
var val = row.Cells["Profile"].Value?.ToString() ?? "";
|
||||||
|
if (!string.IsNullOrEmpty(val) && !colA.Items.Contains(val)) colA.Items.Add(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ordner-Überwachung
|
||||||
|
var colF = (DataGridViewComboBoxColumn)gridFolders.Columns["FProfile"];
|
||||||
|
colF.Items.Clear(); colF.Items.Add("");
|
||||||
|
foreach (var n in names) colF.Items.Add(n);
|
||||||
|
foreach (DataGridViewRow row in gridFolders.Rows)
|
||||||
|
{
|
||||||
|
if (row.IsNewRow) continue;
|
||||||
|
var val = row.Cells["FProfile"].Value?.ToString() ?? "";
|
||||||
|
if (!string.IsNullOrEmpty(val) && !colF.Items.Contains(val)) colF.Items.Add(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// Config laden / speichern
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
private void AutoDetectConfigPath()
|
||||||
|
{
|
||||||
|
var candidates = new[]
|
||||||
|
{
|
||||||
|
Path.Combine(AppContext.BaseDirectory, "appsettings.json"),
|
||||||
|
Path.Combine(AppContext.BaseDirectory, "..", "publish", "appsettings.json"),
|
||||||
|
@"C:\Services\MailPrint\appsettings.json",
|
||||||
|
};
|
||||||
|
txtConfigPath.Text = candidates.FirstOrDefault(File.Exists)
|
||||||
|
?? 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" };
|
||||||
|
if (d.ShowDialog() == DialogResult.OK) txtConfigPath.Text = d.FileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadConfig()
|
||||||
|
{
|
||||||
|
if (!File.Exists(txtConfigPath.Text))
|
||||||
|
{ SetStatus($"Nicht gefunden: {txtConfigPath.Text}", Color.Red); return; }
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var mp = (JObject.Parse(File.ReadAllText(txtConfigPath.Text))["MailPrint"] as JObject) ?? new JObject();
|
||||||
|
|
||||||
|
txtInterval.Text = mp["PollIntervalSeconds"]?.ToString() ?? "60";
|
||||||
|
txtSubjectFilter.Text = mp["SubjectFilter"]?.ToString() ?? "";
|
||||||
|
chkDelete.Checked = mp["DeleteAfterPrint"]?.Value<bool>() ?? true;
|
||||||
|
chkMarkRead.Checked = mp["MarkAsRead"]?.Value<bool>() ?? true;
|
||||||
|
txtSumatraPath.Text = mp["SumatraPath"]?.ToString() ?? "";
|
||||||
|
txtTempDir.Text = mp["TempDirectory"]?.ToString() ?? "";
|
||||||
|
|
||||||
|
txtGlobalAllowed.Text = string.Join(Environment.NewLine,
|
||||||
|
(mp["AllowedSenders"] as JArray ?? new JArray()).Select(t => t.ToString()));
|
||||||
|
txtGlobalBlocked.Text = string.Join(Environment.NewLine,
|
||||||
|
(mp["BlockedSenders"] as JArray ?? new JArray()).Select(t => t.ToString()));
|
||||||
|
|
||||||
|
var api = mp["WebApi"] as JObject ?? new JObject();
|
||||||
|
txtApiPort.Text = api["Port"]?.ToString() ?? "5100";
|
||||||
|
chkBindAll.Checked = api["BindAllInterfaces"]?.Value<bool>() ?? false;
|
||||||
|
txtApiKey.Text = api["ApiKey"]?.ToString() ?? "";
|
||||||
|
|
||||||
|
// Drucker-Profile
|
||||||
|
gridProfiles.Rows.Clear();
|
||||||
|
var printerCol = (DataGridViewComboBoxColumn)gridProfiles.Columns["Printer"];
|
||||||
|
foreach (var p in mp["PrinterProfiles"] as JArray ?? new JArray())
|
||||||
|
{
|
||||||
|
var printer = p["PrinterName"]?.ToString() ?? "";
|
||||||
|
var source = p["PaperSource"]?.ToString() ?? "";
|
||||||
|
var allowed = string.Join(", ", (p["AllowedSenders"] as JArray ?? new JArray()).Select(t => t.ToString()));
|
||||||
|
var blocked = string.Join(", ", (p["BlockedSenders"] as JArray ?? new JArray()).Select(t => t.ToString()));
|
||||||
|
|
||||||
|
if (!printerCol.Items.Contains(printer) && !string.IsNullOrEmpty(printer))
|
||||||
|
printerCol.Items.Add(printer);
|
||||||
|
|
||||||
|
int ri = gridProfiles.Rows.Add(
|
||||||
|
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("");
|
||||||
|
if (!string.IsNullOrEmpty(printer))
|
||||||
|
{
|
||||||
|
var ps = new PrinterSettings { PrinterName = printer };
|
||||||
|
foreach (PaperSource s in ps.PaperSources) sc.Items.Add(s.SourceName);
|
||||||
|
}
|
||||||
|
if (!sc.Items.Contains(source) && !string.IsNullOrEmpty(source)) sc.Items.Add(source);
|
||||||
|
sc.Value = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
RefreshProfileDropdowns();
|
||||||
|
|
||||||
|
// Postfächer
|
||||||
|
gridAccounts.Rows.Clear();
|
||||||
|
foreach (var a in mp["Accounts"] as JArray ?? new JArray())
|
||||||
|
{
|
||||||
|
var proto = a["Protocol"]?.ToString() ?? "IMAP";
|
||||||
|
var profName = a["PrinterProfileName"]?.ToString() ?? "";
|
||||||
|
|
||||||
|
var protoCol = (DataGridViewComboBoxColumn)gridAccounts.Columns["Protocol"];
|
||||||
|
if (!protoCol.Items.Contains(proto)) protoCol.Items.Add(proto);
|
||||||
|
|
||||||
|
var profCol = (DataGridViewComboBoxColumn)gridAccounts.Columns["Profile"];
|
||||||
|
if (!string.IsNullOrEmpty(profName) && !profCol.Items.Contains(profName))
|
||||||
|
profCol.Items.Add(profName);
|
||||||
|
|
||||||
|
gridAccounts.Rows.Add(
|
||||||
|
a["Name"]?.ToString() ?? "",
|
||||||
|
proto,
|
||||||
|
a["Host"]?.ToString() ?? "",
|
||||||
|
a["Port"]?.ToString() ?? "993",
|
||||||
|
a["UseSsl"]?.Value<bool>() ?? true,
|
||||||
|
a["Username"]?.ToString() ?? "",
|
||||||
|
a["Password"]?.ToString() ?? "",
|
||||||
|
a["Folder"]?.ToString() ?? "INBOX",
|
||||||
|
profName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ordner-Überwachung
|
||||||
|
gridFolders.Rows.Clear();
|
||||||
|
foreach (var f in mp["FolderWatchers"] as JArray ?? new JArray())
|
||||||
|
{
|
||||||
|
var profName = f["PrinterProfileName"]?.ToString() ?? "";
|
||||||
|
var profCol = (DataGridViewComboBoxColumn)gridFolders.Columns["FProfile"];
|
||||||
|
if (!string.IsNullOrEmpty(profName) && !profCol.Items.Contains(profName))
|
||||||
|
profCol.Items.Add(profName);
|
||||||
|
|
||||||
|
gridFolders.Rows.Add(
|
||||||
|
f["Name"]?.ToString() ?? "",
|
||||||
|
f["Path"]?.ToString() ?? "",
|
||||||
|
"…",
|
||||||
|
f["IncludeSubfolders"]?.Value<bool>() ?? false,
|
||||||
|
f["DeleteAfterPrint"]?.Value<bool>() ?? true,
|
||||||
|
profName);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetStatus($"Geladen: {txtConfigPath.Text}", Color.DarkGreen);
|
||||||
|
}
|
||||||
|
catch (Exception ex) { SetStatus($"Fehler: {ex.Message}", Color.Red); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveConfig()
|
||||||
|
{
|
||||||
|
gridProfiles.EndEdit();
|
||||||
|
gridAccounts.EndEdit();
|
||||||
|
gridFolders.EndEdit();
|
||||||
|
|
||||||
|
var path = txtConfigPath.Text;
|
||||||
|
var root = File.Exists(path) ? JObject.Parse(File.ReadAllText(path)) : new JObject();
|
||||||
|
|
||||||
|
var profiles = new JArray();
|
||||||
|
foreach (DataGridViewRow r in gridProfiles.Rows)
|
||||||
|
{
|
||||||
|
if (r.IsNewRow) continue;
|
||||||
|
profiles.Add(new JObject
|
||||||
|
{
|
||||||
|
["Name"] = r.Cells["PName"].Value?.ToString() ?? "",
|
||||||
|
["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())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var accounts = new JArray();
|
||||||
|
foreach (DataGridViewRow r in gridAccounts.Rows)
|
||||||
|
{
|
||||||
|
if (r.IsNewRow) continue;
|
||||||
|
accounts.Add(new JObject
|
||||||
|
{
|
||||||
|
["Name"] = r.Cells["AName"].Value?.ToString() ?? "",
|
||||||
|
["Protocol"] = r.Cells["Protocol"].Value?.ToString() ?? "IMAP",
|
||||||
|
["Host"] = r.Cells["Host"].Value?.ToString() ?? "",
|
||||||
|
["Port"] = int.TryParse(r.Cells["Port"].Value?.ToString(), out int p) ? p : 993,
|
||||||
|
["UseSsl"] = r.Cells["Ssl"].Value is true,
|
||||||
|
["Username"] = r.Cells["User"].Value?.ToString() ?? "",
|
||||||
|
["Password"] = r.Cells["Pass"].Value?.ToString() ?? "",
|
||||||
|
["Folder"] = r.Cells["Folder"].Value?.ToString() ?? "INBOX",
|
||||||
|
["PrinterProfileName"] = r.Cells["Profile"].Value?.ToString() ?? ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var folders = new JArray();
|
||||||
|
foreach (DataGridViewRow r in gridFolders.Rows)
|
||||||
|
{
|
||||||
|
if (r.IsNewRow) continue;
|
||||||
|
folders.Add(new JObject
|
||||||
|
{
|
||||||
|
["Name"] = r.Cells["FName"].Value?.ToString() ?? "",
|
||||||
|
["Path"] = r.Cells["FPath"].Value?.ToString() ?? "",
|
||||||
|
["IncludeSubfolders"] = r.Cells["FSubfolders"].Value is true,
|
||||||
|
["DeleteAfterPrint"] = r.Cells["FDelete"].Value is true,
|
||||||
|
["PrinterProfileName"] = r.Cells["FProfile"].Value?.ToString() ?? ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
root["MailPrint"] = new JObject
|
||||||
|
{
|
||||||
|
["PollIntervalSeconds"] = int.TryParse(txtInterval.Text, out int iv) ? iv : 60,
|
||||||
|
["SubjectFilter"] = txtSubjectFilter.Text,
|
||||||
|
["DeleteAfterPrint"] = chkDelete.Checked,
|
||||||
|
["MarkAsRead"] = chkMarkRead.Checked,
|
||||||
|
["SumatraPath"] = txtSumatraPath.Text,
|
||||||
|
["TempDirectory"] = txtTempDir.Text,
|
||||||
|
["AllowedExtensions"] = new JArray(".pdf"),
|
||||||
|
["AllowedSenders"] = ToJArray(txtGlobalAllowed.Text, multiline: true),
|
||||||
|
["BlockedSenders"] = ToJArray(txtGlobalBlocked.Text, multiline: true),
|
||||||
|
["PrinterProfiles"] = profiles,
|
||||||
|
["Accounts"] = accounts,
|
||||||
|
["FolderWatchers"] = folders,
|
||||||
|
["WebApi"] = new JObject
|
||||||
|
{
|
||||||
|
["Port"] = int.TryParse(txtApiPort.Text, out int ap) ? ap : 5100,
|
||||||
|
["BindAllInterfaces"] = chkBindAll.Checked,
|
||||||
|
["ApiKey"] = txtApiKey.Text
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (root["Serilog"] == null)
|
||||||
|
root["Serilog"] = JObject.Parse("{\"MinimumLevel\":{\"Default\":\"Information\",\"Override\":{\"Microsoft\":\"Warning\",\"Microsoft.AspNetCore\":\"Warning\"}}}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||||
|
File.WriteAllText(path, root.ToString(Formatting.Indented));
|
||||||
|
SetStatus($"Gespeichert: {path}", Color.DarkGreen);
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
var sep = multiline ? new[] { "\r\n", "\n" } : new[] { "," };
|
||||||
|
return new JArray(input.Split(sep, StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Select(s => s.Trim()).Where(s => s.Length > 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// EXE starten / stoppen
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
private async Task ToggleExeAsync()
|
||||||
|
{
|
||||||
|
btnStartStop.Enabled = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_proc is { HasExited: false })
|
||||||
|
{
|
||||||
|
_proc.Kill(entireProcessTree: true);
|
||||||
|
await _proc.WaitForExitAsync();
|
||||||
|
_proc = null;
|
||||||
|
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; }
|
||||||
|
|
||||||
|
_proc = new System.Diagnostics.Process
|
||||||
|
{
|
||||||
|
StartInfo = new System.Diagnostics.ProcessStartInfo(exePath)
|
||||||
|
{
|
||||||
|
UseShellExecute = true,
|
||||||
|
WorkingDirectory = Path.GetDirectoryName(exePath)!
|
||||||
|
},
|
||||||
|
EnableRaisingEvents = true
|
||||||
|
};
|
||||||
|
_proc.Exited += (_, _) => BeginInvoke(RefreshStartStop);
|
||||||
|
_proc.Start();
|
||||||
|
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 ? "⏹ EXE stoppen" : "▶ EXE starten";
|
||||||
|
btnStartStop.BackColor = running ? Color.LightCoral : Color.LightGreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
// Hilfsmethoden
|
||||||
|
// ══════════════════════════════════════════════════════════════
|
||||||
|
private void SetStatus(string msg, Color color)
|
||||||
|
{
|
||||||
|
if (InvokeRequired) { BeginInvoke(() => SetStatus(msg, color)); return; }
|
||||||
|
lblStatus.Text = msg; lblStatus.ForeColor = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Button Btn(string text, int x, int y, int w, Color? bg = null)
|
||||||
|
{
|
||||||
|
var b = new Button { Text = text, Left = x, Top = y, Width = w, Height = 26 };
|
||||||
|
if (bg.HasValue) b.BackColor = bg.Value;
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextBox AddText(Control p, string label, ref int y, string def = "")
|
||||||
|
{
|
||||||
|
AddLabel(p, label, y);
|
||||||
|
var tb = new TextBox { Left = LabelW + Pad, Top = y, Width = CtrlW, Text = def };
|
||||||
|
p.Controls.Add(tb); y += RowH; return tb;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CheckBox AddCheck(Control p, string label, ref int y, bool def)
|
||||||
|
{
|
||||||
|
var cb = new CheckBox { Text = label, Left = LabelW + Pad, Top = y, Width = CtrlW + 40, Checked = def };
|
||||||
|
p.Controls.Add(cb); y += RowH; return cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddLabel(Control p, string text, int y)
|
||||||
|
=> p.Controls.Add(new Label { Text = text + ":", Left = Pad, Top = y + 4, Width = LabelW, AutoSize = false });
|
||||||
|
|
||||||
|
protected override void OnFormClosing(FormClosingEventArgs e)
|
||||||
|
{
|
||||||
|
_timer.Stop();
|
||||||
|
base.OnFormClosing(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
5
MailPrintConfig/Program.cs
Normal file
5
MailPrintConfig/Program.cs
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
using MailPrintConfig;
|
||||||
|
|
||||||
|
Application.EnableVisualStyles();
|
||||||
|
Application.SetCompatibleTextRenderingDefault(false);
|
||||||
|
Application.Run(new MainForm());
|
||||||
130
README.md
Normal file
130
README.md
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
# MailPrint
|
||||||
|
|
||||||
|
**Windows-Dienst zum automatischen Drucken von PDF-Anhängen aus E-Mails, per REST API und per Ordner-Überwachung.**
|
||||||
|
|
||||||
|
Kostenlos und quelloffen. Version 1.0.0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 📧 **IMAP / POP3** – Postfächer werden automatisch abgerufen, PDF-Anhänge sofort gedruckt
|
||||||
|
- 📂 **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
|
||||||
|
- ↔️ **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
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
|
||||||
|
- Windows 10 / Server 2019 oder neuer
|
||||||
|
- [.NET 10 Runtime (Windows)](https://dotnet.microsoft.com/download/dotnet/10.0)
|
||||||
|
- [SumatraPDF](https://www.sumatrapdfreader.org/download-free-pdf-viewer) (wird automatisch erkannt wenn installiert oder neben der EXE)
|
||||||
|
- Installierter Windows-Drucker
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### 1. Release entpacken
|
||||||
|
|
||||||
|
Inhalt des `publish`-Ordners in ein Zielverzeichnis kopieren, z.B.:
|
||||||
|
```
|
||||||
|
C:\Services\MailPrint\
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Konfigurieren
|
||||||
|
|
||||||
|
`MailPrintConfig.exe` starten und konfigurieren:
|
||||||
|
|
||||||
|
- **Drucker-Profile** – Drucker + Papierfach + optionale Absender-Filter
|
||||||
|
- **Postfächer** – IMAP/POP3 Zugangsdaten + zugeordnetes Drucker-Profil
|
||||||
|
- **Web API** – Port, API-Key, FritzBox-Portmapping
|
||||||
|
- **Filter** – globale Whitelist/Blacklist
|
||||||
|
- **Allgemein** – Intervall, SumatraPDF-Pfad, Temp-Verzeichnis
|
||||||
|
|
||||||
|
Auf **Speichern** klicken.
|
||||||
|
|
||||||
|
### 3. Als Windows Service installieren
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Als Administrator ausführen:
|
||||||
|
.\install-service.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Deinstallieren
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Als Administrator ausführen:
|
||||||
|
.\uninstall-service.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Web API
|
||||||
|
|
||||||
|
| Methode | Endpunkt | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `POST` | `/api/print/upload` | PDF hochladen (multipart/form-data) |
|
||||||
|
| `POST` | `/api/print/url` | PDF-URL übergeben |
|
||||||
|
| `GET` | `/api/print/printers` | Drucker + Papierfächer auflesen |
|
||||||
|
| `GET` | `/api/print/health` | Healthcheck |
|
||||||
|
| `GET` | `/swagger` | Swagger UI |
|
||||||
|
|
||||||
|
### Authentifizierung
|
||||||
|
|
||||||
|
```http
|
||||||
|
X-Api-Key: <dein-api-key>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Beispiel Upload (curl)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:5100/api/print/upload \
|
||||||
|
-H "X-Api-Key: DEIN_KEY" \
|
||||||
|
-F "file=@rechnung.pdf" \
|
||||||
|
-F "printer=HP LaserJet" \
|
||||||
|
-F "paperSource=Fach 2" \
|
||||||
|
-F "copies=1"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Beispiel URL (PowerShell)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Invoke-RestMethod -Uri "http://localhost:5100/api/print/url" `
|
||||||
|
-Method Post `
|
||||||
|
-Headers @{"X-Api-Key" = "DEIN_KEY"; "Content-Type" = "application/json"} `
|
||||||
|
-Body '{"url":"https://example.com/rechnung.pdf","copies":1}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Papierfach-Steuerung
|
||||||
|
|
||||||
|
SumatraPDF wird automatisch gefunden wenn:
|
||||||
|
- Installiert unter `C:\Program Files\SumatraPDF\`
|
||||||
|
- Installiert unter `%LOCALAPPDATA%\SumatraPDF\`
|
||||||
|
- `SumatraPDF.exe` liegt neben `MailPrint.exe`
|
||||||
|
|
||||||
|
Ohne SumatraPDF: Fallback auf Windows Shell-Print (kein Papierfach-Support).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Logs
|
||||||
|
|
||||||
|
- Konsole (wenn als EXE gestartet)
|
||||||
|
- `logs\mailprint-YYYYMMDD.log` (neben der EXE)
|
||||||
|
- Windows EventLog unter „MailPrint" (wenn als Service)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lizenz
|
||||||
|
|
||||||
|
MIT
|
||||||
42
install-service.ps1
Normal file
42
install-service.ps1
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
#Requires -RunAsAdministrator
|
||||||
|
|
||||||
|
$ServiceName = "MailPrint"
|
||||||
|
$DisplayName = "MailPrint - E-Mail & WebAPI zu Drucker"
|
||||||
|
$Description = "Druckt PDF-Anhaenge automatisch aus E-Mails (IMAP/POP3) und per REST API auf Windows-Drucker."
|
||||||
|
$ExePath = Join-Path $PSScriptRoot "publish\MailPrint.exe"
|
||||||
|
|
||||||
|
if (-not (Test-Path $ExePath)) {
|
||||||
|
Write-Error "MailPrint.exe nicht gefunden: $ExePath"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$existing = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
|
||||||
|
if ($existing) {
|
||||||
|
Write-Host "Dienst '$ServiceName' ist bereits installiert. Stoppe zuerst..." -ForegroundColor Yellow
|
||||||
|
Stop-Service -Name $ServiceName -Force -ErrorAction SilentlyContinue
|
||||||
|
Start-Sleep -Seconds 2
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Installiere Dienst '$ServiceName'..." -ForegroundColor Cyan
|
||||||
|
|
||||||
|
New-Service `
|
||||||
|
-Name $ServiceName `
|
||||||
|
-DisplayName $DisplayName `
|
||||||
|
-Description $Description `
|
||||||
|
-BinaryPathName $ExePath `
|
||||||
|
-StartupType Automatic | Out-Null
|
||||||
|
|
||||||
|
sc.exe failure $ServiceName reset= 3600 actions= restart/5000/restart/10000/restart/30000 | Out-Null
|
||||||
|
|
||||||
|
Write-Host "Starte Dienst..." -ForegroundColor Cyan
|
||||||
|
Start-Service -Name $ServiceName
|
||||||
|
|
||||||
|
$svc = Get-Service -Name $ServiceName
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Ergebnis:" -ForegroundColor Green
|
||||||
|
Write-Host " Name: $($svc.Name)"
|
||||||
|
Write-Host " Status: $($svc.Status)"
|
||||||
|
Write-Host " Start: $($svc.StartType)"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Fertig. MailPrint laeuft jetzt als Windows-Dienst." -ForegroundColor Green
|
||||||
|
Write-Host "Logs: $(Join-Path $PSScriptRoot 'publish\logs')"
|
||||||
29
uninstall-service.ps1
Normal file
29
uninstall-service.ps1
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
#Requires -RunAsAdministrator
|
||||||
|
|
||||||
|
$ServiceName = "MailPrint"
|
||||||
|
|
||||||
|
$svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
|
||||||
|
if (-not $svc) {
|
||||||
|
Write-Host "Dienst '$ServiceName' ist nicht installiert." -ForegroundColor Yellow
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($svc.Status -ne "Stopped") {
|
||||||
|
Write-Host "Stoppe Dienst '$ServiceName'..." -ForegroundColor Cyan
|
||||||
|
Stop-Service -Name $ServiceName -Force
|
||||||
|
Start-Sleep -Seconds 3
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Entferne Dienst '$ServiceName'..." -ForegroundColor Cyan
|
||||||
|
sc.exe delete $ServiceName | Out-Null
|
||||||
|
|
||||||
|
Start-Sleep -Seconds 2
|
||||||
|
|
||||||
|
$check = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
|
||||||
|
if ($check) {
|
||||||
|
Write-Error "Dienst konnte nicht entfernt werden. Bitte manuell pruefen."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Dienst '$ServiceName' erfolgreich deinstalliert." -ForegroundColor Green
|
||||||
Loading…
Add table
Add a link
Reference in a new issue