From 921990a0438f3e1a95cc7f73113050fd82ef0748 Mon Sep 17 00:00:00 2001 From: Yaojia Wang Date: Mon, 3 Nov 2025 21:16:11 +0100 Subject: [PATCH] feat(backend): Implement email service infrastructure for Day 7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add complete email service infrastructure with Mock and SMTP implementations. Changes: - Created EmailMessage domain model for email data - Added IEmailService interface for email sending - Implemented MockEmailService for development/testing (logs emails) - Implemented SmtpEmailService for production SMTP sending - Added IEmailTemplateService interface for email templates - Implemented EmailTemplateService with HTML templates for verification, password reset, and invitation emails - Registered email services in DependencyInjection with provider selection - Added email configuration to appsettings.Development.json (Mock provider by default) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../ColaFlow.API/appsettings.Development.json | 12 ++ .../Services/IEmailService.cs | 17 ++ .../Services/IEmailTemplateService.cs | 37 +++++ .../Services/EmailMessage.cs | 13 ++ .../DependencyInjection.cs | 12 ++ .../Services/EmailTemplateService.cs | 150 ++++++++++++++++++ .../Services/MockEmailService.cs | 34 ++++ .../Services/SmtpEmailService.cs | 87 ++++++++++ 8 files changed, 362 insertions(+) create mode 100644 colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Services/IEmailService.cs create mode 100644 colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Services/IEmailTemplateService.cs create mode 100644 colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Services/EmailMessage.cs create mode 100644 colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/Services/EmailTemplateService.cs create mode 100644 colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/Services/MockEmailService.cs create mode 100644 colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/Services/SmtpEmailService.cs diff --git a/colaflow-api/src/ColaFlow.API/appsettings.Development.json b/colaflow-api/src/ColaFlow.API/appsettings.Development.json index 0ffb605..a24b6fe 100644 --- a/colaflow-api/src/ColaFlow.API/appsettings.Development.json +++ b/colaflow-api/src/ColaFlow.API/appsettings.Development.json @@ -10,6 +10,18 @@ "PMDatabase": "Host=localhost;Port=5432;Database=colaflow_pm;Username=colaflow;Password=colaflow_dev_password", "DefaultConnection": "Host=localhost;Port=5432;Database=colaflow_identity;Username=colaflow;Password=colaflow_dev_password" }, + "Email": { + "Provider": "Mock", + "From": "noreply@colaflow.local", + "FromName": "ColaFlow", + "Smtp": { + "Host": "smtp.example.com", + "Port": "587", + "Username": "", + "Password": "", + "EnableSsl": "true" + } + }, "MediatR": { "LicenseKey": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ikx1Y2t5UGVubnlTb2Z0d2FyZUxpY2Vuc2VLZXkvYmJiMTNhY2I1OTkwNGQ4OWI0Y2IxYzg1ZjA4OGNjZjkiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2x1Y2t5cGVubnlzb2Z0d2FyZS5jb20iLCJhdWQiOiJMdWNreVBlbm55U29mdHdhcmUiLCJleHAiOiIxNzkzNTc3NjAwIiwiaWF0IjoiMTc2MjEyNTU2MiIsImFjY291bnRfaWQiOiIwMTlhNDZkZGZiZjk3YTk4Yjg1ZTVmOTllNWRhZjIwNyIsImN1c3RvbWVyX2lkIjoiY3RtXzAxazkzZHdnOG0weDByanp3Mm5rM2dxeDExIiwic3ViX2lkIjoiLSIsImVkaXRpb24iOiIwIiwidHlwZSI6IjIifQ.V45vUlze27pQG3Vs9dvagyUTSp-a74ymB6I0TIGD_NwFt1mMMPsuVXOKH1qK7A7V5qDQBvYyryzJy8xRE1rRKq2MJKgyfYjvzuGkpBbKbM6JRQPYknb5tjF-Rf3LAeWp73FiqbPZOPt5saCsoKqUHej-4zcKg5GA4y-PpGaGAONKyqwK9G2rvc1BUHfEnHKRMr0pprA5W1Yx-Lry85KOckUsI043HGOdfbubnGdAZs74FKvrV2qVir6K6VsZjWwX8IFnl1CzxjICa5MxyHOAVpXRnRtMt6fpsA1fMstFuRjq_2sbqGfsTv6LyCzLPnXdmU5DnWZHUcjy0xlAT_f0aw" }, diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Services/IEmailService.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Services/IEmailService.cs new file mode 100644 index 0000000..a888c7f --- /dev/null +++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Services/IEmailService.cs @@ -0,0 +1,17 @@ +using ColaFlow.Modules.Identity.Domain.Services; + +namespace ColaFlow.Modules.Identity.Application.Services; + +/// +/// Service for sending emails +/// +public interface IEmailService +{ + /// + /// Sends an email message + /// + /// The email message to send + /// Cancellation token + /// True if email was sent successfully, false otherwise + Task SendEmailAsync(EmailMessage message, CancellationToken cancellationToken = default); +} diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Services/IEmailTemplateService.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Services/IEmailTemplateService.cs new file mode 100644 index 0000000..f82614c --- /dev/null +++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/Services/IEmailTemplateService.cs @@ -0,0 +1,37 @@ +namespace ColaFlow.Modules.Identity.Application.Services; + +/// +/// Service for rendering email templates +/// +public interface IEmailTemplateService +{ + /// + /// Renders a verification email template + /// + /// Name of the recipient + /// URL for email verification + /// HTML email body + string RenderVerificationEmail(string recipientName, string verificationUrl); + + /// + /// Renders a password reset email template + /// + /// Name of the recipient + /// URL for password reset + /// HTML email body + string RenderPasswordResetEmail(string recipientName, string resetUrl); + + /// + /// Renders a tenant invitation email template + /// + /// Name of the recipient + /// Name of the tenant + /// Name of the person who sent the invitation + /// URL for accepting the invitation + /// HTML email body + string RenderInvitationEmail( + string recipientName, + string tenantName, + string inviterName, + string invitationUrl); +} diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Services/EmailMessage.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Services/EmailMessage.cs new file mode 100644 index 0000000..d685e4e --- /dev/null +++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Services/EmailMessage.cs @@ -0,0 +1,13 @@ +namespace ColaFlow.Modules.Identity.Domain.Services; + +/// +/// Represents an email message to be sent +/// +public sealed record EmailMessage( + string To, + string Subject, + string HtmlBody, + string? PlainTextBody = null, + string? FromEmail = null, + string? FromName = null +); diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/DependencyInjection.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/DependencyInjection.cs index 8b10cb7..b771d69 100644 --- a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/DependencyInjection.cs +++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/DependencyInjection.cs @@ -43,6 +43,18 @@ public static class DependencyInjection services.AddScoped(); services.AddScoped(); + // Email Services + var emailProvider = configuration["Email:Provider"] ?? "Mock"; + if (emailProvider.Equals("Mock", StringComparison.OrdinalIgnoreCase)) + { + services.AddSingleton(); + } + else + { + services.AddScoped(); + } + services.AddScoped(); + return services; } } diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/Services/EmailTemplateService.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/Services/EmailTemplateService.cs new file mode 100644 index 0000000..284338c --- /dev/null +++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/Services/EmailTemplateService.cs @@ -0,0 +1,150 @@ +using ColaFlow.Modules.Identity.Application.Services; + +namespace ColaFlow.Modules.Identity.Infrastructure.Services; + +/// +/// Service for rendering HTML email templates +/// +public sealed class EmailTemplateService : IEmailTemplateService +{ + public string RenderVerificationEmail(string recipientName, string verificationUrl) + { + return $@" + + + + + + Verify Your Email + + + +
+
+

ColaFlow

+
+
+

Welcome to ColaFlow, {recipientName}!

+

Thank you for registering. Please verify your email address by clicking the button below:

+ +

Or copy and paste this link into your browser:

+

{verificationUrl}

+

This link will expire in 24 hours.

+

If you didn't create an account, you can safely ignore this email.

+
+
+

© 2025 ColaFlow. All rights reserved.

+
+
+ +"; + } + + public string RenderPasswordResetEmail(string recipientName, string resetUrl) + { + return $@" + + + + + + Reset Your Password + + + +
+
+

ColaFlow

+
+
+

Password Reset Request

+

Hi {recipientName},

+

We received a request to reset your password. Click the button below to create a new password:

+ +

Or copy and paste this link into your browser:

+

{resetUrl}

+
+ Important: This link will expire in 1 hour for security reasons. +
+

If you didn't request a password reset, please ignore this email or contact support if you have concerns.

+
+
+

© 2025 ColaFlow. All rights reserved.

+
+
+ +"; + } + + public string RenderInvitationEmail( + string recipientName, + string tenantName, + string inviterName, + string invitationUrl) + { + return $@" + + + + + + You've Been Invited + + + +
+
+

ColaFlow

+
+
+

You've Been Invited!

+

Hi {recipientName},

+

{inviterName} has invited you to join the {tenantName} workspace on ColaFlow.

+
+ Workspace: {tenantName}
+ Invited by: {inviterName} +
+

Click the button below to accept the invitation and get started:

+ +

Or copy and paste this link into your browser:

+

{invitationUrl}

+

This invitation will expire in 7 days.

+

If you don't know {inviterName} or weren't expecting this invitation, you can safely ignore this email.

+
+
+

© 2025 ColaFlow. All rights reserved.

+
+
+ +"; + } +} diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/Services/MockEmailService.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/Services/MockEmailService.cs new file mode 100644 index 0000000..069cb85 --- /dev/null +++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/Services/MockEmailService.cs @@ -0,0 +1,34 @@ +using ColaFlow.Modules.Identity.Application.Services; +using ColaFlow.Modules.Identity.Domain.Services; +using Microsoft.Extensions.Logging; + +namespace ColaFlow.Modules.Identity.Infrastructure.Services; + +/// +/// Mock email service for development/testing that logs emails instead of sending them +/// +public sealed class MockEmailService : IEmailService +{ + private readonly ILogger _logger; + + public MockEmailService(ILogger logger) + { + _logger = logger; + } + + public Task SendEmailAsync(EmailMessage message, CancellationToken cancellationToken = default) + { + _logger.LogInformation( + "[MOCK EMAIL] To: {To}, Subject: {Subject}, From: {From}", + message.To, + message.Subject, + message.FromEmail ?? "default"); + + _logger.LogDebug( + "[MOCK EMAIL] HTML Body: {HtmlBody}", + message.HtmlBody); + + // Simulate successful send + return Task.FromResult(true); + } +} diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/Services/SmtpEmailService.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/Services/SmtpEmailService.cs new file mode 100644 index 0000000..d1ee120 --- /dev/null +++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/Services/SmtpEmailService.cs @@ -0,0 +1,87 @@ +using System.Net; +using System.Net.Mail; +using ColaFlow.Modules.Identity.Application.Services; +using ColaFlow.Modules.Identity.Domain.Services; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace ColaFlow.Modules.Identity.Infrastructure.Services; + +/// +/// SMTP-based email service for production use +/// +public sealed class SmtpEmailService : IEmailService +{ + private readonly ILogger _logger; + private readonly IConfiguration _configuration; + + public SmtpEmailService( + ILogger logger, + IConfiguration configuration) + { + _logger = logger; + _configuration = configuration; + } + + public async Task SendEmailAsync(EmailMessage message, CancellationToken cancellationToken = default) + { + try + { + var smtpHost = _configuration["Email:Smtp:Host"]; + var smtpPort = int.Parse(_configuration["Email:Smtp:Port"] ?? "587"); + var smtpUsername = _configuration["Email:Smtp:Username"]; + var smtpPassword = _configuration["Email:Smtp:Password"]; + var enableSsl = bool.Parse(_configuration["Email:Smtp:EnableSsl"] ?? "true"); + + var defaultFromEmail = _configuration["Email:From"] ?? "noreply@colaflow.local"; + var defaultFromName = _configuration["Email:FromName"] ?? "ColaFlow"; + + using var smtpClient = new SmtpClient(smtpHost, smtpPort) + { + Credentials = new NetworkCredential(smtpUsername, smtpPassword), + EnableSsl = enableSsl + }; + + using var mailMessage = new MailMessage + { + From = new MailAddress( + message.FromEmail ?? defaultFromEmail, + message.FromName ?? defaultFromName), + Subject = message.Subject, + Body = message.HtmlBody, + IsBodyHtml = true + }; + + mailMessage.To.Add(message.To); + + // Add plain text alternative if provided + if (!string.IsNullOrEmpty(message.PlainTextBody)) + { + var plainView = AlternateView.CreateAlternateViewFromString( + message.PlainTextBody, + null, + "text/plain"); + mailMessage.AlternateViews.Add(plainView); + } + + await smtpClient.SendMailAsync(mailMessage, cancellationToken); + + _logger.LogInformation( + "Email sent successfully to {To} with subject: {Subject}", + message.To, + message.Subject); + + return true; + } + catch (Exception ex) + { + _logger.LogError( + ex, + "Failed to send email to {To} with subject: {Subject}", + message.To, + message.Subject); + + return false; + } + } +}