feat(backend): Implement email service infrastructure for Day 7
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 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,18 @@
|
|||||||
"PMDatabase": "Host=localhost;Port=5432;Database=colaflow_pm;Username=colaflow;Password=colaflow_dev_password",
|
"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"
|
"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": {
|
"MediatR": {
|
||||||
"LicenseKey": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ikx1Y2t5UGVubnlTb2Z0d2FyZUxpY2Vuc2VLZXkvYmJiMTNhY2I1OTkwNGQ4OWI0Y2IxYzg1ZjA4OGNjZjkiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2x1Y2t5cGVubnlzb2Z0d2FyZS5jb20iLCJhdWQiOiJMdWNreVBlbm55U29mdHdhcmUiLCJleHAiOiIxNzkzNTc3NjAwIiwiaWF0IjoiMTc2MjEyNTU2MiIsImFjY291bnRfaWQiOiIwMTlhNDZkZGZiZjk3YTk4Yjg1ZTVmOTllNWRhZjIwNyIsImN1c3RvbWVyX2lkIjoiY3RtXzAxazkzZHdnOG0weDByanp3Mm5rM2dxeDExIiwic3ViX2lkIjoiLSIsImVkaXRpb24iOiIwIiwidHlwZSI6IjIifQ.V45vUlze27pQG3Vs9dvagyUTSp-a74ymB6I0TIGD_NwFt1mMMPsuVXOKH1qK7A7V5qDQBvYyryzJy8xRE1rRKq2MJKgyfYjvzuGkpBbKbM6JRQPYknb5tjF-Rf3LAeWp73FiqbPZOPt5saCsoKqUHej-4zcKg5GA4y-PpGaGAONKyqwK9G2rvc1BUHfEnHKRMr0pprA5W1Yx-Lry85KOckUsI043HGOdfbubnGdAZs74FKvrV2qVir6K6VsZjWwX8IFnl1CzxjICa5MxyHOAVpXRnRtMt6fpsA1fMstFuRjq_2sbqGfsTv6LyCzLPnXdmU5DnWZHUcjy0xlAT_f0aw"
|
"LicenseKey": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ikx1Y2t5UGVubnlTb2Z0d2FyZUxpY2Vuc2VLZXkvYmJiMTNhY2I1OTkwNGQ4OWI0Y2IxYzg1ZjA4OGNjZjkiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2x1Y2t5cGVubnlzb2Z0d2FyZS5jb20iLCJhdWQiOiJMdWNreVBlbm55U29mdHdhcmUiLCJleHAiOiIxNzkzNTc3NjAwIiwiaWF0IjoiMTc2MjEyNTU2MiIsImFjY291bnRfaWQiOiIwMTlhNDZkZGZiZjk3YTk4Yjg1ZTVmOTllNWRhZjIwNyIsImN1c3RvbWVyX2lkIjoiY3RtXzAxazkzZHdnOG0weDByanp3Mm5rM2dxeDExIiwic3ViX2lkIjoiLSIsImVkaXRpb24iOiIwIiwidHlwZSI6IjIifQ.V45vUlze27pQG3Vs9dvagyUTSp-a74ymB6I0TIGD_NwFt1mMMPsuVXOKH1qK7A7V5qDQBvYyryzJy8xRE1rRKq2MJKgyfYjvzuGkpBbKbM6JRQPYknb5tjF-Rf3LAeWp73FiqbPZOPt5saCsoKqUHej-4zcKg5GA4y-PpGaGAONKyqwK9G2rvc1BUHfEnHKRMr0pprA5W1Yx-Lry85KOckUsI043HGOdfbubnGdAZs74FKvrV2qVir6K6VsZjWwX8IFnl1CzxjICa5MxyHOAVpXRnRtMt6fpsA1fMstFuRjq_2sbqGfsTv6LyCzLPnXdmU5DnWZHUcjy0xlAT_f0aw"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using ColaFlow.Modules.Identity.Domain.Services;
|
||||||
|
|
||||||
|
namespace ColaFlow.Modules.Identity.Application.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service for sending emails
|
||||||
|
/// </summary>
|
||||||
|
public interface IEmailService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sends an email message
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The email message to send</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <returns>True if email was sent successfully, false otherwise</returns>
|
||||||
|
Task<bool> SendEmailAsync(EmailMessage message, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
namespace ColaFlow.Modules.Identity.Application.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service for rendering email templates
|
||||||
|
/// </summary>
|
||||||
|
public interface IEmailTemplateService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Renders a verification email template
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="recipientName">Name of the recipient</param>
|
||||||
|
/// <param name="verificationUrl">URL for email verification</param>
|
||||||
|
/// <returns>HTML email body</returns>
|
||||||
|
string RenderVerificationEmail(string recipientName, string verificationUrl);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Renders a password reset email template
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="recipientName">Name of the recipient</param>
|
||||||
|
/// <param name="resetUrl">URL for password reset</param>
|
||||||
|
/// <returns>HTML email body</returns>
|
||||||
|
string RenderPasswordResetEmail(string recipientName, string resetUrl);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Renders a tenant invitation email template
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="recipientName">Name of the recipient</param>
|
||||||
|
/// <param name="tenantName">Name of the tenant</param>
|
||||||
|
/// <param name="inviterName">Name of the person who sent the invitation</param>
|
||||||
|
/// <param name="invitationUrl">URL for accepting the invitation</param>
|
||||||
|
/// <returns>HTML email body</returns>
|
||||||
|
string RenderInvitationEmail(
|
||||||
|
string recipientName,
|
||||||
|
string tenantName,
|
||||||
|
string inviterName,
|
||||||
|
string invitationUrl);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace ColaFlow.Modules.Identity.Domain.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an email message to be sent
|
||||||
|
/// </summary>
|
||||||
|
public sealed record EmailMessage(
|
||||||
|
string To,
|
||||||
|
string Subject,
|
||||||
|
string HtmlBody,
|
||||||
|
string? PlainTextBody = null,
|
||||||
|
string? FromEmail = null,
|
||||||
|
string? FromName = null
|
||||||
|
);
|
||||||
@@ -43,6 +43,18 @@ public static class DependencyInjection
|
|||||||
services.AddScoped<IPasswordHasher, PasswordHasher>();
|
services.AddScoped<IPasswordHasher, PasswordHasher>();
|
||||||
services.AddScoped<IRefreshTokenService, RefreshTokenService>();
|
services.AddScoped<IRefreshTokenService, RefreshTokenService>();
|
||||||
|
|
||||||
|
// Email Services
|
||||||
|
var emailProvider = configuration["Email:Provider"] ?? "Mock";
|
||||||
|
if (emailProvider.Equals("Mock", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
services.AddSingleton<IEmailService, MockEmailService>();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
services.AddScoped<IEmailService, SmtpEmailService>();
|
||||||
|
}
|
||||||
|
services.AddScoped<IEmailTemplateService, EmailTemplateService>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
using ColaFlow.Modules.Identity.Application.Services;
|
||||||
|
|
||||||
|
namespace ColaFlow.Modules.Identity.Infrastructure.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service for rendering HTML email templates
|
||||||
|
/// </summary>
|
||||||
|
public sealed class EmailTemplateService : IEmailTemplateService
|
||||||
|
{
|
||||||
|
public string RenderVerificationEmail(string recipientName, string verificationUrl)
|
||||||
|
{
|
||||||
|
return $@"
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset=""utf-8"">
|
||||||
|
<meta name=""viewport"" content=""width=device-width, initial-scale=1.0"">
|
||||||
|
<title>Verify Your Email</title>
|
||||||
|
<style>
|
||||||
|
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; }}
|
||||||
|
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
|
||||||
|
.header {{ background-color: #4F46E5; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0; }}
|
||||||
|
.content {{ background-color: #f9fafb; padding: 30px; border-radius: 0 0 8px 8px; }}
|
||||||
|
.button {{ display: inline-block; background-color: #4F46E5; color: white; padding: 12px 30px; text-decoration: none; border-radius: 6px; margin: 20px 0; }}
|
||||||
|
.footer {{ text-align: center; margin-top: 20px; font-size: 12px; color: #666; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class=""container"">
|
||||||
|
<div class=""header"">
|
||||||
|
<h1>ColaFlow</h1>
|
||||||
|
</div>
|
||||||
|
<div class=""content"">
|
||||||
|
<h2>Welcome to ColaFlow, {recipientName}!</h2>
|
||||||
|
<p>Thank you for registering. Please verify your email address by clicking the button below:</p>
|
||||||
|
<div style=""text-align: center;"">
|
||||||
|
<a href=""{verificationUrl}"" class=""button"">Verify Email Address</a>
|
||||||
|
</div>
|
||||||
|
<p>Or copy and paste this link into your browser:</p>
|
||||||
|
<p style=""word-break: break-all; color: #4F46E5;"">{verificationUrl}</p>
|
||||||
|
<p>This link will expire in 24 hours.</p>
|
||||||
|
<p>If you didn't create an account, you can safely ignore this email.</p>
|
||||||
|
</div>
|
||||||
|
<div class=""footer"">
|
||||||
|
<p>© 2025 ColaFlow. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string RenderPasswordResetEmail(string recipientName, string resetUrl)
|
||||||
|
{
|
||||||
|
return $@"
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset=""utf-8"">
|
||||||
|
<meta name=""viewport"" content=""width=device-width, initial-scale=1.0"">
|
||||||
|
<title>Reset Your Password</title>
|
||||||
|
<style>
|
||||||
|
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; }}
|
||||||
|
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
|
||||||
|
.header {{ background-color: #DC2626; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0; }}
|
||||||
|
.content {{ background-color: #f9fafb; padding: 30px; border-radius: 0 0 8px 8px; }}
|
||||||
|
.button {{ display: inline-block; background-color: #DC2626; color: white; padding: 12px 30px; text-decoration: none; border-radius: 6px; margin: 20px 0; }}
|
||||||
|
.footer {{ text-align: center; margin-top: 20px; font-size: 12px; color: #666; }}
|
||||||
|
.warning {{ background-color: #FEF3C7; border-left: 4px solid #F59E0B; padding: 12px; margin: 20px 0; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class=""container"">
|
||||||
|
<div class=""header"">
|
||||||
|
<h1>ColaFlow</h1>
|
||||||
|
</div>
|
||||||
|
<div class=""content"">
|
||||||
|
<h2>Password Reset Request</h2>
|
||||||
|
<p>Hi {recipientName},</p>
|
||||||
|
<p>We received a request to reset your password. Click the button below to create a new password:</p>
|
||||||
|
<div style=""text-align: center;"">
|
||||||
|
<a href=""{resetUrl}"" class=""button"">Reset Password</a>
|
||||||
|
</div>
|
||||||
|
<p>Or copy and paste this link into your browser:</p>
|
||||||
|
<p style=""word-break: break-all; color: #DC2626;"">{resetUrl}</p>
|
||||||
|
<div class=""warning"">
|
||||||
|
<strong>Important:</strong> This link will expire in 1 hour for security reasons.
|
||||||
|
</div>
|
||||||
|
<p>If you didn't request a password reset, please ignore this email or contact support if you have concerns.</p>
|
||||||
|
</div>
|
||||||
|
<div class=""footer"">
|
||||||
|
<p>© 2025 ColaFlow. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string RenderInvitationEmail(
|
||||||
|
string recipientName,
|
||||||
|
string tenantName,
|
||||||
|
string inviterName,
|
||||||
|
string invitationUrl)
|
||||||
|
{
|
||||||
|
return $@"
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset=""utf-8"">
|
||||||
|
<meta name=""viewport"" content=""width=device-width, initial-scale=1.0"">
|
||||||
|
<title>You've Been Invited</title>
|
||||||
|
<style>
|
||||||
|
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; }}
|
||||||
|
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
|
||||||
|
.header {{ background-color: #10B981; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0; }}
|
||||||
|
.content {{ background-color: #f9fafb; padding: 30px; border-radius: 0 0 8px 8px; }}
|
||||||
|
.button {{ display: inline-block; background-color: #10B981; color: white; padding: 12px 30px; text-decoration: none; border-radius: 6px; margin: 20px 0; }}
|
||||||
|
.footer {{ text-align: center; margin-top: 20px; font-size: 12px; color: #666; }}
|
||||||
|
.info-box {{ background-color: #E0F2FE; border-left: 4px solid #0EA5E9; padding: 12px; margin: 20px 0; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class=""container"">
|
||||||
|
<div class=""header"">
|
||||||
|
<h1>ColaFlow</h1>
|
||||||
|
</div>
|
||||||
|
<div class=""content"">
|
||||||
|
<h2>You've Been Invited!</h2>
|
||||||
|
<p>Hi {recipientName},</p>
|
||||||
|
<p><strong>{inviterName}</strong> has invited you to join the <strong>{tenantName}</strong> workspace on ColaFlow.</p>
|
||||||
|
<div class=""info-box"">
|
||||||
|
<strong>Workspace:</strong> {tenantName}<br>
|
||||||
|
<strong>Invited by:</strong> {inviterName}
|
||||||
|
</div>
|
||||||
|
<p>Click the button below to accept the invitation and get started:</p>
|
||||||
|
<div style=""text-align: center;"">
|
||||||
|
<a href=""{invitationUrl}"" class=""button"">Accept Invitation</a>
|
||||||
|
</div>
|
||||||
|
<p>Or copy and paste this link into your browser:</p>
|
||||||
|
<p style=""word-break: break-all; color: #10B981;"">{invitationUrl}</p>
|
||||||
|
<p>This invitation will expire in 7 days.</p>
|
||||||
|
<p>If you don't know {inviterName} or weren't expecting this invitation, you can safely ignore this email.</p>
|
||||||
|
</div>
|
||||||
|
<div class=""footer"">
|
||||||
|
<p>© 2025 ColaFlow. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mock email service for development/testing that logs emails instead of sending them
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MockEmailService : IEmailService
|
||||||
|
{
|
||||||
|
private readonly ILogger<MockEmailService> _logger;
|
||||||
|
|
||||||
|
public MockEmailService(ILogger<MockEmailService> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<bool> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SMTP-based email service for production use
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SmtpEmailService : IEmailService
|
||||||
|
{
|
||||||
|
private readonly ILogger<SmtpEmailService> _logger;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
public SmtpEmailService(
|
||||||
|
ILogger<SmtpEmailService> logger,
|
||||||
|
IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user