Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PAS-452 | Design generic fallback for e-mail providers & drop PostMark #636

Merged
merged 29 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b9d7907
Remove Postmark
jonashendrickx Jul 13, 2024
8c5922b
fix format
jonashendrickx Jul 13, 2024
bb4ca41
AWS SES WIP
jonashendrickx Aug 10, 2024
4d9ce76
WIP
jonashendrickx Aug 12, 2024
d071c57
WIP
jonashendrickx Aug 12, 2024
610ee12
Fix /organization/verify page
jonashendrickx Aug 12, 2024
7c28474
WIP
jonashendrickx Aug 12, 2024
d1f6f74
Refactor to use mailproviderfactory for unit testing
jonashendrickx Aug 13, 2024
d6a597d
Merge branch 'refs/heads/main' into PAS-452-Remove-Postmark-and-use-A…
jonashendrickx Aug 13, 2024
4f6915a
fix
jonashendrickx Aug 13, 2024
f25df08
Tests
jonashendrickx Aug 13, 2024
6b48659
Refactor
jonashendrickx Aug 13, 2024
f5a6d3d
Fix path
jonashendrickx Aug 13, 2024
493ac5c
Remove unused constructor
jonashendrickx Aug 13, 2024
001d6c7
possibly fix integration tests
jonashendrickx Aug 13, 2024
aa89d0e
doc
jonashendrickx Aug 13, 2024
d410078
rename
jonashendrickx Aug 14, 2024
3fc45e2
init
jonashendrickx Aug 14, 2024
68f4a70
Merge branch 'main' into PAS-452-Remove-Postmark-and-use-AWS-SES-Send…
jonashendrickx Aug 14, 2024
2c60c8d
fix
jonashendrickx Aug 15, 2024
8b7977a
Merge remote-tracking branch 'origin/PAS-452-Remove-Postmark-and-use-…
jonashendrickx Aug 15, 2024
346d7f8
Merge branch 'main' into PAS-452-Remove-Postmark-and-use-AWS-SES-Send…
jonashendrickx Aug 20, 2024
3a41810
Merge branch 'main' into PAS-452-Remove-Postmark-and-use-AWS-SES-Send…
jonashendrickx Aug 24, 2024
4dbb52f
Merge branch 'main' into PAS-452-Remove-Postmark-and-use-AWS-SES-Send…
jonashendrickx Aug 26, 2024
3582e58
Merge branch 'main' into PAS-452-Remove-Postmark-and-use-AWS-SES-Send…
jonashendrickx Aug 26, 2024
a2a1a09
f
jonashendrickx Aug 26, 2024
0f8a37f
Merge remote-tracking branch 'origin/PAS-452-Remove-Postmark-and-use-…
jonashendrickx Aug 26, 2024
03ce61e
f
jonashendrickx Aug 26, 2024
5b416d7
f
jonashendrickx Aug 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions src/AdminConsole/Components/Pages/Organization/Verify.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
@page "/organization/verify"
@using Microsoft.Extensions.Options
@using Passwordless.Common.Services.Mail

@attribute [AllowAnonymous]

@inject IOptionsSnapshot<MailConfiguration> MailOptions
@inject IHttpContextAccessor HttpContextAccessor
@inject NavigationManager NavigationManager
@inject IWebHostEnvironment WebHostEnvironment

<HeadContent>
@* This to refresh the page to check if the user has verified and logged in *@
<meta http-equiv="refresh" content="15" />
</HeadContent>

<Page Title="Verify your account">
<div class="flex flex-col items-center py-10 sm:max-w-xl mx-auto">
<span class="p-4 text-white rounded-full bg-primary-700">
<MailIcon Class="text-white w-8 h-8" />
</span>
<h2 class="mt-6 mb-8 text-center">Check your email to verify your account</h2>
<p class="mt-4 text-center">We've sent a confirmation link to the email address you provided. Click on the link in the email to verify your account.</p>
</div>

@if (ShouldShowFileMailPath && FileMailPath != null)
{
<div id="file-provider-debug-section">
<p>
<b>Development mode:</b> Email history is saved to <code>@FileMailPath</code> and displayed below.
</p>

@if (FileMailPathExists && FileMailContent != null)
{
<pre class="mt-4 text-wrap break-all">
<code>@FileMailContent</code>
</pre>
}
else
{
<p class="mt-4">No emails have been sent yet.</p>
}
</div>
}
</Page>
35 changes: 35 additions & 0 deletions src/AdminConsole/Components/Pages/Organization/Verify.razor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Microsoft.AspNetCore.Components;
using Passwordless.Common.Services.Mail.File;

