Compare commits

...

5 Commits

Author SHA1 Message Date
Stefan 1ce497b37e -
Playwright Tests / test (push) Has been cancelled
2026-05-18 08:27:07 +02:00
Stefan aceef9ff74 Update NuGet packages and project references
Upgraded multiple NuGet packages to latest versions across all projects, including test and core dependencies. Updated OCORE, OCORE_web, and OCORE_web_pdf project references to use local paths. Added OCORE-related projects to the solution file with environment-specific build configs. Fixed package.json structure for valid JSON.
2026-05-18 08:26:58 +02:00
Stefan 445fc2b858 Add OCORE_Charting as a new Git submodule
Added the OCORE_Charting submodule, referencing its repository at https://git.processweb.de/Stefan/OCORE_Charting.git and tracking commit fcb8f090d4e4147ca7b79c20c65099dd589e3b85.
2026-05-18 08:26:30 +02:00
Stefan 9dcbf0d958 Add OCORE submodules 2026-05-18 08:15:16 +02:00
Stefan b17baca835 Unify email/SMS via ProcessWeb Mailer API, remove legacy
Replaces legacy email/SMS logic with a new IComService abstraction using the ProcessWeb Mailer API for all outbound communication. Removes FuchsFdsEmail, FuchsEmailService, IEmailService, SmtpAccountSettings, and FuchsEmailSettings. Updates controllers to use IComService. Refactors appsettings.json to use a new "Mailer" section. Adds ProcessWebComSettings and a stub for secret management. Removes OCORE.sms.SMS77 and direct SMTP/MailKit usage. Cleans up solution file references to OCORE projects.
2026-05-18 08:11:21 +02:00
25 changed files with 352 additions and 582 deletions
@@ -0,0 +1 @@
-
+12
View File
@@ -0,0 +1,12 @@
[submodule "OCORE"]
path = OCORE
url = https://git.processweb.de/Stefan/OCORE.git
[submodule "OCORE_web"]
path = OCORE_web
url = https://git.processweb.de/Stefan/OCORE_web.git
[submodule "OCORE_web_pdf"]
path = OCORE_web_pdf
url = https://git.processweb.de/Stefan/OCORE_web_pdf.git
[submodule "OCORE_Charting"]
path = OCORE_Charting
url = https://git.processweb.de/Stefan/OCORE_Charting.git
+3 -3
View File
@@ -9,14 +9,14 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="coverlet.collector" Version="6.0.4">
<PackageReference Include="coverlet.collector" Version="10.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
@@ -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();
}
@@ -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<string, byte[]> { [frdic.nz("DocumentName")] = filebyte },
_intranet);
new Dictionary<string, byte[]> { [frdic.nz("DocumentName")] = filebyte });
}
return Ok();
}
+9 -11
View File
@@ -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<IntranetController> _logger;
private readonly IComService _comService;
private readonly List<string> _allowedNonAuth = new() { "spwc", "spw" };
private readonly List<string> _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<IntranetController> logger)
public IntranetController(Fuchs_intranet intranet, fds.IFdsMfr mfr, ILogger<IntranetController> logger, IComService comService)
{
_intranet = intranet;
_mfr = mfr;
_logger = logger;
_comService = comService;
}
// ── Standard param list (pre-populates @authuser) ────────────────────────
@@ -203,11 +206,8 @@ 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,
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",
$"<p>Guten Tag {row.nz("firstname")} {row.nz("name")},<br/>Ihr Passwort: {HttpUtility.HtmlEncode(row.nz("password"))}</p>",
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();
+7 -7
View File
@@ -12,11 +12,11 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\WebProjectComponents\MT940Parser\MT940Parser\MT940Parser.csproj" />
<ProjectReference Include="..\..\..\WebProjectComponents\OCORE_web\OCORE_web.csproj" />
<ProjectReference Include="..\..\..\WebProjectComponents\OCORE\OCORE\OCORE.csproj" />
<ProjectReference Include="..\Fuchs_DataService\Fuchs_DataService.csproj" />
<ProjectReference Include="..\MFR_RESTClient\MFR_RESTClient.csproj" />
<ProjectReference Include="..\..\..\WebProjectComponents\OCORE_web_pdf\OCORE_web_pdf.csproj" />
<ProjectReference Include="..\OCORE\OCORE\OCORE.csproj" />
<ProjectReference Include="..\OCORE_web\OCORE_web\OCORE_web.csproj" />
<ProjectReference Include="..\OCORE_web_pdf\OCORE_web_pdf.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="code\7z.dll" CopyToOutputDirectory="PreserveNewest" />
@@ -40,10 +40,10 @@
<PackageReference Include="MimeKit" Version="4.16.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<!-- New packages (needed for .NET 10) -->
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.6" />
<PackageReference Include="System.Drawing.Common" Version="10.0.6" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.1" />
<PackageReference Include="SixLabors.ImageSharp" Version="4.0.0" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.8" />
<PackageReference Include="System.Drawing.Common" Version="10.0.8" />
</ItemGroup>
<ItemGroup>
<Folder Include="App_Data\cache\" />
+4 -3
View File
@@ -71,9 +71,10 @@ public class Program
builder.Services.AddAuthorization();
builder.Services.AddHttpContextAccessor();
// Email service
builder.Services.Configure<FuchsEmailSettings>(builder.Configuration.GetSection("Fuchs:Email"));
builder.Services.AddScoped<IEmailService, FuchsEmailService>();
// Communication service (email + SMS via ProcessWeb Mailer API)
builder.Services.Configure<ProcessWebComSettings>(builder.Configuration.GetSection("Fuchs:Mailer"));
builder.Services.AddHttpClient("ProcessWebMailer");
builder.Services.AddScoped<IComService, ProcessWebComService>();
}
private static void ConfigureApp(WebApplication app)
-197
View File
@@ -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;
/// <summary>
/// Email service implementation. Replaces the static <c>FuchsFdsEmail</c> class.
/// Reads settings from <c>IOptions&lt;FuchsEmailSettings&gt;</c> (appsettings.json)
/// instead of <c>System.Configuration.ConfigurationManager</c>.
/// </summary>
public class FuchsEmailService : IEmailService
{
private readonly ILogger<FuchsEmailService> _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 =
"<p>&nbsp;</p><p style=\"margin:24px 0 16px 0;line-height:140%;\">" +
"Herzliche Gr\u00fc\u00dfe aus D\u00fcsseldorf-Bilk<br/>" +
"Ihr Team der Firma Sebastian Fuchs</p>";
public FuchsEmailService(
ILogger<FuchsEmailService> logger,
Fuchs_intranet intranet,
IOptions<FuchsEmailSettings> emailSettings)
{
_logger = logger;
_intranet = intranet;
_emailSettings = emailSettings.Value;
}
public async Task<bool> SendEmailAsync(string reference, string subject, string html,
string email, string name, Dictionary<string, byte[]>? attachments)
{
var errors = new List<string>();
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<SqlParameter>
{
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; }
}
}
-34
View File
@@ -1,34 +0,0 @@
namespace Fuchs.Services;
/// <summary>
/// SMTP / email server settings bound from appsettings.json → "Fuchs:Email" section.
/// </summary>
public class FuchsEmailSettings
{
/// <summary>Main account for outgoing customer emails (anfrage@sanitaerfuchs.de).</summary>
public SmtpAccountSettings Main { get; set; } = new();
/// <summary>FDS/invoicing account (rechnungen@sanitaerfuchs.de).</summary>
public SmtpAccountSettings Fds { get; set; } = new();
/// <summary>Internal OCORE service account used for system notifications.</summary>
public SmtpAccountSettings Service { get; set; } = new();
/// <summary>Comma-separated list of addresses used as test recipients.</summary>
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";
/// <summary>
/// Development only: when set, all outgoing emails are redirected to this address;
/// CC and BCC are cleared. Leave empty in production.
/// </summary>
public string DevRedirectAddress { get; set; } = "";
}
@@ -1,9 +1,10 @@
namespace Fuchs.Services;
/// <summary>
/// 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).
/// </summary>
public interface IEmailService
public interface IComService
{
/// <summary>
/// Sends an email and logs the result to the database.
@@ -14,7 +15,15 @@ public interface IEmailService
/// <param name="email">Recipient email address.</param>
/// <param name="name">Recipient display name.</param>
/// <param name="attachments">Optional file attachments (filename → content).</param>
/// <returns><c>true</c> when the email was sent successfully.</returns>
/// <returns><c>true</c> when the email was accepted successfully.</returns>
Task<bool> SendEmailAsync(string reference, string subject, string html,
string email, string name, Dictionary<string, byte[]>? attachments);
string email, string name, Dictionary<string, byte[]>? attachments = null);
/// <summary>
/// Sends an SMS message.
/// </summary>
/// <param name="mobile">Recipient mobile number (E.164 or local format).</param>
/// <param name="message">Text message body.</param>
/// <returns><c>true</c> when the SMS was accepted successfully.</returns>
Task<bool> SendSmsAsync(string mobile, string message);
}
+202
View File
@@ -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;
/// <summary>
/// Outbound communication service backed by the ProcessWeb Mailer API
/// (POST https://api.processweb.de/api/mailer?fn=push_com).
/// When <c>ProcessWebComSettings.Enabled</c> is <c>false</c> the service
/// only logs the intended communication without calling the API.
/// </summary>
public class ProcessWebComService : IComService
{
private readonly ILogger<ProcessWebComService> _logger;
private readonly Fuchs_intranet _intranet;
private readonly ProcessWebComSettings _settings;
private readonly IHttpClientFactory _httpClientFactory;
private const string SignatureIntro =
"<p>&nbsp;</p><p style=\"margin:24px 0 16px 0;line-height:140%;\">" +
"Herzliche Grüße aus Düsseldorf-Bilk<br/>" +
"Ihr Team der Firma Sebastian Fuchs</p>";
public ProcessWebComService(
ILogger<ProcessWebComService> logger,
Fuchs_intranet intranet,
IOptions<ProcessWebComSettings> settings,
IHttpClientFactory httpClientFactory)
{
_logger = logger;
_intranet = intranet;
_settings = settings.Value;
_httpClientFactory = httpClientFactory;
}
public async Task<bool> SendEmailAsync(string reference, string subject, string html,
string email, string name, Dictionary<string, byte[]>? 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<string>();
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<bool> 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<string> errors)
{
try
{
var pl = new List<SqlParameter>
{
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; }
}
}
+23
View File
@@ -0,0 +1,23 @@
namespace Fuchs.Services;
/// <summary>
/// Settings for the ProcessWeb Mailer API, bound from appsettings.json → "Fuchs:Mailer".
/// </summary>
public class ProcessWebComSettings
{
/// <summary>Base URL of the ProcessWeb API (e.g. "https://api.processweb.de").</summary>
public string BaseUrl { get; set; } = "https://api.processweb.de";
/// <summary>Account ID used for HTTP Basic authentication.</summary>
public string AccountId { get; set; } = "";
/// <summary>API token used for HTTP Basic authentication.</summary>
public string Token { get; set; } = "";
/// <summary>
/// When <c>false</c> (default) the service is disabled and only logs the
/// intended communication without actually calling the API.
/// Set to <c>true</c> to enable live sending.
/// </summary>
public bool Enabled { get; set; } = false;
}
@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Builder;
namespace OCORE_web.Secrets;
/// <summary>
/// Placeholder for secret management extension methods.
/// Replace with a real Azure Key Vault / DPAPI implementation when ready.
/// </summary>
public static class SecretManagementExtensions
{
/// <summary>
/// No-op stub. Wire up real secret management (e.g. Azure Key Vault) here.
/// </summary>
public static WebApplicationBuilder AddSecretManagement(this WebApplicationBuilder builder)
=> builder;
}
-46
View File
@@ -1,46 +0,0 @@
using Newtonsoft.Json;
namespace Fuchs.Services;
/// <summary>
/// SMTP account configuration for a single email account.
/// Property names match the OCORE EmailServerSettings JSON format (lowercase).
/// </summary>
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";
/// <summary>
/// Serializes this instance back to the JSON string format expected by
/// <c>OCORE.email.EmailServerSettings(type, raw)</c>.
/// </summary>
public string ToOcoreJson() => JsonConvert.SerializeObject(this);
}
+6 -38
View File
@@ -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
}
}
}
-196
View File
@@ -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;
/// <summary>
/// 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.
/// </summary>
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<Dictionary<string, object>>(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 =
"<p>&nbsp;</p><p style=\"margin:24px 0 16px 0;line-height:140%;\">" +
"Herzliche Gr\u00fc\u00dfe aus D\u00fcsseldorf-Bilk<br/>" +
"Ihr Team der Firma Sebastian Fuchs</p>";
/// <summary>
/// Sends an email and logs the result to the Fuchs FDS database.
/// Returns true on success.
/// </summary>
public static async Task<bool> SendEmail(
string @ref,
string subject,
string html,
string email,
string name,
Dictionary<string, byte[]>? files,
Fuchs_intranet intranet)
{
var errors = new List<string>();
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<SqlParameter>
{
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; }
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
{
{
"name": "fuchs",
"version": "1.1.0",
"dependencies": {
+6 -6
View File
@@ -25,8 +25,8 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MFR_RESTClient\MFR_RESTClient.csproj" />
<ProjectReference Include="..\..\..\WebProjectComponents\OCORE_web\OCORE_web.csproj" />
<ProjectReference Include="..\..\..\WebProjectComponents\OCORE\OCORE\OCORE.csproj" />
<ProjectReference Include="..\OCORE\OCORE\OCORE.csproj" />
<ProjectReference Include="..\OCORE_web\OCORE_web\OCORE_web.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="7z.dll">
@@ -37,9 +37,9 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Squid-Box.SevenZipSharp" Version="1.6.2.24" />
<PackageReference Include="Topshelf" Version="4.3.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.6" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.6" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.6" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.8" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.8" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.8" />
</ItemGroup>
</Project>
+24 -16
View File
@@ -12,22 +12,6 @@
<BuildType Solution="db-dev.processweb.de|Any CPU" Project="Debug" />
<BuildType Solution="server02.processweb.de|Any CPU" Project="Debug" />
</Project>
<Project Path="../../WebProjectComponents/OCORE/OCORE/OCORE.csproj">
<BuildType Solution="db-dev.processweb.de|*" Project="Debug" />
<BuildType Solution="server02.processweb.de|*" Project="Debug" />
</Project>
<Project Path="../../WebProjectComponents/OCORE_Charting/OCORE_Charting.csproj">
<BuildType Solution="db-dev.processweb.de|*" Project="Debug" />
<BuildType Solution="server02.processweb.de|*" Project="Debug" />
</Project>
<Project Path="../../WebProjectComponents/OCORE_web/OCORE_web.csproj">
<BuildType Solution="db-dev.processweb.de|*" Project="Debug" />
<BuildType Solution="server02.processweb.de|*" Project="Debug" />
</Project>
<Project Path="../../WebProjectComponents/OCORE_web_pdf/OCORE_web_pdf.csproj">
<BuildType Solution="db-dev.processweb.de|*" Project="Release" />
<BuildType Solution="server02.processweb.de|*" Project="Debug" />
</Project>
<Project Path="Fuchs.Tests/Fuchs.Tests.csproj">
<BuildType Solution="db-dev.processweb.de|*" Project="Debug" />
<BuildType Solution="server02.processweb.de|*" Project="Debug" />
@@ -35,4 +19,28 @@
<Project Path="Fuchs/Fuchs.csproj" />
<Project Path="Fuchs_DataService/Fuchs_DataService.csproj" />
<Project Path="MFR_RESTClient/MFR_RESTClient.csproj" />
<Project Path="OCORE/OCORE/OCORE.csproj">
<BuildType Solution="db-dev.processweb.de|*" Project="Release" />
<BuildType Solution="server02.processweb.de|*" Project="Debug" />
</Project>
<Project Path="OCORE/OCORETests/OCORETests.csproj">
<BuildType Solution="db-dev.processweb.de|*" Project="Release" />
<BuildType Solution="server02.processweb.de|*" Project="Debug" />
</Project>
<Project Path="OCORE_Charting/OCORE_Charting.csproj">
<BuildType Solution="db-dev.processweb.de|*" Project="Release" />
<BuildType Solution="server02.processweb.de|*" Project="Debug" />
</Project>
<Project Path="OCORE_web/OCORE_web/OCORE_web.csproj">
<BuildType Solution="db-dev.processweb.de|*" Project="Release" />
<BuildType Solution="server02.processweb.de|*" Project="Debug" />
</Project>
<Project Path="OCORE_web/OCORE_webTests/OCORE_webTests.csproj">
<BuildType Solution="db-dev.processweb.de|*" Project="Release" />
<BuildType Solution="server02.processweb.de|*" Project="Debug" />
</Project>
<Project Path="OCORE_web_pdf/OCORE_web_pdf.csproj">
<BuildType Solution="db-dev.processweb.de|*" Project="Release" />
<BuildType Solution="server02.processweb.de|*" Project="Debug" />
</Project>
</Solution>
+10 -10
View File
@@ -26,21 +26,21 @@
<PackageReference Include="Microsoft.Rest.ClientRuntime" Version="2.3.24" />
<PackageReference Include="System.Spatial" Version="5.8.5" />
<!-- Updated packages -->
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.6" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.17.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.8" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.18.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="RestSharp" Version="114.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.17.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.18.0" />
<!-- New packages (replacements) -->
<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.20.1" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.6" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.6" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.6" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.8" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.8" />
<!-- Deprecated but kept for compatibility (review in follow-up) -->
<PackageReference Include="Microsoft.IdentityModel.Abstractions" Version="8.17.0" />
<PackageReference Include="Microsoft.IdentityModel.Abstractions" Version="8.18.0" />
<PackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory" Version="5.3.0" />
<PackageReference Include="Microsoft.IdentityModel.Logging" Version="8.17.0" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.17.0" />
<PackageReference Include="Microsoft.IdentityModel.Logging" Version="8.18.0" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.18.0" />
</ItemGroup>
</Project>
Submodule
+1
Submodule OCORE added at 9f4826767f
+1
Submodule OCORE_Charting added at fcb8f090d4
Submodule
+1
Submodule OCORE_web added at fd5b23ec77
Submodule
+1
Submodule OCORE_web_pdf added at a94ddf90c3