Skip to content

Commit

Permalink
Add EmailSender abstraction for sending mails using either SMTP or Ma…
Browse files Browse the repository at this point in the history
…ilgun (#293)

This PR adds an `IEmailSender` abstraction along with implementations
`SmtpEmailSender` and `MailgunEmailSender`, and refactors `EmailService`
to use said abstraction. The main idea is to allow for local email
testing using an SMTP server such as
[MailHog](https://github.com/mailhog/MailHog), which has also been added
to the Docker Compose configuration.

<img width="1494" alt="image"
src="https://github.com/user-attachments/assets/99e86a78-02eb-4e82-bcb8-da70192f6667">
  • Loading branch information
duckth authored Nov 12, 2024
1 parent f56a864 commit bfcd44b
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 31 deletions.
11 changes: 11 additions & 0 deletions coffeecard/CoffeeCard.Common/Configuration/SmtpSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;

namespace CoffeeCard.Common.Configuration
{
public class SmtpSettings
{
[Required] public string Host { get; set; }
[Required] public int Port { get; set; }
}
}

38 changes: 7 additions & 31 deletions coffeecard/CoffeeCard.Library/Services/EmailService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ public class EmailService : IEmailService
{
private readonly IWebHostEnvironment _env;
private readonly EnvironmentSettings _environmentSettings;
private readonly MailgunSettings _mailgunSettings;
private readonly IEmailSender _emailSender;
private readonly IMapperService _mapperService;

public EmailService(MailgunSettings mailgunSettings, EnvironmentSettings environmentSettings,
public EmailService(IEmailSender emailSender, EnvironmentSettings environmentSettings,
IWebHostEnvironment env, IMapperService mapperService)
{
_mailgunSettings = mailgunSettings;
_emailSender = emailSender;
_environmentSettings = environmentSettings;
_env = env;
_mapperService = mapperService;
Expand Down Expand Up @@ -54,7 +54,7 @@ public async Task SendInvoiceAsync(UserDto user, PurchaseDto purchase)

Log.Information("Sending invoice for PurchaseId {PurchaseId} to UserId {UserId}, E-mail {Email}", purchase.Id, user.Id, user.Email);

await SendEmailAsync(message);
await _emailSender.SendEmailAsync(message);
}

public async Task SendRegistrationVerificationEmailAsync(User user, string token)
Expand All @@ -71,7 +71,7 @@ public async Task SendRegistrationVerificationEmailAsync(User user, string token

message.Body = builder.ToMessageBody();

await SendEmailAsync(message);
await _emailSender.SendEmailAsync(message);
}

public async Task SendVerificationEmailForLostPwAsync(User user, string token)
Expand All @@ -87,7 +87,7 @@ public async Task SendVerificationEmailForLostPwAsync(User user, string token)

message.Body = builder.ToMessageBody();

await SendEmailAsync(message);
await _emailSender.SendEmailAsync(message);
}

public async Task SendVerificationEmailForDeleteAccount(User user, string token)
Expand All @@ -104,7 +104,7 @@ public async Task SendVerificationEmailForDeleteAccount(User user, string token)

message.Body = builder.ToMessageBody();

await SendEmailAsync(message);
await _emailSender.SendEmailAsync(message);
}

public async Task SendInvoiceAsyncV2(Purchase purchase, User user)
Expand Down Expand Up @@ -150,29 +150,5 @@ private BodyBuilder RetrieveTemplate(string templateName)

return builder;
}

private async Task SendEmailAsync(MimeMessage mail)
{
var client = new RestClient(_mailgunSettings.MailgunApiUrl)
{
Authenticator = new HttpBasicAuthenticator("api", _mailgunSettings.ApiKey)
};

var request = new RestRequest();
request.AddParameter("domain", _mailgunSettings.Domain, ParameterType.UrlSegment);
request.Resource = "{domain}/messages";
request.AddParameter("from", "Cafe Analog <[email protected]>");
request.AddParameter("to", mail.To[0].ToString());
request.AddParameter("subject", mail.Subject);
request.AddParameter("html", mail.HtmlBody);
request.Method = Method.Post;

var response = await client.ExecutePostAsync(request);

if (!response.IsSuccessful)
{
Log.Error("Error sending request to Mailgun. StatusCode: {statusCode} ErrorMessage: {errorMessage}", response.StatusCode, response.ErrorMessage);
}
}
}
}
9 changes: 9 additions & 0 deletions coffeecard/CoffeeCard.Library/Services/IEmailSender.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Threading.Tasks;
using MimeKit;

