add lettermint dispatcher
This commit is contained in:
@@ -253,6 +253,7 @@ paket-files/
|
||||
# JetBrains Rider
|
||||
.idea/
|
||||
*.sln.iml
|
||||
.junie
|
||||
|
||||
# CodeRush
|
||||
.cr/
|
||||
|
||||
@@ -11,4 +11,9 @@ public interface IMailDispatcher : IDisposable
|
||||
/// Whether a certain sender signature is supported by this dispatcher
|
||||
/// </summary>
|
||||
Task<bool> IsSenderSupported(string email, CancellationToken token = default) => Task.FromResult(true);
|
||||
|
||||
/// <summary>
|
||||
/// Check whether a certain email address is suppressed
|
||||
/// </summary>
|
||||
Task<MailSuppressionReason> GetSuppressionReason(string email, CancellationToken token = default) => Task.FromResult(MailSuppressionReason.None);
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
using System.IO;
|
||||
using System.Collections.Specialized;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Net.Mail;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Web;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -12,8 +16,6 @@ namespace Mixtape.Mails.Dispatchers.Lettermint;
|
||||
|
||||
public class LettermintDispatcher : IMailDispatcher
|
||||
{
|
||||
protected Queue<Mail> Queue { get; } = new();
|
||||
|
||||
protected MailOptions Options { get; set; }
|
||||
|
||||
protected IWebHostEnvironment Env { get; set; }
|
||||
@@ -31,9 +33,10 @@ public class LettermintDispatcher : IMailDispatcher
|
||||
Env = env;
|
||||
Http = http;
|
||||
Http.DefaultRequestHeaders.Add("x-lettermint-token", Options.Lettermint.Token);
|
||||
JsonSerializerOptions = new JsonSerializerOptions
|
||||
JsonSerializerOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
Logger = logger;
|
||||
|
||||
@@ -47,6 +50,58 @@ public class LettermintDispatcher : IMailDispatcher
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<MailSuppressionReason> GetSuppressionReason(string email, CancellationToken token = default)
|
||||
{
|
||||
if (Options.Lettermint.TeamToken.IsNullOrEmpty())
|
||||
{
|
||||
Logger.LogWarning("Lettermint team token is not set. Suppression status cannot be checked.");
|
||||
return MailSuppressionReason.None;
|
||||
}
|
||||
|
||||
// build url
|
||||
NameValueCollection query = new()
|
||||
{
|
||||
{ "filter[value]", email },
|
||||
{ "page[size]", "1" }
|
||||
};
|
||||
string uri = Options.Lettermint.ApiUrl + $"/v1/suppressions?" + query;
|
||||
|
||||
try
|
||||
{
|
||||
using HttpRequestMessage request = new(HttpMethod.Get, uri);
|
||||
request.Headers.Authorization = new("Bearer", Options.Lettermint.TeamToken);
|
||||
|
||||
using HttpResponseMessage responseMessage = await Http.SendAsync(request, token);
|
||||
LettermintResponse.ListSuppressions response = await responseMessage.Content.ReadFromJsonAsync<LettermintResponse.ListSuppressions>(JsonSerializerOptions, token);
|
||||
|
||||
if (!responseMessage.IsSuccessStatusCode)
|
||||
{
|
||||
throw new($"Could not get suppression status via Lettermint API. Status code: {responseMessage.StatusCode} Message: {response.ErrorMessage}");
|
||||
}
|
||||
|
||||
LettermintResponse.ListSuppressionsItem suppression = response.Suppressions.FirstOrDefault();
|
||||
|
||||
// the suppression has to be an email address
|
||||
// and match the query email because the API endpoint does partial matching for email address,
|
||||
// which can lead to false-positives
|
||||
if (suppression is not { Type: LettermintResponse.SuppressionType.Email } || !suppression.Value.Equals(email, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return MailSuppressionReason.None;
|
||||
}
|
||||
|
||||
Logger.LogDebug("Suppression status {status} for email {email} retrieved from Lettermint API", suppression.Reason, email);
|
||||
return suppression.Reason;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Could not send message via Lettermint API");
|
||||
}
|
||||
|
||||
return MailSuppressionReason.None;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Send(Mail message, CancellationToken token = default)
|
||||
@@ -75,7 +130,7 @@ public class LettermintDispatcher : IMailDispatcher
|
||||
Subject = message.Subject,
|
||||
|
||||
// tag/group
|
||||
Tag = message.Tag,
|
||||
Tag = Safenames.Tag(message.Tag),
|
||||
|
||||
// metadata
|
||||
Metadata = message.Metadata,
|
||||
|
||||
@@ -5,6 +5,8 @@ public class LettermintOptions
|
||||
public string ApiUrl { get; set; } = "https://api.lettermint.co";
|
||||
|
||||
public string Token { get; set; }
|
||||
|
||||
public string TeamToken { get; set; }
|
||||
|
||||
public string Route { get; set; } = "outgoing";
|
||||
}
|
||||
@@ -13,4 +13,51 @@ public class LettermintResponse
|
||||
[JsonPropertyName("message")]
|
||||
public string ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
public class ListSuppressions
|
||||
{
|
||||
[JsonPropertyName("data")]
|
||||
public ListSuppressionsItem[] Suppressions { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("message")]
|
||||
public string ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
public class ListSuppressionsItem
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
public SuppressionType Type { get; set; }
|
||||
|
||||
public string Value { get; set; }
|
||||
|
||||
public MailSuppressionReason Reason { get; set; } = MailSuppressionReason.None;
|
||||
|
||||
public SuppressionScope Scope { get; set; }
|
||||
|
||||
public string ProjectId { get; set; }
|
||||
|
||||
public string RouteId { get; set; }
|
||||
|
||||
[JsonPropertyName("created_at")]
|
||||
public DateTimeOffset CreatedDate { get; set; }
|
||||
|
||||
[JsonPropertyName("updated_at")]
|
||||
public DateTimeOffset UpdatedDate { get; set; }
|
||||
}
|
||||
|
||||
public enum SuppressionType
|
||||
{
|
||||
Email,
|
||||
Domain,
|
||||
Extension
|
||||
}
|
||||
|
||||
public enum SuppressionScope
|
||||
{
|
||||
Global,
|
||||
Team,
|
||||
Project,
|
||||
Route
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace Mixtape.Mails;
|
||||
|
||||
public enum MailSuppressionReason
|
||||
{
|
||||
/// <summary>
|
||||
/// Not suppressed
|
||||
/// </summary>
|
||||
None = 0,
|
||||
/// <summary>
|
||||
/// Suppressed after permanent delivery failure
|
||||
/// </summary>
|
||||
HardBounce = 1,
|
||||
/// <summary>
|
||||
/// Suppressed after a recipient reports spam
|
||||
/// </summary>
|
||||
SpamComplaint = 2,
|
||||
/// <summary>
|
||||
/// Manually suppressed via dashboard or API
|
||||
/// </summary>
|
||||
Manual = 3,
|
||||
/// <summary>
|
||||
/// Hosted unsubscribe and unsubscribe workflows
|
||||
/// </summary>
|
||||
Unsubscribe = 4,
|
||||
/// <summary>
|
||||
/// Unknown reason
|
||||
/// </summary>
|
||||
Other = 9
|
||||
}
|
||||
+48
-19
@@ -3,19 +3,21 @@ using System.Text;
|
||||
|
||||
namespace Mixtape.Utils;
|
||||
|
||||
public class Safenames
|
||||
public static class Safenames
|
||||
{
|
||||
public enum Scope
|
||||
{
|
||||
Url,
|
||||
File
|
||||
File,
|
||||
Tag
|
||||
}
|
||||
|
||||
const char HYPHEN = '-';
|
||||
|
||||
const char DOT = '.';
|
||||
|
||||
static char[] TICKS = ['`', '\'', '´'];
|
||||
|
||||
const char Underscore = '_';
|
||||
const char Hyphen = '-';
|
||||
const char Dot = '.';
|
||||
const char Plus = '+';
|
||||
const char Ampersand = '&';
|
||||
static readonly char[] Ticks = ['`', '\'', '´'];
|
||||
|
||||
|
||||
/// <summary>
|
||||
@@ -43,6 +45,15 @@ public class Safenames
|
||||
{
|
||||
return Generate(value?.ToString(), Scope.Url);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Converts an untrusted to a safe tag ([a-z][0-9][-][_])
|
||||
/// </summary>
|
||||
public static string Tag(string value)
|
||||
{
|
||||
return Generate(value, Scope.Tag);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
@@ -55,13 +66,13 @@ public class Safenames
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
char previous = default;
|
||||
char previous = '\0';
|
||||
StringBuilder output = new();
|
||||
|
||||
for (int i = 0; i < value.Length; i++)
|
||||
foreach (char t in value)
|
||||
{
|
||||
// get character in lower case
|
||||
char character = char.ToLower(value[i]);
|
||||
char character = char.ToLower(t);
|
||||
char target;
|
||||
|
||||
// do not handle surrogates
|
||||
@@ -69,8 +80,9 @@ public class Safenames
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// do not handle ticks
|
||||
else if (TICKS.Contains(character))
|
||||
if (Ticks.Contains(character))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -90,27 +102,44 @@ public class Safenames
|
||||
{
|
||||
target = character;
|
||||
}
|
||||
// - sign for + and &
|
||||
else if (scope == Scope.File && character == DOT)
|
||||
// + sign for + and &
|
||||
else if (scope is not Scope.Tag && character is Plus or Ampersand)
|
||||
{
|
||||
target = DOT;
|
||||
target = Plus;
|
||||
}
|
||||
else if (scope == Scope.File && character == Dot)
|
||||
{
|
||||
target = Dot;
|
||||
}
|
||||
else if (scope is Scope.Tag && character == Underscore)
|
||||
{
|
||||
target = Underscore;
|
||||
}
|
||||
// add hyphen for all other characters
|
||||
else
|
||||
{
|
||||
target = HYPHEN;
|
||||
target = Hyphen;
|
||||
}
|
||||
|
||||
// add default characters
|
||||
if (target != HYPHEN)
|
||||
if (target != Hyphen && target != Plus)
|
||||
{
|
||||
output.Append(target);
|
||||
}
|
||||
// add hyphen if it isn't first and previous char is not + or -
|
||||
else if (target == HYPHEN && previous != default(char) && previous != HYPHEN)
|
||||
else if (target == Hyphen && previous != 0 && previous != Plus && previous != Hyphen)
|
||||
{
|
||||
output.Append(target);
|
||||
}
|
||||
// add plus. do remove hyphen it is the previous character
|
||||
else if (target == Plus)
|
||||
{
|
||||
if (previous == Hyphen)
|
||||
{
|
||||
output.Remove(output.Length - 1, 1);
|
||||
}
|
||||
output.Append(target);
|
||||
}
|
||||
|
||||
if (output.Length > 0)
|
||||
{
|
||||
@@ -125,7 +154,7 @@ public class Safenames
|
||||
|
||||
if (output.Length == 0)
|
||||
{
|
||||
output.Append(HYPHEN);
|
||||
output.Append(Hyphen);
|
||||
}
|
||||
|
||||
return output.ToString();
|
||||
|
||||
Reference in New Issue
Block a user