diff --git a/Elwig/Documents/Document.cs b/Elwig/Documents/Document.cs index edae367..41ba537 100644 --- a/Elwig/Documents/Document.cs +++ b/Elwig/Documents/Document.cs @@ -5,6 +5,7 @@ using Elwig.Helpers; using System.Collections.Generic; using System.Linq; using Elwig.Helpers.Printing; +using MimeKit; namespace Elwig.Documents { public abstract partial class Document : IDisposable { @@ -150,6 +151,16 @@ namespace Elwig.Documents { Pdf.Show(_pdfFile.NewReference(), Title + (this is BusinessDocument b ? $" - {b.Member.Name}" : "")); } + public MimePart AsEmailAttachment(string filename) { + if (PdfPath == null) throw new InvalidOperationException("Pdf file has not been generated yet"); + return new("application", "pdf") { + Content = new MimeContent(File.OpenRead(PdfPath)), + ContentDisposition = new ContentDisposition(ContentDisposition.Attachment), + ContentTransferEncoding = ContentEncoding.Base64, + FileName = filename + }; + } + private class MergedDocument(IEnumerable docs) : Document("Mehrere Dokumente") { public IEnumerable Documents = docs; } diff --git a/Elwig/Elwig.csproj b/Elwig/Elwig.csproj index d478427..c5c2334 100644 --- a/Elwig/Elwig.csproj +++ b/Elwig/Elwig.csproj @@ -27,6 +27,7 @@ + diff --git a/Elwig/Helpers/Config.cs b/Elwig/Helpers/Config.cs index d8d2da2..172ff7f 100644 --- a/Elwig/Helpers/Config.cs +++ b/Elwig/Helpers/Config.cs @@ -42,6 +42,16 @@ namespace Elwig.Helpers { public string? UpdateUrl = null; public bool UpdateAuto = false; + public string? SmtpHost = null; + public int? SmtpPort = null; + public string? SmtpMode = null; + public string? SmtpUsername = null; + public string? SmtpPassword = null; + public string? SmtpFrom = null; + public (string Host, int Port, string Mode, string Username, string Password, string From)? Smtp => + SmtpHost == null || SmtpPort == null || SmtpMode == null || SmtpUsername == null || SmtpPassword == null || SmtpFrom == null ? + null : (SmtpHost, (int)SmtpPort, SmtpMode, SmtpUsername, SmtpPassword, SmtpFrom); + public IList Scales; private readonly List ScaleList = []; @@ -62,6 +72,13 @@ namespace Elwig.Helpers { UpdateUrl = config["update:url"]; UpdateAuto = TrueValues.Contains(config["update:auto"]?.ToLower()); + SmtpHost = config["smtp:host"]; + SmtpPort = config["smtp:port"]?.All(char.IsAsciiDigit) == true && config["smtp:port"]?.Length > 0 ? int.Parse(config["smtp:port"]!) : null; + SmtpMode = config["smtp:mode"]; + SmtpUsername = config["smtp:username"]; + SmtpPassword = config["smtp:password"]; + SmtpFrom = config["smtp:from"]; + var scales = config.AsEnumerable().Where(i => i.Key.StartsWith("scale.")).GroupBy(i => i.Key.Split(':')[0][6..]).Select(i => i.Key); ScaleList.Clear(); Scales = ScaleList; @@ -72,25 +89,5 @@ namespace Elwig.Helpers { )); } } - - public void Write() { - using var file = new StreamWriter(FileName, false, Utils.UTF8); - file.Write($"\r\n[general]\r\n"); - if (Branch != null) file.Write($"branch = {Branch}\r\n"); - if (Debug) file.Write("debug = true\r\n"); - file.Write($"\r\n[database]\r\nfile = {DatabaseFile}\r\n"); - if (DatabaseLog != null) file.Write($"log = {DatabaseLog}\r\n"); - file.Write($"\r\n[update]\r\n"); - if (UpdateUrl != null) file.Write($"url = {UpdateUrl}\r\n"); - if (UpdateAuto) file.Write($"auto = true\r\n"); - - foreach (var s in ScaleList) { - file.Write($"\r\n[scale.{s.Id}]\r\ntype = {s.Type}\r\nmodel = {s.Model}\r\nconnection = {s.Connection}\r\n"); - if (s.Empty != null) file.Write($"empty = {s.Empty}\r\n"); - if (s.Filling != null) file.Write($"filling = {s.Filling}\r\n"); - if (s.Limit != null) file.Write($"limit = {s.Limit}\r\n"); - if (s._Log != null) file.Write($"log = {s._Log}\r\n"); - } - } } } diff --git a/Elwig/Helpers/Utils.cs b/Elwig/Helpers/Utils.cs index 8af90a9..2a14813 100644 --- a/Elwig/Helpers/Utils.cs +++ b/Elwig/Helpers/Utils.cs @@ -16,6 +16,9 @@ using System.Runtime.InteropServices; using System.Net.Http; using System.Text.Json.Nodes; using System.IO; +using MailKit.Net.Smtp; +using MailKit.Security; +using OpenTK.Compute.OpenCL; namespace Elwig.Helpers { public static partial class Utils { @@ -27,30 +30,33 @@ namespace Elwig.Helpers { public static int CurrentLastSeason => DateTime.Now.Year - (DateTime.Now.Month <= 7 ? 1 : 0); public static DateTime Today => (DateTime.Now.Hour >= 3) ? DateTime.Today : DateTime.Today.AddDays(-1); - public static readonly Regex SerialRegex = GeneratedSerialRegex(); - public static readonly Regex TcpRegex = GeneratedTcpRegex(); - public static readonly Regex DateFromToRegex = GeneratedFromToDateRegex(); - public static readonly Regex FromToRegex = GeneratedFromToRegex(); - public static readonly Regex FromToTimeRegex = GeneratedFromToTimeRegex(); - public static readonly Regex AddressRegex = GeneratedAddressRegex(); - [GeneratedRegex("^serial://([A-Za-z0-9]+):([0-9]+)(,([5-9]),([NOEMSnoems]),(0|1|1\\.5|2|))?$", RegexOptions.Compiled)] private static partial Regex GeneratedSerialRegex(); + public static readonly Regex SerialRegex = GeneratedSerialRegex(); [GeneratedRegex("^tcp://([A-Za-z0-9._-]+):([0-9]+)$", RegexOptions.Compiled)] private static partial Regex GeneratedTcpRegex(); + public static readonly Regex TcpRegex = GeneratedTcpRegex(); [GeneratedRegex(@"^(-?(0?[1-9]|[12][0-9]|3[01])\.(0?[1-9]|1[0-2])\.([0-9]{4})?-?){1,2}$", RegexOptions.Compiled)] private static partial Regex GeneratedFromToDateRegex(); + public static readonly Regex DateFromToRegex = GeneratedFromToDateRegex(); [GeneratedRegex(@"^([0-9]+([\.,][0-9]+)?)?-([0-9]+([\.,][0-9]+)?)?$", RegexOptions.Compiled)] private static partial Regex GeneratedFromToRegex(); + public static readonly Regex FromToRegex = GeneratedFromToRegex(); [GeneratedRegex(@"^([0-9]{1,2}:[0-9]{2})?-([0-9]{1,2}:[0-9]{2})?$", RegexOptions.Compiled)] private static partial Regex GeneratedFromToTimeRegex(); + public static readonly Regex FromToTimeRegex = GeneratedFromToTimeRegex(); [GeneratedRegex(@"^(.*?) +([0-9].*)$", RegexOptions.Compiled)] private static partial Regex GeneratedAddressRegex(); + public static readonly Regex AddressRegex = GeneratedAddressRegex(); + + [GeneratedRegex(@"[^A-Za-z0-9ÄÜÖẞäöüß-]+")] + private static partial Regex GeneratedInvalidFileNamePartsRegex(); + public static readonly Regex InvalidFileNamePartsRegex = GeneratedInvalidFileNamePartsRegex(); public static readonly string GroupSeparator = "\u202F"; public static readonly string UnitSeparator = "\u00A0"; @@ -412,5 +418,19 @@ namespace Elwig.Helpers { file.Delete(); } } + + public static string NormalizeFileName(string filename) { + return InvalidFileNamePartsRegex.Replace(filename.Replace('/', '-'), "_"); + } + + public static async Task GetSmtpClient() { + if (App.Config.Smtp == null) + return null; + var (host, port, mode, username, password, _) = App.Config.Smtp.Value; + var client = new SmtpClient(); + await client.ConnectAsync(host, port, mode == "starttls" ? SecureSocketOptions.StartTls : SecureSocketOptions.None); + await client.AuthenticateAsync(username, password); + return client; + } } } diff --git a/Elwig/Windows/MailWindow.xaml.cs b/Elwig/Windows/MailWindow.xaml.cs index 65e5057..2d10942 100644 --- a/Elwig/Windows/MailWindow.xaml.cs +++ b/Elwig/Windows/MailWindow.xaml.cs @@ -3,15 +3,16 @@ using Elwig.Helpers; using Elwig.Helpers.Billing; using Elwig.Models.Dtos; using Elwig.Models.Entities; +using MailKit.Net.Smtp; using Microsoft.EntityFrameworkCore; using Microsoft.Win32; +using MimeKit; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; using System.Linq; -using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; @@ -543,8 +544,8 @@ namespace Elwig.Windows { GenerateButton.IsEnabled = true; Mouse.OverrideCursor = null; PreviewButton.IsEnabled = true; - PrintButton.IsEnabled = true; - //EmailButton.IsEnabled = true; + PrintButton.IsEnabled = PrintDocument != null; + EmailButton.IsEnabled = EmailDocuments != null && App.Config.Smtp != null; } private void PreviewButton_Click(object sender, RoutedEventArgs evt) { @@ -560,7 +561,7 @@ namespace Elwig.Windows { Directory.CreateDirectory(folder); foreach (var item in docs.Select((d, i) => new { Index = i, Doc = d })) { var doc = item.Doc; - var name = Regex.Replace(doc.Title.Replace('/', '-'), @"[^A-Za-z0-9ÄÜÖẞäöüß-]+", "_"); + var name = Utils.NormalizeFileName(doc.Title); doc.SaveTo($"{folder}/{item.Index + 1:00}.{name}.pdf"); } @@ -573,20 +574,65 @@ namespace Elwig.Windows { private async void PrintButton_Click(object sender, RoutedEventArgs evt) { if (PrintDocument == null) return; + PrintButton.IsEnabled = false; var res = MessageBox.Show($"Sollen {PrintDocument.Pages} Blätter ({PrintDocument.TotalPages} Seiten) gedruckt werden?\n" + $"Sind die \"Duplex-Einstellungen\" des Standarddruckers entsprechend eingestellt (doppelseitig bzw. einseitig)?", "Rundschreiben drucken", MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No); if (res == MessageBoxResult.Yes) { + Mouse.OverrideCursor = Cursors.AppStarting; if (App.Config.Debug) { PrintDocument.Show(); } else { await PrintDocument.Print(); } + Mouse.OverrideCursor = null; } + PrintButton.IsEnabled = true; } - private void EmailButton_Click(object sender, RoutedEventArgs evt) { - // TODO + private async void EmailButton_Click(object sender, RoutedEventArgs evt) { + if (App.Config.Smtp == null || EmailDocuments == null) return; + + EmailButton.IsEnabled = false; + SmtpClient? client = null; + try { + Mouse.OverrideCursor = Cursors.AppStarting; + client = await Utils.GetSmtpClient(); + Mouse.OverrideCursor = null; + + var res = MessageBox.Show($"Sollen {EmailDocuments.Count} E-Mails verschickt werden?", + "Rundschreiben verschicken", MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No); + if (res != MessageBoxResult.Yes) { + return; + } + + Mouse.OverrideCursor = Cursors.AppStarting; + var subject = EmailSubjectInput.Text; + var text = EmailBodyInput.Text; + foreach (var (m, docs) in EmailDocuments) { + using var msg = new MimeMessage(); + msg.From.Add(new MailboxAddress(App.Client.NameFull, App.Config.Smtp.Value.From)); + msg.To.AddRange(m.EmailAddresses.OrderBy(a => a.Nr).Select(a => new MailboxAddress(m.AdministrativeName, a.Address))); + msg.Subject = subject; + var body = new Multipart("mixed") { + new TextPart("plain") { Text = text } + }; + foreach (var doc in docs) { + var name = Utils.NormalizeFileName(doc.Title); + body.Add(doc.AsEmailAttachment($"{name}.pdf")); + } + msg.Body = body; + await client!.SendAsync(msg); + } + } catch (Exception exc) { + MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error); + } finally { + if (client != null) + await client.DisconnectAsync(true); + client?.Dispose(); + EmailButton.IsEnabled = true; + Mouse.OverrideCursor = null; + } } public void AddDeliveryConfirmation() { diff --git a/Elwig/Windows/MainWindow.xaml b/Elwig/Windows/MainWindow.xaml index 5a86423..fb6b3cc 100644 --- a/Elwig/Windows/MainWindow.xaml +++ b/Elwig/Windows/MainWindow.xaml @@ -21,8 +21,9 @@ - - + + + diff --git a/Elwig/Windows/MainWindow.xaml.cs b/Elwig/Windows/MainWindow.xaml.cs index d5caa51..6fff62c 100644 --- a/Elwig/Windows/MainWindow.xaml.cs +++ b/Elwig/Windows/MainWindow.xaml.cs @@ -1,6 +1,9 @@ +using Elwig.Helpers; +using System; using System.ComponentModel; using System.Reflection; using System.Windows; +using System.Windows.Input; namespace Elwig.Windows { public partial class MainWindow : Window { @@ -11,10 +14,11 @@ namespace Elwig.Windows { VersionField.Text = "Version: " + (v == null ? "?" : $"{v.Major}.{v.Minor}.{v.Build}") + $" – {App.BranchName}"; if (App.Client.Client == null) VersionField.Text += " (Unbekannt)"; if (!App.Config.Debug) { - HelpMenu.Items.Remove(TestWindowButton); + HelpMenu.Items.Remove(Menu_Help_TestWindow); //QueryWindowButton.Visibility = Visibility.Hidden; } - if (App.Config.UpdateUrl == null) CheckForUpdatesButton.IsEnabled = false; + if (App.Config.UpdateUrl == null) Menu_Help_Update.IsEnabled = false; + if (App.Config.Smtp == null) Menu_Help_Smtp.IsEnabled = false; } private void Window_Loaded(object sender, RoutedEventArgs evt) { } @@ -40,6 +44,18 @@ namespace Elwig.Windows { await App.CheckForUpdates(); } + private async void Menu_Help_Smtp_Click(object sender, RoutedEventArgs evt) { + Mouse.OverrideCursor = Cursors.AppStarting; + try { + using var client = await Utils.GetSmtpClient(); + await client!.DisconnectAsync(true); + MessageBox.Show("E-Mail-Einstellungen erfolgreich überprüft!", "Erfolg", MessageBoxButton.OK, MessageBoxImage.Information); + } catch (Exception exc) { + MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error); + } + Mouse.OverrideCursor = null; + } + private void Menu_Database_Query_Click(object sender, RoutedEventArgs evt) { var w = new QueryWindow(); w.Show(); diff --git a/Installer/Files/config.ini b/Installer/Files/config.ini index 7745905..75ffd96 100644 --- a/Installer/Files/config.ini +++ b/Installer/Files/config.ini @@ -14,6 +14,14 @@ file = database.sqlite3 url = https://www.necronda.net/elwig/files/elwig/latest?format=json auto = true +[smtp] +;host = +;port = +;mode = starttls +;username = "" +;password = "" +;from = mail@wg.at + ;[scale.1] ;type = SysTec-IT ;model = IT3000