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.Extensions.Options; | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using AspNetCore.Authentication.ApiKey; | ||||
| using Microsoft.AspNetCore.Authorization; | ||||
| using Microsoft.AspNetCore.OutputCaching; | ||||
| using Wave.Data; | ||||
|  | @ -33,7 +34,7 @@ public class ApiController(ApplicationDbContext context, IOptions<Customization> | |||
| 
 | ||||
| 	[HttpGet("email/subscriber/{email}")] | ||||
| 	[Produces("application/json")] | ||||
| 	[Authorize("EmailApi")] | ||||
| 	[Authorize("EmailApi", AuthenticationSchemes = ApiKeyDefaults.AuthenticationScheme)] | ||||
| 	[OutputCache(Duration = 60*10)] | ||||
| 	public async Task<Results<Ok<EmailSubscriberDto>, NotFound>> GetEmailSubscriber([EmailAddress] string email) { | ||||
| 		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 => { | ||||
| 		options.KeyName = "X-API-KEY"; | ||||
| 		options.Realm = "Wave API"; | ||||
| 	}).AddApiKeyInRouteValues<ApiKeyProvider>("ApiKeyInRoute", options => { | ||||
| 		options.KeyName = "apiKey"; | ||||
| 		options.Realm = "Wave API"; | ||||
| 	}) | ||||
| 	.AddIdentityCookies(); | ||||
| 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) { | ||||
| 		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)); | ||||
| 
 | ||||
| 			var apiKey = await context.Set<ApiKey>().Include(a => a.ApiClaims).SingleOrDefaultAsync(k => k.Key == hashedKey); | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue