Implemented Mailtrap Webhook API
This commit is contained in:
parent
a530375669
commit
f27ba8200f
2
Wave.sln.DotSettings
Normal file
2
Wave.sln.DotSettings
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=mailtrap/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
|
@ -3,6 +3,7 @@
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using AspNetCore.Authentication.ApiKey;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.OutputCaching;
|
using Microsoft.AspNetCore.OutputCaching;
|
||||||
using Wave.Data;
|
using Wave.Data;
|
||||||
|
@ -33,7 +34,7 @@ public class ApiController(ApplicationDbContext context, IOptions<Customization>
|
||||||
|
|
||||||
[HttpGet("email/subscriber/{email}")]
|
[HttpGet("email/subscriber/{email}")]
|
||||||
[Produces("application/json")]
|
[Produces("application/json")]
|
||||||
[Authorize("EmailApi")]
|
[Authorize("EmailApi", AuthenticationSchemes = ApiKeyDefaults.AuthenticationScheme)]
|
||||||
[OutputCache(Duration = 60*10)]
|
[OutputCache(Duration = 60*10)]
|
||||||
public async Task<Results<Ok<EmailSubscriberDto>, NotFound>> GetEmailSubscriber([EmailAddress] string email) {
|
public async Task<Results<Ok<EmailSubscriberDto>, NotFound>> GetEmailSubscriber([EmailAddress] string email) {
|
||||||
var subscriber = await context.Set<EmailSubscriber>()
|
var subscriber = await context.Set<EmailSubscriber>()
|
||||||
|
|
63
Wave/Controllers/WebhookController.cs
Normal file
63
Wave/Controllers/WebhookController.cs
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
using Humanizer;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Wave.Data;
|
||||||
|
using Wave.Data.Api.Mailtrap;
|
||||||
|
|
||||||
|
namespace Wave.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("/api/[controller]")]
|
||||||
|
public class WebhookController(ILogger<WebhookController> logger, ApplicationDbContext context) : ControllerBase {
|
||||||
|
[HttpPost("mailtrap/{apiKey}")]
|
||||||
|
[Authorize("EmailApi", AuthenticationSchemes = "ApiKeyInRoute")]
|
||||||
|
public async Task<IActionResult> Mailtrap(Webhook webhook, string apiKey) {
|
||||||
|
Console.WriteLine(apiKey);
|
||||||
|
foreach (var webhookEvent in webhook.Events) {
|
||||||
|
var subscriber = await context.Set<EmailSubscriber>().FirstOrDefaultAsync(s => s.Email == webhookEvent.Email);
|
||||||
|
|
||||||
|
if (subscriber is null) {
|
||||||
|
logger.LogWarning(
|
||||||
|
"Received webhook event from mailtrap of type {type}, but failed to find subscriber with E-Mail {email}.",
|
||||||
|
webhookEvent.Type, webhookEvent.Email);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
|
||||||
|
switch (webhookEvent.Type) {
|
||||||
|
case WebhookEventType.Delivery:
|
||||||
|
subscriber.LastMailReceived = webhookEvent.EventDateTime;
|
||||||
|
break;
|
||||||
|
case WebhookEventType.Open:
|
||||||
|
subscriber.LastMailOpened = webhookEvent.EventDateTime;
|
||||||
|
break;
|
||||||
|
case WebhookEventType.Bounce:
|
||||||
|
// Store this message in case it develops into a suspension
|
||||||
|
subscriber.UnsubscribeReason = webhookEvent.Response;
|
||||||
|
break;
|
||||||
|
case WebhookEventType.Suspension:
|
||||||
|
subscriber.Unsubscribed = true;
|
||||||
|
subscriber.UnsubscribeReason ??= "unknown";
|
||||||
|
break;
|
||||||
|
case WebhookEventType.Unsubscribe:
|
||||||
|
subscriber.Unsubscribed = true;
|
||||||
|
subscriber.UnsubscribeReason ??= "User Unsubscribed";
|
||||||
|
break;
|
||||||
|
case WebhookEventType.SpamComplaint:
|
||||||
|
subscriber.Unsubscribed = true;
|
||||||
|
subscriber.UnsubscribeReason ??= "User reported as Spam";
|
||||||
|
break;
|
||||||
|
case WebhookEventType.Reject:
|
||||||
|
subscriber.Unsubscribed = true;
|
||||||
|
subscriber.UnsubscribeReason ??= webhookEvent.Reason?.Humanize().Titleize() ?? "Rejected";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
}
|
49
Wave/Data/Api/Mailtrap/WebhookEvent.cs
Normal file
49
Wave/Data/Api/Mailtrap/WebhookEvent.cs
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Wave.Data.Api.Mailtrap;
|
||||||
|
|
||||||
|
public enum WebhookEventType {
|
||||||
|
Delivery,
|
||||||
|
SoftBounce,
|
||||||
|
Bounce,
|
||||||
|
Suspension,
|
||||||
|
Unsubscribe,
|
||||||
|
Open,
|
||||||
|
SpamComplaint,
|
||||||
|
Click,
|
||||||
|
Reject
|
||||||
|
}
|
||||||
|
|
||||||
|
public record WebhookEvent(
|
||||||
|
[property:JsonPropertyName("event")]
|
||||||
|
string EventTypeString,
|
||||||
|
[property:JsonPropertyName("category")]
|
||||||
|
string Category,
|
||||||
|
[property:JsonPropertyName("message_id")]
|
||||||
|
string MessageId,
|
||||||
|
[property:JsonPropertyName("event_id")]
|
||||||
|
string EventId,
|
||||||
|
[property:JsonPropertyName("email")]
|
||||||
|
string Email,
|
||||||
|
[property:JsonPropertyName("timestamp")]
|
||||||
|
long Timestamp,
|
||||||
|
[property:JsonPropertyName("response")]
|
||||||
|
string? Response,
|
||||||
|
[property:JsonPropertyName("reason")]
|
||||||
|
string? Reason,
|
||||||
|
[property:JsonPropertyName("response_code")]
|
||||||
|
int? ResponseCode) {
|
||||||
|
|
||||||
|
public WebhookEventType Type => Enum.Parse<WebhookEventType>(EventTypeString.Replace("_", ""), true);
|
||||||
|
public DateTimeOffset EventDateTime => DateTimeOffset.FromUnixTimeMilliseconds(Timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Webhook {
|
||||||
|
[JsonPropertyName("events")]
|
||||||
|
[JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
|
||||||
|
public ICollection<WebhookEvent> Events { get; } = [];
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return $"Webhook {{ Events = [{string.Join(", ", Events)}] }}";
|
||||||
|
}
|
||||||
|
}
|
|
@ -101,6 +101,9 @@
|
||||||
}).AddApiKeyInHeader<ApiKeyProvider>(ApiKeyDefaults.AuthenticationScheme, options => {
|
}).AddApiKeyInHeader<ApiKeyProvider>(ApiKeyDefaults.AuthenticationScheme, options => {
|
||||||
options.KeyName = "X-API-KEY";
|
options.KeyName = "X-API-KEY";
|
||||||
options.Realm = "Wave API";
|
options.Realm = "Wave API";
|
||||||
|
}).AddApiKeyInRouteValues<ApiKeyProvider>("ApiKeyInRoute", options => {
|
||||||
|
options.KeyName = "apiKey";
|
||||||
|
options.Realm = "Wave API";
|
||||||
})
|
})
|
||||||
.AddIdentityCookies();
|
.AddIdentityCookies();
|
||||||
if (builder.Configuration.GetSection("Oidc").Get<OidcConfiguration>() is {} oidc && !string.IsNullOrWhiteSpace(oidc.Authority)) {
|
if (builder.Configuration.GetSection("Oidc").Get<OidcConfiguration>() is {} oidc && !string.IsNullOrWhiteSpace(oidc.Authority)) {
|
||||||
|
|
|
@ -13,7 +13,10 @@ private record ActualApiKey(string Key, string OwnerName, IReadOnlyCollection<Cl
|
||||||
|
|
||||||
public async Task<IApiKey?> ProvideAsync(string key) {
|
public async Task<IApiKey?> ProvideAsync(string key) {
|
||||||
try {
|
try {
|
||||||
byte[] data = Convert.FromBase64String(key);
|
string unescapedKey = key;
|
||||||
|
if (unescapedKey.Contains('%')) unescapedKey = Uri.UnescapeDataString(key);
|
||||||
|
|
||||||
|
byte[] data = Convert.FromBase64String(unescapedKey);
|
||||||
string hashedKey = Convert.ToBase64String(SHA256.HashData(data));
|
string hashedKey = Convert.ToBase64String(SHA256.HashData(data));
|
||||||
|
|
||||||
var apiKey = await context.Set<ApiKey>().Include(a => a.ApiClaims).SingleOrDefaultAsync(k => k.Key == hashedKey);
|
var apiKey = await context.Set<ApiKey>().Include(a => a.ApiClaims).SingleOrDefaultAsync(k => k.Key == hashedKey);
|
||||||
|
|
Loading…
Reference in a new issue