From 7b67c1e2b0c3dd1286714a15f5522bb0c601bfc2 Mon Sep 17 00:00:00 2001 From: Morgana Date: Wed, 1 Apr 2026 16:27:20 -0300 Subject: [PATCH 1/3] feat: add welcome email for user registration - Add SendWelcomeEmailToUserAsync method to IEmailService interface - Implement method in SmtpEmailService and SendGridEmailService with branded HTML template - Inject IEmailService into AuthApplication and call it fire-and-forget after successful registration - Email includes: congratulations message, next steps, ClientManager branding - No PDF attachment (unlike customer welcome email) - Update AuthApplicationTests to include IEmailService mock --- .../AuthApplication.cs | 16 +++++ .../Interfaces/Services/IEmailService.cs | 2 + .../Services/SendGridEmailService.cs | 67 +++++++++++++++++++ .../Services/SmtpEmailService.cs | 67 +++++++++++++++++++ .../AuthApplicationTests.cs | 3 + 5 files changed, 155 insertions(+) diff --git a/src/ClientManager.Application/AuthApplication.cs b/src/ClientManager.Application/AuthApplication.cs index fb0d59d..fe569e8 100644 --- a/src/ClientManager.Application/AuthApplication.cs +++ b/src/ClientManager.Application/AuthApplication.cs @@ -8,6 +8,7 @@ public class AuthApplication : IAuthApplication { private readonly IUserService _userService; private readonly ITokenService _tokenService; + private readonly IEmailService _emailService; private readonly IValidator _createUserValidator; private readonly IValidator _loginValidator; @@ -17,11 +18,13 @@ public class AuthApplication : IAuthApplication public AuthApplication( IUserService userService, ITokenService tokenService, + IEmailService emailService, IValidator createUserValidator, IValidator loginValidator) { _userService = userService; _tokenService = tokenService; + _emailService = emailService; _createUserValidator = createUserValidator; _loginValidator = loginValidator; } @@ -61,6 +64,19 @@ public AuthApplication( ExpiresAt = DateTimeOffset.UtcNow.AddMinutes(60) }; + // Fire-and-forget: send welcome email without blocking the response + _ = Task.Run(async () => + { + try + { + await _emailService.SendWelcomeEmailToUserAsync(user.Email, user.Username); + } + catch + { + // Silently ignore errors to not affect the registration response + } + }); + return ServiceResponse.Ok(authResponse, "UserRegistered"); } diff --git a/src/ClientManager.Domain.Core/Interfaces/Services/IEmailService.cs b/src/ClientManager.Domain.Core/Interfaces/Services/IEmailService.cs index 40b7bba..47c2b04 100644 --- a/src/ClientManager.Domain.Core/Interfaces/Services/IEmailService.cs +++ b/src/ClientManager.Domain.Core/Interfaces/Services/IEmailService.cs @@ -3,4 +3,6 @@ namespace ClientManager.Domain.Core.Interfaces.Services; public interface IEmailService { Task SendWelcomeEmailAsync(string email, string name, byte[]? attachment = null, string? attachmentName = null); + + Task SendWelcomeEmailToUserAsync(string email, string username); } diff --git a/src/ClientManager.Infrastructure/Services/SendGridEmailService.cs b/src/ClientManager.Infrastructure/Services/SendGridEmailService.cs index 1e51b53..41131ba 100644 --- a/src/ClientManager.Infrastructure/Services/SendGridEmailService.cs +++ b/src/ClientManager.Infrastructure/Services/SendGridEmailService.cs @@ -73,4 +73,71 @@ public async Task SendWelcomeEmailAsync(string email, string name, byte[]? attac logger.LogError(ex, "Exception while sending email to {Email} via SendGrid", email); } } + + public async Task SendWelcomeEmailToUserAsync(string email, string username) + { + var apiKey = configuration["SendGrid:ApiKey"]; + var fromEmail = configuration["SendGrid:FromEmail"] ?? "no-reply@clientmanager.com"; + var fromName = configuration["SendGrid:FromName"] ?? "ClientManager Team"; + + if (string.IsNullOrEmpty(apiKey) || apiKey == "YOUR_SENDGRID_API_KEY") + { + logger.LogWarning("SendGrid ApiKey not configured. Skipping welcome email to {Email}", email); + return; + } + + var client = new SendGridClient(apiKey); + var from = new EmailAddress(fromEmail, fromName); + var subject = "Welcome to ClientManager!"; + var to = new EmailAddress(email, username); + + var plainTextContent = $"Hello {username}, welcome to ClientManager! Your registration has been completed successfully."; + var htmlContent = $@" +
+
+

Welcome to ClientManager!

+
+
+

Hello {username},

+

Congratulations! Your registration at ClientManager has been completed successfully.

+

You now have full access to our client and document management platform.

+
+

Next steps:

+
    +
  • Access your account and complete your profile
  • +
  • Start registering your clients
  • +
  • Upload and organize your documents
  • +
  • Explore all available features
  • +
+
+

If you have any questions, our team is ready to help.

+

Best regards,
ClientManager Team

+
+
+

© {DateTime.UtcNow.Year} ClientManager. All rights reserved.

+
+
"; + + var msg = MailHelper.CreateSingleEmail(from, to, subject, plainTextContent, htmlContent); + + try + { + var response = await client.SendEmailAsync(msg); + + if (response.IsSuccessStatusCode) + { + logger.LogInformation("Welcome email sent successfully to {Email} via SendGrid", email); + } + else + { + var body = await response.Body.ReadAsStringAsync(); + logger.LogError("Failed to send welcome email to {Email}. Status: {Status}. Error: {Error}", + email, response.StatusCode, body); + } + } + catch (Exception ex) + { + logger.LogError(ex, "Exception while sending welcome email to {Email} via SendGrid", email); + } + } } diff --git a/src/ClientManager.Infrastructure/Services/SmtpEmailService.cs b/src/ClientManager.Infrastructure/Services/SmtpEmailService.cs index c2a6c08..1abcf50 100644 --- a/src/ClientManager.Infrastructure/Services/SmtpEmailService.cs +++ b/src/ClientManager.Infrastructure/Services/SmtpEmailService.cs @@ -57,4 +57,71 @@ public async Task SendWelcomeEmailAsync(string email, string name, byte[]? attac logger.LogError(ex, "Exception while sending email to {Email} via SMTP", email); } } + + public async Task SendWelcomeEmailToUserAsync(string email, string username) + { + var host = configuration["Smtp:Host"]; + var port = int.Parse(configuration["Smtp:Port"] ?? "587"); + var username2 = configuration["Smtp:Username"]; + var password = configuration["Smtp:Password"]; + var fromEmail = configuration["Smtp:FromEmail"] ?? "no-reply@clientmanager.com"; + var fromName = configuration["Smtp:FromName"] ?? "ClientManager Team"; + + if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(username2)) + { + logger.LogWarning("SMTP not configured. Skipping welcome email to {Email}", email); + return; + } + + try + { + using var client = new SmtpClient(host, port) + { + Credentials = new NetworkCredential(username2, password), + EnableSsl = true + }; + + var htmlContent = $@" +
+
+

Welcome to ClientManager!

+
+
+

Hello {username},

+

Congratulations! Your registration at ClientManager has been completed successfully.

+

You now have full access to our client and document management platform.

+
+

Next steps:

+
    +
  • Access your account and complete your profile
  • +
  • Start registering your clients
  • +
  • Upload and organize your documents
  • +
  • Explore all available features
  • +
+
+

If you have any questions, our team is ready to help.

+

Best regards,
ClientManager Team

+
+
+

© {DateTime.UtcNow.Year} ClientManager. All rights reserved.

+
+
"; + + var mailMessage = new MailMessage + { + From = new MailAddress(fromEmail, fromName), + Subject = "Welcome to ClientManager!", + Body = htmlContent, + IsBodyHtml = true + }; + mailMessage.To.Add(new MailAddress(email, username)); + + await client.SendMailAsync(mailMessage); + logger.LogInformation("Welcome email sent successfully to {Email} via SMTP ({Host})", email, host); + } + catch (Exception ex) + { + logger.LogError(ex, "Exception while sending welcome email to {Email} via SMTP", email); + } + } } diff --git a/tests/ClientManager.Application.Tests/AuthApplicationTests.cs b/tests/ClientManager.Application.Tests/AuthApplicationTests.cs index 70dc81d..79ff16f 100644 --- a/tests/ClientManager.Application.Tests/AuthApplicationTests.cs +++ b/tests/ClientManager.Application.Tests/AuthApplicationTests.cs @@ -14,6 +14,7 @@ public class AuthApplicationTests { private readonly Mock _userServiceMock; private readonly Mock _tokenServiceMock; + private readonly Mock _emailServiceMock; private readonly Mock> _createUserValidatorMock; private readonly Mock> _loginValidatorMock; private readonly AuthApplication _authApplication; @@ -22,12 +23,14 @@ public AuthApplicationTests() { _userServiceMock = new Mock(); _tokenServiceMock = new Mock(); + _emailServiceMock = new Mock(); _createUserValidatorMock = new Mock>(); _loginValidatorMock = new Mock>(); _authApplication = new AuthApplication( _userServiceMock.Object, _tokenServiceMock.Object, + _emailServiceMock.Object, _createUserValidatorMock.Object, _loginValidatorMock.Object ); From c3c4bdd9f6c8990957736418c31702b87e2bf0fc Mon Sep 17 00:00:00 2001 From: Morgana Date: Wed, 1 Apr 2026 16:31:12 -0300 Subject: [PATCH 2/3] fix: implement SendWelcomeEmailToUserAsync in ResendEmailService (#20) --- .../Services/ResendEmailService.cs | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/ClientManager.Infrastructure/Services/ResendEmailService.cs b/src/ClientManager.Infrastructure/Services/ResendEmailService.cs index 6e66ef4..1130cfe 100644 --- a/src/ClientManager.Infrastructure/Services/ResendEmailService.cs +++ b/src/ClientManager.Infrastructure/Services/ResendEmailService.cs @@ -71,4 +71,68 @@ public async Task SendWelcomeEmailAsync(string email, string name, byte[]? attac logger.LogError(ex, "Exception while sending email to {Email} via Resend", email); } } + + public async Task SendWelcomeEmailToUserAsync(string email, string username) + { + var apiKey = configuration["Resend:ApiKey"]; + var fromEmail = configuration["Resend:FromEmail"] ?? "onboarding@resend.dev"; + var fromName = configuration["Resend:FromName"] ?? "ClientManager"; + + if (string.IsNullOrEmpty(apiKey)) + { + logger.LogWarning("Resend API key not configured. Skipping welcome email to {Email}", email); + return; + } + + try + { + using var client = new SmtpClient("smtp.resend.com", 465) + { + Credentials = new NetworkCredential("resend", apiKey), + EnableSsl = true + }; + + var htmlContent = $@" +
+
+

Welcome to ClientManager!

+
+
+

Hello {username},

+

Congratulations! Your registration at ClientManager has been completed successfully.

+

You now have full access to our client and document management platform.

+
+

Next steps:

+
    +
  • Access your account and complete your profile
  • +
  • Start registering your clients
  • +
  • Upload and organize your documents
  • +
  • Explore all available features
  • +
+
+

If you have any questions, our team is ready to help.

+

Best regards,
ClientManager Team

+
+
+

© {DateTime.UtcNow.Year} ClientManager. All rights reserved.

+
+
"; + + var mailMessage = new MailMessage + { + From = new MailAddress(fromEmail, fromName), + Subject = "Welcome to ClientManager!", + Body = htmlContent, + IsBodyHtml = true + }; + mailMessage.To.Add(new MailAddress(email, username)); + + await client.SendMailAsync(mailMessage); + logger.LogInformation("Welcome email sent successfully to {Email} via Resend", email); + } + catch (Exception ex) + { + logger.LogError(ex, "Exception while sending welcome email to {Email} via Resend", email); + } + } } From ace7b24d64398f32d4e1c2689b8eb79087215c8f Mon Sep 17 00:00:00 2001 From: Morgana Date: Wed, 1 Apr 2026 19:37:57 +0000 Subject: [PATCH 3/3] fix(build): add CultureInfo.InvariantCulture to int.Parse in SmtpEmailService CA1305 error in Release mode because int.Parse without IFormatProvider varies by locale. This caused Docker build to fail on Render since Directory.Build.props sets TreatWarningsAsErrors=true for Release. --- src/ClientManager.Infrastructure/Services/SmtpEmailService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ClientManager.Infrastructure/Services/SmtpEmailService.cs b/src/ClientManager.Infrastructure/Services/SmtpEmailService.cs index 1abcf50..3b839d4 100644 --- a/src/ClientManager.Infrastructure/Services/SmtpEmailService.cs +++ b/src/ClientManager.Infrastructure/Services/SmtpEmailService.cs @@ -61,7 +61,7 @@ public async Task SendWelcomeEmailAsync(string email, string name, byte[]? attac public async Task SendWelcomeEmailToUserAsync(string email, string username) { var host = configuration["Smtp:Host"]; - var port = int.Parse(configuration["Smtp:Port"] ?? "587"); + var port = int.Parse(configuration["Smtp:Port"] ?? "587", CultureInfo.InvariantCulture); var username2 = configuration["Smtp:Username"]; var password = configuration["Smtp:Password"]; var fromEmail = configuration["Smtp:FromEmail"] ?? "no-reply@clientmanager.com";