diff --git a/.github/instructions/New Text Document.txt b/.github/instructions/New Text Document.txt new file mode 100644 index 0000000..e48b7ea --- /dev/null +++ b/.github/instructions/New Text Document.txt @@ -0,0 +1 @@ +- \ No newline at end of file diff --git a/Fuchs/Controllers/IntranetController.Reminder.cs b/Fuchs/Controllers/IntranetController.Reminder.cs index cf322c4..7ecaafe 100644 --- a/Fuchs/Controllers/IntranetController.Reminder.cs +++ b/Fuchs/Controllers/IntranetController.Reminder.cs @@ -1,4 +1,5 @@ using Fuchs.intranet; +using Fuchs.Services; using Microsoft.AspNetCore.Mvc; using Microsoft.Data.SqlClient; using Newtonsoft.Json; @@ -117,11 +118,11 @@ public partial class IntranetController frdic.no("InvoiceFile", null!) is byte[] invFile) remdoc[frdic.nz("InvoiceFileName")] = invFile; - bool sent = await FuchsFdsEmail.SendEmail( + bool sent = await _comService.SendEmailAsync( $"inv_{remId}", $"SanitärFuchs - {frdic.nz("subject").ne(frdic.nz("DocumentName"))}", BuildReminderBody(Convert.ToDouble(frdic.no("amount_open", 0))), - email.Trim(), "", remdoc, _intranet); + email.Trim(), "", remdoc); if (sent) { var pls = StdParamlist(SQL_VarChar("@Id", remId), SQL_Bit("@auto", true)); @@ -175,10 +176,10 @@ public partial class IntranetController if (!string.IsNullOrEmpty(frdic.nz("InvoiceFileName")) && frdic.no("InvoiceFile", null!) is byte[] invFile) remdoc[frdic.nz("InvoiceFileName")] = invFile; - await FuchsFdsEmail.SendEmail($"rem_{remId}", + await _comService.SendEmailAsync($"rem_{remId}", $"SanitärFuchs - {frdic.nz("subject").ne(frdic.nz("DocumentName"))}", BuildReminderBody(Convert.ToDouble(frdic.no("amount_open", 0))), - email.Trim(), "", remdoc, _intranet); + email.Trim(), "", remdoc); } return Ok(); } diff --git a/Fuchs/Controllers/IntranetController.Requests.cs b/Fuchs/Controllers/IntranetController.Requests.cs index a3d1530..f8f7539 100644 --- a/Fuchs/Controllers/IntranetController.Requests.cs +++ b/Fuchs/Controllers/IntranetController.Requests.cs @@ -1,5 +1,6 @@ using System.Globalization; using Fuchs.intranet; +using Fuchs.Services; using Microsoft.AspNetCore.Mvc; using Microsoft.Data.SqlClient; using MFR_RESTClient.generic; @@ -273,9 +274,9 @@ public partial class IntranetController double bal = Convert.ToDouble(frdic.no("InvoiceBalance", 0)); string terms = fdInv.PaymentTerms.Replace("wd", " Werktagen").Replace("d", " Tagen").Replace("wk", " Wochen").ne("10 Tagen"); string body = BuildInvoiceBody(bal, terms); - bool sent = await FuchsFdsEmail.SendEmail( + bool sent = await _comService.SendEmailAsync( $"inv_{invId}", $"Sanit\u00e4rFuchs - {frdic.nz("DocumentName")}", - body, email.Trim(), "", inv, _intranet); + body, email.Trim(), "", inv); if (sent) { var pls = StdParamlist(SQL_VarChar("@Id", invId), SQL_Bit("@auto", true)); @@ -328,11 +329,10 @@ public partial class IntranetController { double bal = Convert.ToDouble(frdic.no("InvoiceBalance", 0)); string terms = fdInv.PaymentTerms.Replace("wd", " Werktagen").Replace("d", " Tagen").Replace("wk", " Wochen").ne("10 Tagen"); - await FuchsFdsEmail.SendEmail( + await _comService.SendEmailAsync( $"inv_{invId}", $"Sanit\u00e4rFuchs - {frdic.nz("DocumentName")}", BuildInvoiceBody(bal, terms), email.Trim(), "", - new Dictionary { [frdic.nz("DocumentName")] = filebyte }, - _intranet); + new Dictionary { [frdic.nz("DocumentName")] = filebyte }); } return Ok(); } diff --git a/Fuchs/Controllers/IntranetController.cs b/Fuchs/Controllers/IntranetController.cs index d5472d6..86967fa 100644 --- a/Fuchs/Controllers/IntranetController.cs +++ b/Fuchs/Controllers/IntranetController.cs @@ -1,5 +1,6 @@ using System.Web; using Fuchs.intranet; +using Fuchs.Services; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -24,6 +25,7 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller internal readonly Fuchs_intranet _intranet; internal readonly fds.IFdsMfr _mfr; private readonly ILogger _logger; + private readonly IComService _comService; private readonly List _allowedNonAuth = new() { "spwc", "spw" }; private readonly List _allowedGet = new() { @@ -39,11 +41,12 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller public string UserAccountID => UserIdent.UserAccountId; public string AuthAccount => UserIdent.Email; - public IntranetController(Fuchs_intranet intranet, fds.IFdsMfr mfr, ILogger logger) + public IntranetController(Fuchs_intranet intranet, fds.IFdsMfr mfr, ILogger logger, IComService comService) { _intranet = intranet; _mfr = mfr; _logger = logger; + _comService = comService; } // ── Standard param list (pre-populates @authuser) ──────────────────────── @@ -203,12 +206,9 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller row.nz("name").ToLower().Trim() == lastname.ToLower().Trim() && row.nz("mobile").Length > 5 && !Request.Host.Host.ToLower().Contains("localhost")) { - OCORE.sms.SMS77.Settings.APIKey = _intranet.Intranet__SMS_API_key; - using var sms = new OCORE.sms.SMS77.SMS("ProcessWeb"); string totp = OCORE.security.TFA.generateTotp_12h(_intranet.Intranet__TOTPsharedsecret_base); - if (long.TryParse(row.nz("mobile").Replace("+", "00").Replace(" ", ""), out long smsNum)) - sms.SendSMS_sync(smsNum, - "Zur Bestätigung des Passwortversands auf sanitarfuchs.de, verwenden Sie bitte folgenden Code:" + totp); + await _comService.SendSmsAsync(row.nz("mobile"), + "Zur Bestätigung des Passwortversands auf sanitarfuchs.de, verwenden Sie bitte folgenden Code:" + totp); } return Ok(); // always OK to prevent enumeration } @@ -226,10 +226,10 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller var row = await _intranet.GetUserAccountByEmailAsync(email, includePassword: true); if (row != null && row.nz("email").Length > 5) { - await FuchsFdsEmail.SendEmail("pw_" + row.nz("email"), + await _comService.SendEmailAsync("pw_" + row.nz("email"), "sanitaerfuchs.de Intranet Passwort", $"

Guten Tag {row.nz("firstname")} {row.nz("name")},
Ihr Passwort: {HttpUtility.HtmlEncode(row.nz("password"))}

", - row.nz("email"), $"{row.nz("firstname")} {row.nz("name")}", null, _intranet); + row.nz("email"), $"{row.nz("firstname")} {row.nz("name")}", null); } } return Ok(); @@ -243,11 +243,9 @@ public partial class IntranetController : Microsoft.AspNetCore.Mvc.Controller var row = await _intranet.GetUserAccountByEmailAsync(UserIdent.Email, includePassword: true); if (row != null && row.nz("mobile").Length > 5 && !Request.Host.Host.Contains("localhost")) { - OCORE.sms.SMS77.Settings.APIKey = _intranet.Intranet__SMS_API_key; - using var sms2 = new OCORE.sms.SMS77.SMS("ProcessWeb"); string totp2 = OCORE.security.TFA.generateTotp_3h(_intranet.Intranet__TOTPsharedsecret_base + "3MDR"); - if (long.TryParse(row.nz("mobile").Replace("+", "00").Replace(" ", ""), out long mob2)) - sms2.SendSMS_sync(mob2, "Zur Bestätigung der Passwortänderung auf sanitarfuchs.de: " + totp2); + await _comService.SendSmsAsync(row.nz("mobile"), + "Zur Bestätigung der Passwortänderung auf sanitarfuchs.de: " + totp2); } return Ok(); diff --git a/Fuchs/Program.cs b/Fuchs/Program.cs index 5899e61..7fcfe62 100644 --- a/Fuchs/Program.cs +++ b/Fuchs/Program.cs @@ -71,9 +71,10 @@ public class Program builder.Services.AddAuthorization(); builder.Services.AddHttpContextAccessor(); - // Email service - builder.Services.Configure(builder.Configuration.GetSection("Fuchs:Email")); - builder.Services.AddScoped(); + // Communication service (email + SMS via ProcessWeb Mailer API) + builder.Services.Configure(builder.Configuration.GetSection("Fuchs:Mailer")); + builder.Services.AddHttpClient("ProcessWebMailer"); + builder.Services.AddScoped(); } private static void ConfigureApp(WebApplication app) diff --git a/Fuchs/Services/FuchsEmailService.cs b/Fuchs/Services/FuchsEmailService.cs deleted file mode 100644 index 305fa6c..0000000 --- a/Fuchs/Services/FuchsEmailService.cs +++ /dev/null @@ -1,197 +0,0 @@ -using Fuchs.intranet; -using Microsoft.Data.SqlClient; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using MimeKit; -using Newtonsoft.Json; -using OCORE.SQL; -using static OCORE.SQL.sql; - -namespace Fuchs.Services; - -/// -/// Email service implementation. Replaces the static FuchsFdsEmail class. -/// Reads settings from IOptions<FuchsEmailSettings> (appsettings.json) -/// instead of System.Configuration.ConfigurationManager. -/// -public class FuchsEmailService : IEmailService -{ - private readonly ILogger _logger; - private readonly Fuchs_intranet _intranet; - private readonly FuchsEmailSettings _emailSettings; - private OCORE.email.EmailServerSettings? _cachedSettings; - - private const string ReplyToName = "Sebastian Fuchs - Bad und Heizung"; - private const string ReplyToAddress = "info@sanitaerfuchs.de"; - private const string SignatureIntro = - "

 

" + - "Herzliche Gr\u00fc\u00dfe aus D\u00fcsseldorf-Bilk
" + - "Ihr Team der Firma Sebastian Fuchs

"; - - public FuchsEmailService( - ILogger logger, - Fuchs_intranet intranet, - IOptions emailSettings) - { - _logger = logger; - _intranet = intranet; - _emailSettings = emailSettings.Value; - } - - public async Task SendEmailAsync(string reference, string subject, string html, - string email, string name, Dictionary? attachments) - { - var errors = new List(); - string guid = ""; - string config = ""; - DateTime sent = default; - - if (!IsValidEmail(email)) - errors.Add("Die Email-Adresse ist nicht g\u00fcltig."); - if (string.IsNullOrEmpty(html)) - errors.Add("Bitte geben Sie eine Nachricht ein."); - - if (errors.Count == 0) - { - try - { - var settings = GetEmailSettings(); - string body = html + BuildSignature(); - - // Development redirect: replace To with DevRedirectAddress, suppress CC/BCC - string devRedirect = _emailSettings.DevRedirectAddress; - bool isDevRedirect = !string.IsNullOrWhiteSpace(devRedirect); - if (isDevRedirect) - { - _logger.LogWarning("DEV redirect: email to '{Original}' redirected to '{Redirect}'", email, devRedirect); - email = devRedirect; - name = "Dev Redirect"; - } - - if (settings != null) - { - var msg = new OCORE.email.Email( - Mode: OCORE.email.EmailMode.DirectMode, - type: settings.type) - { - EmailSettings = settings, - Subject = subject - }; - msg.AddTo(email, name); - msg.AddReplyTo(new MailboxAddress(ReplyToName, ReplyToAddress)); - msg.SetBody(body); - if (attachments != null) - foreach (var kv in attachments) - msg.AttachFile(filecontent: kv.Value, kv.Key); - - var result = await msg.SendAsync( - (dref, ex) => _logger.LogError(ex, "Email send error {Reference}", dref)); - - guid = msg.MessageId ?? ""; - config = msg.EmailConfig_serialized; - sent = result.Timestamp; - errors.AddRange(result.ErrorMessages); - } - else - { - var main = _emailSettings.Main; - string host = !string.IsNullOrEmpty(main.Host) ? main.Host : _emailSettings.SmtpHost; - string user = !string.IsNullOrEmpty(main.Username) ? main.Username : _emailSettings.SmtpUser; - string pass = !string.IsNullOrEmpty(main.Password) ? main.Password : _emailSettings.SmtpPass; - string from = !string.IsNullOrEmpty(main.From) ? main.From - : (!string.IsNullOrEmpty(_emailSettings.SmtpFrom) ? _emailSettings.SmtpFrom : user); - string fromName = !string.IsNullOrEmpty(main.Alias) ? main.Alias : _emailSettings.SmtpFromName; - int port = main.Port > 0 ? main.Port : _emailSettings.SmtpPort; - - var message = new MimeMessage(); - message.From.Add(new MailboxAddress(fromName, from)); - message.To.Add(new MailboxAddress(name, email)); - message.ReplyTo.Add(new MailboxAddress(ReplyToName, ReplyToAddress)); - message.Subject = subject; - - var builder = new BodyBuilder { HtmlBody = body }; - if (attachments != null) - foreach (var kv in attachments) - builder.Attachments.Add(kv.Key, kv.Value); - message.Body = builder.ToMessageBody(); - - using var client = new MailKit.Net.Smtp.SmtpClient(); - await client.ConnectAsync(host, port, MailKit.Security.SecureSocketOptions.Auto); - if (!string.IsNullOrEmpty(user)) - await client.AuthenticateAsync(user, pass); - await client.SendAsync(message); - await client.DisconnectAsync(true); - - guid = message.MessageId!; - sent = DateTime.UtcNow; - } - } - catch (Exception ex) - { - errors.Add("Beim Versenden ist ein Fehler aufgetreten."); - _logger.LogError(ex, "SendEmailAsync failed for {Reference}", reference); - } - } - - if (errors.Count > 0) - _logger.LogWarning("SendEmailAsync errors for {Reference}: {Errors}", reference, errors); - - // SQL audit log - try - { - var pl = new List - { - SQL_VarChar("@Ref", reference), - SQL_VarChar("@guid", guid), - SQL_DateTime("@DateSent", sent == default ? DBNull.Value : (object)sent), - SQL_NVarChar("@config", config, dbNull_IfEmpty: true), - SQL_Bit("@success", errors.Count == 0), - SQL_NVarChar("@log", JsonConvert.SerializeObject(errors)) - }; - await setSQLValue_async( - "EXECUTE [dbo].[fds__logEmail] @Ref, @guid, @DateSent, @config, @success, @log;", - _intranet.Intranet__SQLConnectionString, pl, - Security: _intranet.GetDbSecurity()); - } - catch (Exception logEx) - { - _logger.LogError(logEx, "Failed to log email audit for {Reference}", reference); - } - - return errors.Count == 0; - } - - private OCORE.email.EmailServerSettings? GetEmailSettings() - { - if (_cachedSettings != null) return _cachedSettings; - - var main = _emailSettings.Main; - if (string.IsNullOrWhiteSpace(main.Host)) return null; - - _cachedSettings = new OCORE.email.EmailServerSettings(main.Type, main.ToOcoreJson()); - return _cachedSettings; - } - - private static string BuildSignature() - { - try - { - string sigPath = Path.Combine(AppContext.BaseDirectory, - "email_signature", "sanitaerfuchs_email_signature.txt"); - if (File.Exists(sigPath)) - return SignatureIntro + File.ReadAllText(sigPath); - } - catch { /* signature is optional */ } - return ""; - } - - private static bool IsValidEmail(string email) - { - try - { - var a = new System.Net.Mail.MailAddress(email); - return string.Equals(a.Address, email, StringComparison.OrdinalIgnoreCase); - } - catch { return false; } - } -} diff --git a/Fuchs/Services/FuchsEmailSettings.cs b/Fuchs/Services/FuchsEmailSettings.cs deleted file mode 100644 index 1e24cc1..0000000 --- a/Fuchs/Services/FuchsEmailSettings.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace Fuchs.Services; - -/// -/// SMTP / email server settings bound from appsettings.json → "Fuchs:Email" section. -/// -public class FuchsEmailSettings -{ - /// Main account for outgoing customer emails (anfrage@sanitaerfuchs.de). - public SmtpAccountSettings Main { get; set; } = new(); - - /// FDS/invoicing account (rechnungen@sanitaerfuchs.de). - public SmtpAccountSettings Fds { get; set; } = new(); - - /// Internal OCORE service account used for system notifications. - public SmtpAccountSettings Service { get; set; } = new(); - - /// Comma-separated list of addresses used as test recipients. - public string TestAddresses { get; set; } = ""; - - // ── MailKit fallback settings (used when Main is not configured) ── - public string SmtpHost { get; set; } = ""; - public int SmtpPort { get; set; } = 587; - public string SmtpUser { get; set; } = ""; - public string SmtpPass { get; set; } = ""; - public string SmtpFrom { get; set; } = ""; - public string SmtpFromName { get; set; } = "SanitärFuchs"; - - /// - /// Development only: when set, all outgoing emails are redirected to this address; - /// CC and BCC are cleared. Leave empty in production. - /// - public string DevRedirectAddress { get; set; } = ""; -} - diff --git a/Fuchs/Services/IEmailService.cs b/Fuchs/Services/IComService.cs similarity index 54% rename from Fuchs/Services/IEmailService.cs rename to Fuchs/Services/IComService.cs index b39e069..5e5dd1f 100644 --- a/Fuchs/Services/IEmailService.cs +++ b/Fuchs/Services/IComService.cs @@ -1,9 +1,10 @@ namespace Fuchs.Services; /// -/// Abstraction for sending emails with optional attachments and audit logging. +/// Abstraction for outbound communication: email and SMS. +/// Backed by the ProcessWeb Mailer API (POST /api/mailer?fn=push_com). /// -public interface IEmailService +public interface IComService { /// /// Sends an email and logs the result to the database. @@ -14,7 +15,15 @@ public interface IEmailService /// Recipient email address. /// Recipient display name. /// Optional file attachments (filename → content). - /// true when the email was sent successfully. + /// true when the email was accepted successfully. Task SendEmailAsync(string reference, string subject, string html, - string email, string name, Dictionary? attachments); + string email, string name, Dictionary? attachments = null); + + /// + /// Sends an SMS message. + /// + /// Recipient mobile number (E.164 or local format). + /// Text message body. + /// true when the SMS was accepted successfully. + Task SendSmsAsync(string mobile, string message); } diff --git a/Fuchs/Services/ProcessWebComService.cs b/Fuchs/Services/ProcessWebComService.cs new file mode 100644 index 0000000..811870f --- /dev/null +++ b/Fuchs/Services/ProcessWebComService.cs @@ -0,0 +1,202 @@ +using System.Net.Http.Headers; +using System.Text; +using Fuchs.intranet; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using OCORE.SQL; +using static OCORE.SQL.sql; + +namespace Fuchs.Services; + +/// +/// Outbound communication service backed by the ProcessWeb Mailer API +/// (POST https://api.processweb.de/api/mailer?fn=push_com). +/// When ProcessWebComSettings.Enabled is false the service +/// only logs the intended communication without calling the API. +/// +public class ProcessWebComService : IComService +{ + private readonly ILogger _logger; + private readonly Fuchs_intranet _intranet; + private readonly ProcessWebComSettings _settings; + private readonly IHttpClientFactory _httpClientFactory; + + private const string SignatureIntro = + "

 

" + + "Herzliche Grüße aus Düsseldorf-Bilk
" + + "Ihr Team der Firma Sebastian Fuchs

"; + + public ProcessWebComService( + ILogger logger, + Fuchs_intranet intranet, + IOptions settings, + IHttpClientFactory httpClientFactory) + { + _logger = logger; + _intranet = intranet; + _settings = settings.Value; + _httpClientFactory = httpClientFactory; + } + + public async Task SendEmailAsync(string reference, string subject, string html, + string email, string name, Dictionary? attachments = null) + { + if (!IsValidEmail(email)) + { + _logger.LogWarning("SendEmailAsync: invalid email address '{Email}' for ref {Reference}", email, reference); + return false; + } + + string body = html + BuildSignature(); + + if (!_settings.Enabled) + { + _logger.LogInformation( + "[ComService DISABLED] Would send email ref={Reference} subject='{Subject}' to={Email}", + reference, subject, email); + await WriteAuditLogAsync(reference, "", "", default, false, ["Service disabled – email not sent"]); + return false; + } + + bool success = false; + string messageId = ""; + var errors = new List(); + + try + { + var payload = new + { + comType = "email", + recipient = email, + subject, + body + }; + + var (ok, responseBody) = await PostToApiAsync("push_com", payload); + if (ok) + { + success = true; + messageId = Guid.NewGuid().ToString("N"); + } + else + { + errors.Add($"API error: {responseBody}"); + _logger.LogWarning("SendEmailAsync API error for {Reference}: {Body}", reference, responseBody); + } + } + catch (Exception ex) + { + errors.Add("Beim Versenden ist ein Fehler aufgetreten."); + _logger.LogError(ex, "SendEmailAsync failed for {Reference}", reference); + } + + await WriteAuditLogAsync(reference, messageId, "", success ? DateTime.UtcNow : default, success, errors); + return success; + } + + public async Task SendSmsAsync(string mobile, string message) + { + if (string.IsNullOrWhiteSpace(mobile)) + { + _logger.LogWarning("SendSmsAsync: empty mobile number"); + return false; + } + + if (!_settings.Enabled) + { + _logger.LogInformation( + "[ComService DISABLED] Would send SMS to={Mobile} message='{Message}'", + mobile, message); + return false; + } + + try + { + var payload = new + { + comType = "sms", + recipient = mobile, + body = message + }; + + var (ok, responseBody) = await PostToApiAsync("push_com", payload); + if (ok) return true; + + _logger.LogWarning("SendSmsAsync API error for {Mobile}: {Body}", mobile, responseBody); + return false; + } + catch (Exception ex) + { + _logger.LogError(ex, "SendSmsAsync failed for {Mobile}", mobile); + return false; + } + } + + // ── Private helpers ──────────────────────────────────────────────────────── + + private async Task<(bool ok, string body)> PostToApiAsync(string fn, object payload) + { + var client = _httpClientFactory.CreateClient("ProcessWebMailer"); + var json = JsonConvert.SerializeObject(payload); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + string credentials = Convert.ToBase64String( + Encoding.UTF8.GetBytes($"{_settings.AccountId}:{_settings.Token}")); + client.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("Basic", credentials); + + var response = await client.PostAsync($"{_settings.BaseUrl}/api/mailer?fn={fn}", content); + string responseBody = await response.Content.ReadAsStringAsync(); + return (response.IsSuccessStatusCode, responseBody); + } + + private async Task WriteAuditLogAsync(string reference, string guid, string config, + DateTime sent, bool success, IEnumerable errors) + { + try + { + var pl = new List + { + SQL_VarChar("@Ref", reference), + SQL_VarChar("@guid", guid), + SQL_DateTime("@DateSent", sent == default ? DBNull.Value : (object)sent), + SQL_NVarChar("@config", config, dbNull_IfEmpty: true), + SQL_Bit("@success", success), + SQL_NVarChar("@log", JsonConvert.SerializeObject(errors.ToList())) + }; + await setSQLValue_async( + "EXECUTE [dbo].[fds__logEmail] @Ref, @guid, @DateSent, @config, @success, @log;", + _intranet.Intranet__SQLConnectionString, pl, + Security: _intranet.GetDbSecurity()); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to write audit log for {Reference}", reference); + } + } + + private static string BuildSignature() + { + try + { + string sigPath = Path.Combine(AppContext.BaseDirectory, + "email_signature", "sanitaerfuchs_email_signature.txt"); + if (File.Exists(sigPath)) + return SignatureIntro + File.ReadAllText(sigPath); + } + catch { /* signature is optional */ } + return ""; + } + + private static bool IsValidEmail(string email) + { + try + { + var a = new System.Net.Mail.MailAddress(email); + return string.Equals(a.Address, email, StringComparison.OrdinalIgnoreCase); + } + catch { return false; } + } +} diff --git a/Fuchs/Services/ProcessWebComSettings.cs b/Fuchs/Services/ProcessWebComSettings.cs new file mode 100644 index 0000000..a2cde25 --- /dev/null +++ b/Fuchs/Services/ProcessWebComSettings.cs @@ -0,0 +1,23 @@ +namespace Fuchs.Services; + +/// +/// Settings for the ProcessWeb Mailer API, bound from appsettings.json → "Fuchs:Mailer". +/// +public class ProcessWebComSettings +{ + /// Base URL of the ProcessWeb API (e.g. "https://api.processweb.de"). + public string BaseUrl { get; set; } = "https://api.processweb.de"; + + /// Account ID used for HTTP Basic authentication. + public string AccountId { get; set; } = ""; + + /// API token used for HTTP Basic authentication. + public string Token { get; set; } = ""; + + /// + /// When false (default) the service is disabled and only logs the + /// intended communication without actually calling the API. + /// Set to true to enable live sending. + /// + public bool Enabled { get; set; } = false; +} diff --git a/Fuchs/Services/SecretManagementExtensions.cs b/Fuchs/Services/SecretManagementExtensions.cs new file mode 100644 index 0000000..143f14c --- /dev/null +++ b/Fuchs/Services/SecretManagementExtensions.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Builder; + +namespace OCORE_web.Secrets; + +/// +/// Placeholder for secret management extension methods. +/// Replace with a real Azure Key Vault / DPAPI implementation when ready. +/// +public static class SecretManagementExtensions +{ + /// + /// No-op stub. Wire up real secret management (e.g. Azure Key Vault) here. + /// + public static WebApplicationBuilder AddSecretManagement(this WebApplicationBuilder builder) + => builder; +} diff --git a/Fuchs/Services/SmtpAccountSettings.cs b/Fuchs/Services/SmtpAccountSettings.cs deleted file mode 100644 index c55d4a6..0000000 --- a/Fuchs/Services/SmtpAccountSettings.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Newtonsoft.Json; - -namespace Fuchs.Services; - -/// -/// SMTP account configuration for a single email account. -/// Property names match the OCORE EmailServerSettings JSON format (lowercase). -/// -public class SmtpAccountSettings -{ - [JsonProperty("alias")] - public string Alias { get; set; } = ""; - - [JsonProperty("to")] - public string To { get; set; } = ""; - - [JsonProperty("from")] - public string From { get; set; } = ""; - - [JsonProperty("bcc")] - public string Bcc { get; set; } = ""; - - [JsonProperty("host")] - public string Host { get; set; } = ""; - - [JsonProperty("port")] - public int Port { get; set; } = 587; - - [JsonProperty("security")] - public string Security { get; set; } = "StartTls"; - - [JsonProperty("username")] - public string Username { get; set; } = ""; - - [JsonProperty("password")] - public string Password { get; set; } = ""; - - [JsonProperty("type")] - public string Type { get; set; } = "smtp"; - - /// - /// Serializes this instance back to the JSON string format expected by - /// OCORE.email.EmailServerSettings(type, raw). - /// - public string ToOcoreJson() => JsonConvert.SerializeObject(this); -} diff --git a/Fuchs/appsettings.json b/Fuchs/appsettings.json index 0a21eaf..c4897b4 100644 --- a/Fuchs/appsettings.json +++ b/Fuchs/appsettings.json @@ -8,9 +8,7 @@ "ConnectionStrings--ocms-ConnectionString", "ConnectionStrings--fuchs-fds-ConnectionString", "Fuchs--SMS-APIKey", - "Fuchs--Email--Main--password", - "Fuchs--Email--Fds--password", - "Fuchs--Email--Service--password", + "Fuchs--Mailer--Token", "Fuchs--fuchs-captcha-TOTP", "Fuchs--fuchs-intranet-TOTP" ] @@ -35,41 +33,11 @@ "fuchs_captcha_TOTP": "MANAGED_BY_KEYVAULT", "fuchs_intranet_TOTP": "MANAGED_BY_KEYVAULT", "SMS_APIKey": "MANAGED_BY_KEYVAULT", - "Email": { - "Main": { - "alias": "Sebastian Fuchs - Bad und Heizung", - "to": "anfrage@sanitaerfuchs.de", - "from": "anfrage@sanitaerfuchs.de", - "bcc": "info@processweb.de", - "host": "smtp.office365.com", - "port": 587, - "security": "StartTls", - "username": "anfrage@sanitaerfuchs.de", - "password": "MANAGED_BY_KEYVAULT" - }, - "Fds": { - "alias": "Sebastian Fuchs - Bad und Heizung", - "to": "", - "from": "rechnungen@sanitaerfuchs.de", - "bcc": "", - "host": "smtp.office365.com", - "port": 587, - "security": "StartTls", - "username": "rechnungen@sanitaerfuchs.de", - "password": "MANAGED_BY_KEYVAULT" - }, - "Service": { - "alias": "ProcessWeb Service", - "to": "", - "from": "service@emails.processweb.de", - "bcc": "", - "host": "emails.processweb.de", - "port": 587, - "security": "StartTls", - "username": "service@emails.processweb.de", - "password": "MANAGED_BY_KEYVAULT" - }, - "TestAddresses": "st.ott@web.de,info@processweb.de" + "Mailer": { + "BaseUrl": "https://api.processweb.de", + "AccountId": "", + "Token": "MANAGED_BY_KEYVAULT", + "Enabled": false } } } \ No newline at end of file diff --git a/Fuchs/code/FuchsFdsEmail.cs b/Fuchs/code/FuchsFdsEmail.cs deleted file mode 100644 index c36e2ce..0000000 --- a/Fuchs/code/FuchsFdsEmail.cs +++ /dev/null @@ -1,196 +0,0 @@ -using Cfg = System.Configuration.ConfigurationManager; -using Fuchs.intranet; -using MimeKit; -using Newtonsoft.Json; -using Microsoft.Data.SqlClient; -using OCORE.SQL; -using static OCORE.SQL.sql; - -namespace Fuchs.intranet; - -/// -/// Email sending helper for Fuchs intranet. Full port of fuchs_fds_email.vb. -/// Uses OCORE.email.Emailcommons.SendEmail_async when settings are available, -/// falls back to MailKit direct send otherwise. -/// -public static class FuchsFdsEmail -{ - private static OCORE.email.EmailServerSettings? _settings; - - private static OCORE.email.EmailServerSettings? GetEmailSettings() - { - if (_settings != null) return _settings; - string raw = Cfg.AppSettings["FDS_EmailSettings"] ?? ""; - if (string.IsNullOrWhiteSpace(raw)) return null; - - // The serialised string is JSON containing a "type" field - string type = "smtp"; - try - { - var dic = JsonConvert.DeserializeObject>(raw); - if (dic?.TryGetValue("type", out var t) == true) - type = t?.ToString() ?? "smtp"; - } - catch { /* keep default type */ } - - _settings = new OCORE.email.EmailServerSettings(type, raw); - return _settings; - } - - private const string ReplyToName = "Sebastian Fuchs - Bad und Heizung"; - private const string ReplyToAddress = "info@sanitaerfuchs.de"; - private const string SignatureIntro = - "

 