namespace CoffeeCard.Library.Services;

public interface IEmailSender
{
public Task SendEmailAsync(MimeMessage email);
}
34 changes: 34 additions & 0 deletions coffeecard/CoffeeCard.Library/Services/MailgunEmailSender.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.Threading.Tasks;
using CoffeeCard.Common.Configuration;
using MimeKit;
using RestSharp;
using RestSharp.Authenticators;
using Serilog;

namespace CoffeeCard.Library.Services;

public class MailgunEmailSender(MailgunSettings mailgunSettings) : IEmailSender
{
public async Task SendEmailAsync(MimeMessage mail)
{
using var client = new RestClient(mailgunSettings.MailgunApiUrl);
client.Authenticator = new HttpBasicAuthenticator("api", mailgunSettings.ApiKey);

var request = new RestRequest();
request.AddParameter("domain", mailgunSettings.Domain, ParameterType.UrlSegment);
request.Resource = "{domain}/messages";
request.AddParameter("from", "Cafe Analog <[email protected]>");
request.AddParameter("to", mail.To[0].ToString());
request.AddParameter("subject", mail.Subject);
request.AddParameter("html", mail.HtmlBody);
request.Method = Method.Post;

var response = await client.ExecutePostAsync(request);

if (!response.IsSuccessful)
{
Log.Error("Error sending request to Mailgun. StatusCode: {statusCode} ErrorMessage: {errorMessage}",
response.StatusCode, response.ErrorMessage);
}
}
}
28 changes: 28 additions & 0 deletions coffeecard/CoffeeCard.Library/Services/SmtpEmailSender.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using System.Threading.Tasks;
using CoffeeCard.Common.Configuration;
using MailKit.Net.Smtp;
using MimeKit;
using Serilog;

namespace CoffeeCard.Library.Services;

public class SmtpEmailSender(SmtpSettings smtpSettings) : IEmailSender
{
public async Task SendEmailAsync(MimeMessage mail)
{
mail.From.Add(new MailboxAddress("Cafe Analog", "[email protected]"));

try
{
using var smtpClient = new SmtpClient();
await smtpClient.ConnectAsync(smtpSettings.Host, smtpSettings.Port);
await smtpClient.SendAsync(mail);
await smtpClient.DisconnectAsync(true);
}
catch (Exception ex)
{
Log.Error("Error sending request to SMTP server. Error: {errorMessage}", ex.Message);
}
}
}
9 changes: 9 additions & 0 deletions coffeecard/CoffeeCard.WebApi/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ public void ConfigureServices(IServiceCollection services)
services.AddScoped<Library.Services.v2.IAccountService, Library.Services.v2.AccountService>();
services.AddScoped<IPurchaseService, PurchaseService>();
services.AddScoped<IMapperService, MapperService>();
if (_environment.IsDevelopment())
{
services.AddTransient<IEmailSender, SmtpEmailSender>();
services.AddSingleton(_configuration.GetSection("SmtpSettings").Get<SmtpSettings>());
}
else
{
services.AddTransient<IEmailSender, MailgunEmailSender>();
}
services.AddScoped<IEmailService, EmailService>();
services.AddScoped<IVoucherService, VoucherService>();
services.AddScoped<IProgrammeService, ProgrammeService>();
Expand Down
4 changes: 4 additions & 0 deletions coffeecard/CoffeeCard.WebApi/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
"EmailBaseUrl": "https://localhost",
"MailgunApiUrl": "https://api.mailgun.net/v3"
},
"SmtpSettings": {
"Host": "mailhog",
"Port": 1025
},
"MobilePaySettingsV2": {
"ApiUrl": "https://invalidurl.test/",
"ApiKey": "DummyKey",
Expand Down
6 changes: 6 additions & 0 deletions coffeecard/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,9 @@ services:
volumes:
- ./CoffeeCard.WebApi/appsettings.json:/app/appsettings.json:z
- ~/.aspnet/https:/https:ro

mailhog:
image: mailhog/mailhog
ports:
- "1025:1025"
- "8025:8025"

0 comments on commit bfcd44b

Please sign in to comment.