namespace Passwordless.AdminConsole.Components.Pages.Organization;

public partial class Verify : ComponentBase
{
public bool ShouldShowFileMailPath => WebHostEnvironment.IsDevelopment() && MailOptions.Value.Providers.All(p => p.Name.Equals(FileMailProviderOptions.Provider, StringComparison.InvariantCultureIgnoreCase));

public string? FileMailPath { get; set; }

public bool FileMailPathExists { get; set; }

public string? FileMailContent { get; set; }

protected override async Task OnInitializedAsync()
{
if (HttpContextAccessor.HttpContext?.User.Identity?.IsAuthenticated ?? false)
{
NavigationManager.NavigateTo("/account/useronboarding");
return;
}

if (ShouldShowFileMailPath)
{
var fileMailProvider = MailOptions.Value.Providers.First() as FileMailProviderOptions;
FileMailPath = Path.GetFullPath(fileMailProvider!.Path);
FileMailPathExists = File.Exists(FileMailPath);
if (FileMailPathExists)
{
FileMailContent = await File.ReadAllTextAsync(FileMailPath);
}
}
}
}
44 changes: 0 additions & 44 deletions src/AdminConsole/Pages/Organization/Verify.cshtml

This file was deleted.

25 changes: 0 additions & 25 deletions src/AdminConsole/Pages/Organization/Verify.cshtml.cs

This file was deleted.