" + - "Herzliche Gr\u00fc\u00dfe aus D\u00fcsseldorf-Bilk
" + - "Ihr Team der Firma Sebastian Fuchs

"; - - /// - /// Sends an email and logs the result to the Fuchs FDS database. - /// Returns true on success. - /// - public static async Task SendEmail( - string @ref, - string subject, - string html, - string email, - string name, - Dictionary? files, - Fuchs_intranet intranet) - { - var errors = new List(); - string guid = ""; - string config = ""; - DateTime sent = default; - - if (!IsValidEmail(email)) - errors.Add("Die Email-Adresse ist nicht g\u00fcltig."); - if (string.IsNullOrEmpty(html)) - errors.Add("Bitte geben Sie eine Nachricht ein."); - - if (errors.Count == 0) - { - try - { - var settings = GetEmailSettings(); - string body = html + BuildSignature(); - - if (settings != null) - { - // ── OCORE path ───────────────────────────────────────── - var msg = new OCORE.email.Email( - Mode: OCORE.email.EmailMode.DirectMode, - type: settings.type) - { - EmailSettings = settings, - Subject = subject - }; - msg.AddTo(email, name); - msg.AddReplyTo(new MailboxAddress(ReplyToName, ReplyToAddress)); - msg.SetBody(body); - if (files != null) - foreach (var kv in files) - msg.AttachFile(filecontent: kv.Value, kv.Key); - - var result = await msg.SendAsync( - (dref, ex) => intranet.debug_log( - $"FuchsFdsEmail.SendEmail {dref}", ex)); - - guid = msg.MessageId ?? ""; - config = msg.EmailConfig_serialized; - sent = result.Timestamp; - errors.AddRange(result.ErrorMessages); - } - else - { - // ── MailKit fallback (app settings) ──────────────────── - string host = Cfg.AppSettings["smtp_host"] ?? ""; - string user = Cfg.AppSettings["smtp_user"] ?? ""; - string pass = Cfg.AppSettings["smtp_pass"] ?? ""; - string from = Cfg.AppSettings["smtp_from"] ?? user; - string fromName = Cfg.AppSettings["smtp_fromname"] ?? "Sanit\u00e4rFuchs"; - int port = int.TryParse(Cfg.AppSettings["smtp_port"], out int p) ? p : 587; - - var message = new MimeMessage(); - message.From.Add(new MailboxAddress(fromName, from)); - message.To.Add(new MailboxAddress(name, email)); - message.ReplyTo.Add(new MailboxAddress(ReplyToName, ReplyToAddress)); - message.Subject = subject; - - var builder = new BodyBuilder { HtmlBody = body }; - if (files != null) - foreach (var kv in files) - builder.Attachments.Add(kv.Key, kv.Value); - message.Body = builder.ToMessageBody(); - - using var client = new MailKit.Net.Smtp.SmtpClient(); - await client.ConnectAsync(host, port, - MailKit.Security.SecureSocketOptions.Auto); - if (!string.IsNullOrEmpty(user)) - await client.AuthenticateAsync(user, pass); - await client.SendAsync(message); - await client.DisconnectAsync(true); - - guid = message.MessageId!; - sent = DateTime.UtcNow; - } - } - catch (Exception ex) - { - errors.Add("Beim Versenden ist ein Fehler aufgetreten."); - intranet.debug_log("FuchsFdsEmail.SendEmail inner", ex, data: errors); - } - } - - if (errors.Count > 0) - intranet.debug_log("FuchsFdsEmail.SendEmail", - data: new { @ref, email, errors }); - - // ── SQL audit log ────────────────────────────────────────────────── - try - { - var pl = new List - { - SQL_VarChar("@Ref", @ref), - SQL_VarChar("@guid", guid), - SQL_DateTime("@DateSent", sent == default ? DBNull.Value : (object)sent), - SQL_NVarChar("@config", config, dbNull_IfEmpty: true), - SQL_Bit("@success", errors.Count == 0), - SQL_NVarChar("@log", JsonConvert.SerializeObject(errors)) - }; - await setSQLValue_async( - "EXECUTE [dbo].[fds__logEmail] @Ref, @guid, @DateSent, @config, @success, @log;", - intranet.Intranet__SQLConnectionString, pl, - Security: intranet.GetDbSecurity()); - } - catch (Exception logEx) - { - intranet.debug_log("FuchsFdsEmail.SendEmail log", logEx); - } - - return errors.Count == 0; - } - - // ── Helpers ──────────────────────────────────────────────────────────────── - private static string BuildSignature() - { - try - { - string sigPath = Path.Combine(AppContext.BaseDirectory, - "email_signature", "sanitaerfuchs_email_signature.txt"); - if (File.Exists(sigPath)) - return SignatureIntro + File.ReadAllText(sigPath); - } - catch { /* signature is optional */ } - return ""; - } - - private static bool IsValidEmail(string email) - { - try - { - var a = new System.Net.Mail.MailAddress(email); - return string.Equals(a.Address, email, StringComparison.OrdinalIgnoreCase); - } - catch { return false; } - } -} diff --git a/Fuchs_Intranet.slnx b/Fuchs_Intranet.slnx index dc55268..cdd57ad 100644 --- a/Fuchs_Intranet.slnx +++ b/Fuchs_Intranet.slnx @@ -12,22 +12,6 @@ - - - - - - - - - - - - - - - -