🧱 Render email based on razor components
This commit is contained in:
80
DysonNetwork.Sphere/Account/Email/EmailService.cs
Normal file
80
DysonNetwork.Sphere/Account/Email/EmailService.cs
Normal file
@ -0,0 +1,80 @@
|
||||
using MailKit.Net.Smtp;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MimeKit;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account.Email;
|
||||
|
||||
public class EmailServiceConfiguration
|
||||
{
|
||||
public string Server { get; set; } = null!;
|
||||
public int Port { get; set; }
|
||||
public bool UseSsl { get; set; }
|
||||
public string Username { get; set; } = null!;
|
||||
public string Password { get; set; } = null!;
|
||||
public string FromAddress { get; set; } = null!;
|
||||
public string FromName { get; set; } = null!;
|
||||
public string SubjectPrefix { get; set; } = null!;
|
||||
}
|
||||
|
||||
public class EmailService
|
||||
{
|
||||
private readonly EmailServiceConfiguration _configuration;
|
||||
private readonly RazorViewRenderer _viewRenderer;
|
||||
private readonly ILogger<EmailService> _logger;
|
||||
|
||||
public EmailService(IConfiguration configuration, RazorViewRenderer viewRenderer, ILogger<EmailService> logger)
|
||||
{
|
||||
var cfg = configuration.GetSection("Email").Get<EmailServiceConfiguration>();
|
||||
_configuration = cfg ?? throw new ArgumentException("Email service was not configured.");
|
||||
_viewRenderer = viewRenderer;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task SendEmailAsync(string? recipientName, string recipientEmail, string subject, string textBody)
|
||||
{
|
||||
await SendEmailAsync(recipientName, recipientEmail, subject, textBody, null);
|
||||
}
|
||||
|
||||
public async Task SendEmailAsync(string? recipientName, string recipientEmail, string subject, string textBody,
|
||||
string? htmlBody)
|
||||
{
|
||||
subject = $"[{_configuration.SubjectPrefix}] {subject}";
|
||||
|
||||
var emailMessage = new MimeMessage();
|
||||
emailMessage.From.Add(new MailboxAddress(_configuration.FromName, _configuration.FromAddress));
|
||||
emailMessage.To.Add(new MailboxAddress(recipientName, recipientEmail));
|
||||
emailMessage.Subject = subject;
|
||||
|
||||
var bodyBuilder = new BodyBuilder
|
||||
{
|
||||
TextBody = textBody
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(htmlBody))
|
||||
bodyBuilder.HtmlBody = htmlBody;
|
||||
|
||||
emailMessage.Body = bodyBuilder.ToMessageBody();
|
||||
|
||||
using var client = new SmtpClient();
|
||||
await client.ConnectAsync(_configuration.Server, _configuration.Port, _configuration.UseSsl);
|
||||
await client.AuthenticateAsync(_configuration.Username, _configuration.Password);
|
||||
await client.SendAsync(emailMessage);
|
||||
await client.DisconnectAsync(true);
|
||||
}
|
||||
|
||||
public async Task SendTemplatedEmailAsync<TComponent, TModel>(string? recipientName, string recipientEmail,
|
||||
string subject, TModel model, string fallbackTextBody)
|
||||
where TComponent : IComponent
|
||||
{
|
||||
try
|
||||
{
|
||||
var htmlBody = await _viewRenderer.RenderComponentToStringAsync<TComponent, TModel>(model);
|
||||
await SendEmailAsync(recipientName, recipientEmail, subject, fallbackTextBody, htmlBody);
|
||||
}
|
||||
catch (Exception err)
|
||||
{
|
||||
_logger.LogError(err, "Failed to render email template...");
|
||||
await SendEmailAsync(recipientName, recipientEmail, subject, fallbackTextBody);
|
||||
}
|
||||
}
|
||||
}
|
7
DysonNetwork.Sphere/Account/Email/LandingEmail.cs
Normal file
7
DysonNetwork.Sphere/Account/Email/LandingEmail.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace DysonNetwork.Sphere.Account.Email;
|
||||
|
||||
public class LandingEmailModel
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
public required string VerificationLink { get; set; }
|
||||
}
|
47
DysonNetwork.Sphere/Account/Email/RazorViewRenderer.cs
Normal file
47
DysonNetwork.Sphere/Account/Email/RazorViewRenderer.cs
Normal file
@ -0,0 +1,47 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.Razor;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.ViewEngines;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using RouteData = Microsoft.AspNetCore.Routing.RouteData;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account.Email;
|
||||
|
||||
public class RazorViewRenderer(
|
||||
IServiceProvider serviceProvider,
|
||||
ILoggerFactory loggerFactory,
|
||||
ILogger<RazorViewRenderer> logger
|
||||
)
|
||||
{
|
||||
public async Task<string> RenderComponentToStringAsync<TComponent, TModel>(TModel model)
|
||||
where TComponent : IComponent
|
||||
{
|
||||
await using var htmlRenderer = new HtmlRenderer(serviceProvider, loggerFactory);
|
||||
|
||||
var viewDictionary = new ViewDataDictionary<TModel>(
|
||||
new EmptyModelMetadataProvider(),
|
||||
new ModelStateDictionary())
|
||||
{
|
||||
Model = model
|
||||
};
|
||||
|
||||
return await htmlRenderer.Dispatcher.InvokeAsync(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var parameterView = ParameterView.FromDictionary(viewDictionary);
|
||||
var output = await htmlRenderer.RenderComponentAsync<TComponent>(parameterView);
|
||||
return output.ToHtmlString();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error rendering component {ComponentName}", typeof(TComponent).Name);
|
||||
throw;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
using MailKit.Net.Smtp;
|
||||
using MimeKit;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
|
||||
public class EmailServiceConfiguration
|
||||
{
|
||||
public string Server { get; set; }
|
||||
public int Port { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string FromAddress { get; set; }
|
||||
public string FromName { get; set; }
|
||||
public string SubjectPrefix { get; set; }
|
||||
}
|
||||
|
||||
public class EmailService
|
||||
{
|
||||
private readonly EmailServiceConfiguration _configuration;
|
||||
|
||||
public EmailService(IConfiguration configuration)
|
||||
{
|
||||
var cfg = configuration.GetSection("Email").Get<EmailServiceConfiguration>();
|
||||
_configuration = cfg ?? throw new ArgumentException("Email service was not configured.");
|
||||
}
|
||||
|
||||
public async Task SendEmailAsync(string? recipientName, string recipientEmail, string subject, string textBody)
|
||||
{
|
||||
subject = $"[{_configuration.SubjectPrefix}] {subject}";
|
||||
|
||||
var emailMessage = new MimeMessage();
|
||||
emailMessage.From.Add(new MailboxAddress(_configuration.FromName, _configuration.FromAddress));
|
||||
emailMessage.To.Add(new MailboxAddress(recipientName, recipientEmail));
|
||||
emailMessage.Subject = subject;
|
||||
|
||||
var bodyBuilder = new BodyBuilder
|
||||
{
|
||||
TextBody = textBody
|
||||
};
|
||||
|
||||
emailMessage.Body = bodyBuilder.ToMessageBody();
|
||||
|
||||
using var client = new SmtpClient();
|
||||
await client.ConnectAsync(_configuration.Server, _configuration.Port, true);
|
||||
await client.AuthenticateAsync(_configuration.Username, _configuration.Password);
|
||||
await client.SendAsync(emailMessage);
|
||||
await client.DisconnectAsync(true);
|
||||
}
|
||||
}
|
@ -1,11 +1,13 @@
|
||||
using System.Security.Cryptography;
|
||||
using DysonNetwork.Sphere.Account.Email;
|
||||
using DysonNetwork.Sphere.Pages.Emails;
|
||||
using DysonNetwork.Sphere.Permission;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Sphere.Account;
|
||||
|
||||
public class MagicSpellService(AppDatabase db, EmailService email, ILogger<MagicSpellService> logger)
|
||||
public class MagicSpellService(AppDatabase db, EmailService email, IConfiguration configuration, ILogger<MagicSpellService> logger)
|
||||
{
|
||||
public async Task<MagicSpell> CreateMagicSpell(
|
||||
Account account,
|
||||
@ -42,23 +44,25 @@ public class MagicSpellService(AppDatabase db, EmailService email, ILogger<Magic
|
||||
.FirstOrDefaultAsync();
|
||||
if (contact is null) throw new ArgumentException("Account has no contact method that can use");
|
||||
|
||||
// TODO replace the baseurl
|
||||
var link = $"https://api.sn.solsynth.dev/spells/{Uri.EscapeDataString(spell.Spell)}";
|
||||
var link = $"${configuration.GetValue<string>("BaseUrl")}/spells/{Uri.EscapeDataString(spell.Spell)}";
|
||||
|
||||
logger.LogError($"Sending magic spell... {link}");
|
||||
logger.LogInformation($"Sending magic spell... {link}");
|
||||
|
||||
try
|
||||
{
|
||||
switch (spell.Type)
|
||||
{
|
||||
case MagicSpellType.AccountActivation:
|
||||
await email.SendEmailAsync(
|
||||
await email.SendTemplatedEmailAsync<LandingEmail, LandingEmailModel>(
|
||||
contact.Account.Name,
|
||||
contact.Content,
|
||||
"Confirm your registration",
|
||||
"Thank you for creating an account.\n" +
|
||||
"For accessing all the features, confirm your registration with the link below:\n\n" +
|
||||
$"{link}"
|
||||
new LandingEmailModel
|
||||
{
|
||||
Name = contact.Account.Name,
|
||||
VerificationLink = link
|
||||
},
|
||||
$"Thank you for creating an account.\nFor accessing all the features, confirm your registration with the link below:\n\n{link}"
|
||||
);
|
||||
break;
|
||||
default:
|
||||
|
Reference in New Issue
Block a user