15 changes: 11 additions & 4 deletions src/AdminConsole/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,18 @@ The [Admin Console](https://admin.passwordless.dev/) is your primary GUI for cre
"ApiSecret": "myAppId:secret:123456",
"ApiUrl": "<optional>"
},
// https://docs.passwordless.dev/guide/self-hosting/configuration.html#e-mail
"Mail": {
// Mail Providers are pluggable. Remove "Postmark" to use the FileProvider
"Postmark": {
"ApiKey": "",
"From": ""
"Smtp": {
"From": "sender@example.com",
"Username": "username",
"Password": "password",
"Host": "smtp.example.com",
"Port": 587,
"StartTls": true,
"Ssl": false,
"SslOverride": false,
"TrustServer": false
}
},
"ConnectionStrings": {
Expand Down
3 changes: 2 additions & 1 deletion src/Common/Common.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@
<PackageReference Include="MailKit" Version="4.7.1.1" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.8" />
<PackageReference Include="Postmark" Version="4.7.12" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.2" />
<PackageReference Include="Serilog.Enrichers.Environment" Version="3.0.1" />
<PackageReference Include="Serilog.Expressions" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.Datadog.Logs" Version="0.5.2" />
<PackageReference Include="Serilog.Sinks.Seq" Version="8.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.7.1" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="8.0.0" />
<PackageReference Include="AWSSDK.SimpleEmailV2" Version="3.7.400.2" />
<PackageReference Include="SendGrid" Version="9.29.3" />
</ItemGroup>

<ItemGroup>
Expand Down
10 changes: 5 additions & 5 deletions src/Common/HealthChecks/MailKitHealthCheck.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
using MailKit.Net.Smtp;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Passwordless.Common.Services.Mail;
using Passwordless.Common.Services.Mail.Smtp;

namespace Passwordless.Common.HealthChecks;

public class MailKitHealthCheck : IHealthCheck
{
private readonly MailKitSmtpMailProvider _mailProvider;
private readonly SmtpMailProvider _smtpMailProvider;

public MailKitHealthCheck(MailKitSmtpMailProvider mailProvider)
public MailKitHealthCheck(SmtpMailProvider smtpMailProvider)
{
_mailProvider = mailProvider;
_smtpMailProvider = smtpMailProvider;
}

public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
Expand All @@ -19,7 +19,7 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context
bool hasErrors = false;
try
{
client = await _mailProvider.GetClientAsync();
client = await _smtpMailProvider.GetClientAsync();
}
catch
{
Expand Down
52 changes: 52 additions & 0 deletions src/Common/Services/Mail/AggregateMailProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Microsoft.Extensions.Options;

namespace Passwordless.Common.Services.Mail;

/// <summary>
/// Wraps multiple mail providers and tries to send the message using them in order.
/// </summary>
public class AggregateMailProvider : IMailProvider
{
private readonly IOptionsSnapshot<MailConfiguration> _options;
private readonly IMailProviderFactory _factory;
private readonly ILogger<AggregateMailProvider> _logger;

public const string FallBackFailedMessage = "No registered mail provider was able to send the message";

public AggregateMailProvider(
IOptionsSnapshot<MailConfiguration> options,
IMailProviderFactory factory,
ILogger<AggregateMailProvider> logger)
{
_options = options;
_factory = factory;
_logger = logger;
}

public async Task SendAsync(MailMessage message)
{
if (message.From == null)
{
message.From = _options.Value.From;
}
foreach (var providerConfiguration in _options.Value.Providers)
{
try
{
_logger.LogDebug("Attempting to send message using provider '{Provider}'", providerConfiguration.Name);
var provider = _factory.Create(providerConfiguration.Name, providerConfiguration);

await provider.SendAsync(message);
_logger.LogInformation("Sent message using provider '{Provider}'", providerConfiguration.Name);
return;
}
catch (Exception e)
{
_logger.LogError(e, "Failed to send message using provider '{Provider}'", providerConfiguration.Name);
}
}

_logger.LogCritical(FallBackFailedMessage);
throw new InvalidOperationException(FallBackFailedMessage);
}
}
86 changes: 86 additions & 0 deletions src/Common/Services/Mail/Aws/AwsMailProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using Amazon;
using Amazon.Runtime;
using Amazon.SimpleEmailV2;
using Amazon.SimpleEmailV2.Model;

namespace Passwordless.Common.Services.Mail.Aws;

public class AwsMailProvider : IMailProvider
{
private readonly IAmazonSimpleEmailServiceV2 _client;
private readonly ILogger<AwsMailProvider> _logger;

public AwsMailProvider(
AwsMailProviderOptions options,
ILogger<AwsMailProvider> logger)
{
var credentials = new BasicAWSCredentials(options.AccessKey, options.SecretKey);
_client = new AmazonSimpleEmailServiceV2Client(credentials, RegionEndpoint.GetBySystemName(options.Region));
_logger = logger;
}

public async Task SendAsync(MailMessage message)
{
var request = new SendEmailRequest
{
FromEmailAddress = message.From
};

if (message.To.Any())
{
request.Destination = new Destination
{
ToAddresses = message.To.ToList()
};
}

request.Content = new EmailContent
{
Simple = new Message
{
Subject = new Content { Data = message.Subject },
Body = new Body
{
Html = new Content { Data = message.HtmlBody },
Text = new Content { Data = message.TextBody },
}
}
};

try
{
await _client.SendEmailAsync(request);
_logger.LogInformation("Sent email with subject '{Subject}' to '{To}'", message.Subject, message.To);
}
catch (AccountSuspendedException ex)
{
_logger.LogError(ex, "The account's ability to send email has been permanently restricted.");
throw;
}
catch (MailFromDomainNotVerifiedException ex)
{
_logger.LogError(ex, "The sending domain is not verified.");
throw;
}
catch (MessageRejectedException ex)
{
_logger.LogError(ex, "The message content is invalid.");
throw;
}
catch (SendingPausedException ex)
{
_logger.LogError(ex, "The account's ability to send email is currently paused.");
throw;
}
catch (TooManyRequestsException ex)
{
_logger.LogError(ex, "Too many requests were made. Please try again later.");
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while sending the email.");
throw;
}
}
}
17 changes: 17 additions & 0 deletions src/Common/Services/Mail/Aws/AwsMailProviderOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Passwordless.Common.Services.Mail.Aws;

public class AwsMailProviderOptions : BaseMailProviderOptions
{
public const string Provider = "aws";
jonashendrickx marked this conversation as resolved.
Show resolved Hide resolved

public AwsMailProviderOptions()
{
Name = Provider;
}

public string AccessKey { get; set; }

public string SecretKey { get; set; }

public string Region { get; set; }
}
9 changes: 9 additions & 0 deletions src/Common/Services/Mail/BaseMailProviderOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Passwordless.Common.Services.Mail;

public abstract class BaseMailProviderOptions
{
/// <summary>
/// The name of the provider.
/// </summary>
public string Name { get; set; }
}
Loading
Loading