diff --git a/Source/StrongGrid.Benchmark/StrongGrid.Benchmark.csproj b/Source/StrongGrid.Benchmark/StrongGrid.Benchmark.csproj
index 09f8e8d7..0a4aee00 100644
--- a/Source/StrongGrid.Benchmark/StrongGrid.Benchmark.csproj
+++ b/Source/StrongGrid.Benchmark/StrongGrid.Benchmark.csproj
@@ -6,7 +6,7 @@
-
+
diff --git a/Source/StrongGrid.IntegrationTests/StrongGrid.IntegrationTests.csproj b/Source/StrongGrid.IntegrationTests/StrongGrid.IntegrationTests.csproj
index 34298f18..8e6184a7 100644
--- a/Source/StrongGrid.IntegrationTests/StrongGrid.IntegrationTests.csproj
+++ b/Source/StrongGrid.IntegrationTests/StrongGrid.IntegrationTests.csproj
@@ -15,7 +15,7 @@
-
+
diff --git a/Source/StrongGrid.UnitTests/StrongGrid.UnitTests.csproj b/Source/StrongGrid.UnitTests/StrongGrid.UnitTests.csproj
index a8abfdb5..9a8f5aa9 100644
--- a/Source/StrongGrid.UnitTests/StrongGrid.UnitTests.csproj
+++ b/Source/StrongGrid.UnitTests/StrongGrid.UnitTests.csproj
@@ -11,8 +11,8 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
diff --git a/Source/StrongGrid/Extensions/Internal.cs b/Source/StrongGrid/Extensions/Internal.cs
index a541fab9..d55453e3 100644
--- a/Source/StrongGrid/Extensions/Internal.cs
+++ b/Source/StrongGrid/Extensions/Internal.cs
@@ -1,4 +1,3 @@
-using HttpMultipartParser;
using Pathoschild.Http.Client;
using StrongGrid.Json;
using StrongGrid.Models;
@@ -19,6 +18,7 @@
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
+using static StrongGrid.Utilities.DiagnosticHandler;
namespace StrongGrid
{
@@ -33,7 +33,7 @@ internal enum UnixTimePrecision
Milliseconds = 1
}
- private static readonly DateTime EPOCH = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+ private static readonly DateTime EPOCH = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
///
/// Converts a 'unix time', which is expressed as the number of seconds (or milliseconds) since
@@ -117,9 +117,13 @@ internal static async Task ReadAsStringAsync(this HttpContent httpConten
if (httpContent != null)
{
+#if NET7_0_OR_GREATER
+ var contentStream = await httpContent.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+#else
var contentStream = await httpContent.ReadAsStreamAsync().ConfigureAwait(false);
+#endif
- if (encoding == null) encoding = httpContent.GetEncoding(Encoding.UTF8);
+ encoding ??= httpContent.GetEncoding(Encoding.UTF8);
// This is important: we must make a copy of the response stream otherwise we would get an
// exception on subsequent attempts to read the content of the stream
@@ -130,7 +134,11 @@ internal static async Task ReadAsStringAsync(this HttpContent httpConten
ms.Position = 0;
using (var sr = new StreamReader(ms, encoding))
{
+#if NET7_0_OR_GREATER
+ content = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
+#else
content = await sr.ReadToEndAsync().ConfigureAwait(false);
+#endif
}
// It's important to rewind the stream
@@ -184,19 +192,6 @@ internal static Encoding GetEncoding(this HttpContent content, Encoding defaultE
return encoding;
}
- ///
- /// Returns the value of a parameter or the default value if it doesn't exist.
- ///
- /// The parser.
- /// The name of the parameter.
- /// The default value.
- /// The value of the parameter.
- internal static string GetParameterValue(this MultipartFormDataParser parser, string name, string defaultValue)
- {
- if (parser.HasParameter(name)) return parser.GetParameterValue(name);
- else return defaultValue;
- }
-
/// Asynchronously retrieve the JSON encoded response body and convert it to an object of the desired type.
/// The response model to deserialize into.
/// The response.
@@ -581,14 +576,14 @@ internal static IEnumerable> ParseQuerystring(this
return querystringParameters;
}
- internal static (WeakReference RequestReference, string Diagnostic, long RequestTimeStamp, long ResponseTimestamp) GetDiagnosticInfo(this IResponse response)
+ internal static DiagnosticInfo GetDiagnosticInfo(this IResponse response)
{
var diagnosticId = response.Message.RequestMessage.Headers.GetValue(DiagnosticHandler.DIAGNOSTIC_ID_HEADER_NAME);
- DiagnosticHandler.DiagnosticsInfo.TryGetValue(diagnosticId, out (WeakReference RequestReference, string Diagnostic, long RequestTimeStamp, long ResponseTimestamp) diagnosticInfo);
+ DiagnosticHandler.DiagnosticsInfo.TryGetValue(diagnosticId, out DiagnosticInfo diagnosticInfo);
return diagnosticInfo;
}
- internal static async Task<(bool, string)> GetErrorMessageAsync(this HttpResponseMessage message)
+ internal static async Task<(bool IsError, string Message)> GetErrorMessageAsync(this HttpResponseMessage message)
{
// Default error message
var errorMessage = $"{(int)message.StatusCode}: {message.ReasonPhrase}";
diff --git a/Source/StrongGrid/Extensions/Public.cs b/Source/StrongGrid/Extensions/Public.cs
index 0a1989f3..cb43a312 100644
--- a/Source/StrongGrid/Extensions/Public.cs
+++ b/Source/StrongGrid/Extensions/Public.cs
@@ -7,6 +7,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
@@ -1276,7 +1277,7 @@ public static async Task DownloadExportFilesAsync(this IContacts contacts, strin
var destinationPath = Path.Combine(destinationFolder, exportFile.FileName);
using (Stream output = File.OpenWrite(destinationPath))
{
- exportFile.Stream.CopyTo(output);
+ await exportFile.Stream.CopyToAsync(output).ConfigureAwait(false);
}
}
}
@@ -1313,7 +1314,7 @@ public static async Task DownloadCsvAsync(this IEmailActivities emailActivities,
{
using (Stream output = File.OpenWrite(destinationPath))
{
- responseStream.CopyTo(output);
+ await responseStream.CopyToAsync(output).ConfigureAwait(false);
}
}
}
@@ -1334,5 +1335,106 @@ public static Task AddAddressToUnsubscribeGroupAsync(this ISuppressions suppress
{
return suppressions.AddAddressesToUnsubscribeGroupAsync(groupId, new[] { email }, onBehalfOf, cancellationToken);
}
+
+ ///
+ /// Generate a new API Key for billing.
+ ///
+ /// The ApiKeys resource.
+ /// The name.
+ /// The user to impersonate.
+ /// Cancellation token.
+ ///
+ /// The .
+ ///
+ public static Task CreateWithBillingPermissionsAsync(this IApiKeys apiKeys, string name, string onBehalfOf = null, CancellationToken cancellationToken = default)
+ {
+ var scopes = new[]
+ {
+ "billing.delete",
+ "billing.read",
+ "billing.update"
+ };
+
+ return apiKeys.CreateAsync(name, scopes, onBehalfOf, cancellationToken);
+ }
+
+ ///
+ /// Generate a new API Key with the same permissions that have been granted to you.
+ ///
+ /// The ApiKeys resource.
+ /// The name.
+ /// The user to impersonate.
+ /// Cancellation token.
+ ///
+ /// The .
+ ///
+ ///
+ /// If you specify an API Key when instanciating the , the new API Key will inherit the permissions of that API Key.
+ /// If you specify a username and password when instanciating the , the new API Key will inherit the permissions of that user.
+ ///
+ public static async Task CreateWithAllPermissionsAsync(this IApiKeys apiKeys, string name, string onBehalfOf = null, CancellationToken cancellationToken = default)
+ {
+ var privateField = apiKeys.GetType().GetField("_client", BindingFlags.NonPublic | BindingFlags.Instance);
+ if (privateField == null) throw new ArgumentException("Unable to find the HttpClient in the resource.", nameof(apiKeys));
+ var client = (Pathoschild.Http.Client.IClient)privateField.GetValue(apiKeys);
+
+ var scopes = await client.GetCurrentScopes(true, cancellationToken).ConfigureAwait(false);
+ var superApiKey = await apiKeys.CreateAsync(name, scopes, onBehalfOf, cancellationToken).ConfigureAwait(false);
+ return superApiKey;
+ }
+
+ ///
+ /// Generate a new API Key with the same "read" permissions that have ben granted to you.
+ ///
+ /// The ApiKeys resource.
+ /// The name.
+ /// The user to impersonate.
+ /// Cancellation token.
+ ///
+ /// The .
+ ///
+ ///
+ /// If you specify an API Key when instanciating the , the new API Key will inherit the "read" permissions of that API Key.
+ /// If you specify a username and password when instanciating the , the new API Key will inherit the "read" permissions of that user.
+ ///
+ public static async Task CreateWithReadOnlyPermissionsAsync(this IApiKeys apiKeys, string name, string onBehalfOf = null, CancellationToken cancellationToken = default)
+ {
+ var privateField = apiKeys.GetType().GetField("_client", BindingFlags.NonPublic | BindingFlags.Instance);
+ if (privateField == null) throw new ArgumentException("Unable to find the HttpClient in the resource.", nameof(apiKeys));
+ var client = (Pathoschild.Http.Client.IClient)privateField.GetValue(apiKeys);
+
+ var scopes = await client.GetCurrentScopes(true, cancellationToken).ConfigureAwait(false);
+ scopes = scopes.Where(s => s.EndsWith(".read", System.StringComparison.OrdinalIgnoreCase)).ToArray();
+
+ var readOnlyApiKey = await apiKeys.CreateAsync(name, scopes, onBehalfOf, cancellationToken).ConfigureAwait(false);
+ return readOnlyApiKey;
+ }
+
+ ///
+ /// Send a teammate invitation via email with the same "read" permissions that have been granted to you.
+ /// A teammate invite will expire after 7 days, but you may resend the invite at any time
+ /// to reset the expiration date.
+ ///
+ /// The teammates resource.
+ /// The email address of the teammate.
+ /// The cancellation token.
+ ///
+ /// The async task.
+ ///
+ ///
+ /// Essentials, Legacy Lite, and Free Trial users may create up to one teammate per account.
+ /// There is not a teammate limit for Pro and higher plans.
+ ///
+ public static async Task InviteTeammateWithReadOnlyPrivilegesAsync(this ITeammates teammates, string email, CancellationToken cancellationToken = default)
+ {
+ var privateField = teammates.GetType().GetField("_client", BindingFlags.NonPublic | BindingFlags.Instance);
+ if (privateField == null) throw new ArgumentException("Unable to find the HttpClient in the resource.", nameof(teammates));
+ var client = (Pathoschild.Http.Client.IClient)privateField.GetValue(teammates);
+
+ var scopes = await client.GetCurrentScopes(true, cancellationToken).ConfigureAwait(true);
+ scopes = scopes.Where(s => s.EndsWith(".read", StringComparison.OrdinalIgnoreCase)).ToArray();
+
+ return await teammates.InviteTeammateAsync(email, scopes, cancellationToken).ConfigureAwait(false);
+ }
}
}
diff --git a/Source/StrongGrid/Json/EventConverter.cs b/Source/StrongGrid/Json/EventConverter.cs
index a34e0c5d..4e09321a 100644
--- a/Source/StrongGrid/Json/EventConverter.cs
+++ b/Source/StrongGrid/Json/EventConverter.cs
@@ -17,6 +17,7 @@ internal class EventConverter : BaseJsonConverter
{
"asm_group_id",
"attempt",
+ "bounce_classification",
"category",
"cert_err",
"email",
diff --git a/Source/StrongGrid/Models/BounceClassification.cs b/Source/StrongGrid/Models/BounceClassification.cs
new file mode 100644
index 00000000..c64fe911
--- /dev/null
+++ b/Source/StrongGrid/Models/BounceClassification.cs
@@ -0,0 +1,55 @@
+using StrongGrid.Json;
+using System.Runtime.Serialization;
+using System.Text.Json.Serialization;
+
+namespace StrongGrid.Models
+{
+ ///
+ /// Enumeration to indicate the type of SMTP failure.
+ ///
+ [JsonConverter(typeof(StringEnumConverter))]
+ public enum BounceClassification
+ {
+ ///
+ /// Unclassified.
+ ///
+ [EnumMember(Value = "Unclassified")]
+ Unclassified,
+
+ ///
+ /// Invalid Addres.
+ ///
+ [EnumMember(Value = "Invalid Address")]
+ InvalidAddress,
+
+ ///
+ /// Technical.
+ ///
+ [EnumMember(Value = "Technical")]
+ Technical,
+
+ ///
+ /// Content.
+ ///
+ [EnumMember(Value = "Content")]
+ Content,
+
+ ///
+ /// Reputation.
+ ///
+ [EnumMember(Value = "Reputation")]
+ Reputation,
+
+ ///
+ /// Frequency or volume.
+ ///
+ [EnumMember(Value = "Frequency or Volume Too High")]
+ FrequencyOrVolume,
+
+ ///
+ /// Mailbox Unavailable.
+ ///
+ [EnumMember(Value = "Mailbox Unavailable")]
+ MailboxUnavailable,
+ }
+}
diff --git a/Source/StrongGrid/Models/Webhooks/BouncedEvent.cs b/Source/StrongGrid/Models/Webhooks/BouncedEvent.cs
index 9a661f94..17547a40 100644
--- a/Source/StrongGrid/Models/Webhooks/BouncedEvent.cs
+++ b/Source/StrongGrid/Models/Webhooks/BouncedEvent.cs
@@ -76,5 +76,15 @@ public BouncedEvent()
///
[JsonPropertyName("type")]
public BounceType Type { get; set; }
+
+ ///
+ /// Gets or sets a value indicating the type of SMTP failure.
+ ///
+ /// The SMTP failure classification.
+ ///
+ /// See SendGrid' Bounce Classifications documentation to understand each classification.
+ ///
+ [JsonPropertyName("bounce_classification")]
+ public BounceClassification Classification { get; set; }
}
}
diff --git a/Source/StrongGrid/Resources/ApiKeys.cs b/Source/StrongGrid/Resources/ApiKeys.cs
index 8b277c71..74db75ba 100644
--- a/Source/StrongGrid/Resources/ApiKeys.cs
+++ b/Source/StrongGrid/Resources/ApiKeys.cs
@@ -143,69 +143,6 @@ public Task UpdateAsync(string keyId, string name, Parameter();
}
- ///
- /// Generate a new API Key for billing.
- ///
- /// The name.
- /// The user to impersonate.
- /// Cancellation token.
- ///
- /// The .
- ///
- public Task CreateWithBillingPermissionsAsync(string name, string onBehalfOf = null, CancellationToken cancellationToken = default)
- {
- var scopes = new[]
- {
- "billing.delete",
- "billing.read",
- "billing.update"
- };
-
- return this.CreateAsync(name, scopes, onBehalfOf, cancellationToken);
- }
-
- ///
- /// Generate a new API Key with the same permissions that have been granted to you.
- ///
- /// The name.
- /// The user to impersonate.
- /// Cancellation token.
- ///
- /// The .
- ///
- ///
- /// If you specify an API Key when instanciating the , the new API Key will inherit the permissions of that API Key.
- /// If you specify a username and password when instanciating the , the new API Key will inherit the permissions of that user.
- ///
- public async Task CreateWithAllPermissionsAsync(string name, string onBehalfOf = null, CancellationToken cancellationToken = default)
- {
- var scopes = await _client.GetCurrentScopes(true, cancellationToken).ConfigureAwait(false);
- var superApiKey = await this.CreateAsync(name, scopes, onBehalfOf, cancellationToken).ConfigureAwait(false);
- return superApiKey;
- }
-
- ///
- /// Generate a new API Key with the same "read" permissions that have ben granted to you.
- ///
- /// The name.
- /// The user to impersonate.
- /// Cancellation token.
- ///
- /// The .
- ///
- ///
- /// If you specify an API Key when instanciating the , the new API Key will inherit the "read" permissions of that API Key.
- /// If you specify a username and password when instanciating the , the new API Key will inherit the "read" permissions of that user.
- ///
- public async Task CreateWithReadOnlyPermissionsAsync(string name, string onBehalfOf = null, CancellationToken cancellationToken = default)
- {
- var scopes = await _client.GetCurrentScopes(true, cancellationToken).ConfigureAwait(false);
- scopes = scopes.Where(s => s.EndsWith(".read", System.StringComparison.OrdinalIgnoreCase)).ToArray();
-
- var readOnlyApiKey = await this.CreateAsync(name, scopes, onBehalfOf, cancellationToken).ConfigureAwait(false);
- return readOnlyApiKey;
- }
-
private static StrongGridJsonObject ConvertToJson(string name, Parameter> scopes)
{
var result = new StrongGridJsonObject();
diff --git a/Source/StrongGrid/Resources/IApiKeys.cs b/Source/StrongGrid/Resources/IApiKeys.cs
index c8537c7c..2cd74d95 100644
--- a/Source/StrongGrid/Resources/IApiKeys.cs
+++ b/Source/StrongGrid/Resources/IApiKeys.cs
@@ -74,46 +74,5 @@ public interface IApiKeys
/// Cancellation token.
/// The .
Task UpdateAsync(string keyId, string name, Parameter> scopes = default, string onBehalfOf = null, CancellationToken cancellationToken = default);
-
- ///
- /// Generate a new API Key for billing.
- ///
- /// The name.
- /// The user to impersonate.
- /// Cancellation token.
- ///
- /// The .
- ///
- Task CreateWithBillingPermissionsAsync(string name, string onBehalfOf = null, CancellationToken cancellationToken = default);
-
- ///
- /// Generate a new API Key with all permissions.
- ///
- /// The name.
- /// The user to impersonate.
- /// Cancellation token.
- ///
- /// The .
- ///
- ///
- /// If you specify an API Key when instanciating the , the new API Key will inherit the permissions of that API Key.
- /// If you specify a username and password when instanciating the , the new API Key will inherit the permissions of that user.
- ///
- Task CreateWithAllPermissionsAsync(string name, string onBehalfOf = null, CancellationToken cancellationToken = default);
-
- ///
- /// Generate a new API Key with the same "read" permissions that have ben granted to you.
- ///
- /// The name.
- /// The user to impersonate.
- /// Cancellation token.
- ///
- /// The .
- ///
- ///
- /// If you specify an API Key when instanciating the , the new API Key will inherit the "read" permissions of that API Key.
- /// If you specify a username and password when instanciating the , the new API Key will inherit the "read" permissions of that user.
- ///
- Task CreateWithReadOnlyPermissionsAsync(string name, string onBehalfOf = null, CancellationToken cancellationToken = default);
}
}
diff --git a/Source/StrongGrid/Resources/ITeammates.cs b/Source/StrongGrid/Resources/ITeammates.cs
index c5479d68..a18386a9 100644
--- a/Source/StrongGrid/Resources/ITeammates.cs
+++ b/Source/StrongGrid/Resources/ITeammates.cs
@@ -103,22 +103,6 @@ public interface ITeammates
///
Task InviteTeammateAsync(string email, IEnumerable scopes, CancellationToken cancellationToken = default);
- ///
- /// Send a teammate invitation via email with the same "read" permissions that have been granted to you.
- /// A teammate invite will expire after 7 days, but you may resend the invite at any time
- /// to reset the expiration date.
- ///
- /// The email address of the teammate.
- /// The cancellation token.
- ///
- /// The async task.
- ///
- ///
- /// Essentials, Legacy Lite, and Free Trial users may create up to one teammate per account.
- /// There is not a teammate limit for Pro and higher plans.
- ///
- Task InviteTeammateWithReadOnlyPrivilegesAsync(string email, CancellationToken cancellationToken = default);
-
///
/// Send a teammate invitation via email with admin permissions.
/// A teammate invite will expire after 7 days, but you may resend the invite at any time
diff --git a/Source/StrongGrid/Resources/Teammates.cs b/Source/StrongGrid/Resources/Teammates.cs
index 41e4d735..0cb4df21 100644
--- a/Source/StrongGrid/Resources/Teammates.cs
+++ b/Source/StrongGrid/Resources/Teammates.cs
@@ -3,7 +3,6 @@
using StrongGrid.Models;
using System;
using System.Collections.Generic;
-using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -168,52 +167,7 @@ public Task DeleteInvitationAsync(string token, CancellationToken cancellationTo
///
public Task InviteTeammateAsync(string email, IEnumerable scopes, CancellationToken cancellationToken = default)
{
- if (string.IsNullOrEmpty(email)) throw new ArgumentNullException(nameof(email));
-
- var data = new StrongGridJsonObject();
- data.AddProperty("email", email);
- data.AddProperty("scopes", scopes);
- data.AddProperty("is_admin", false);
-
- return _client
- .PostAsync(_endpoint)
- .WithJsonBody(data)
- .WithCancellationToken(cancellationToken)
- .AsObject();
- }
-
- ///
- /// Send a teammate invitation via email with the same "read" permissions that have been granted to you.
- /// A teammate invite will expire after 7 days, but you may resend the invite at any time
- /// to reset the expiration date.
- ///
- /// The email address of the teammate.
- /// The cancellation token.
- ///
- /// The async task.
- ///
- ///
- /// Essentials, Legacy Lite, and Free Trial users may create up to one teammate per account.
- /// There is not a teammate limit for Pro and higher plans.
- ///
- public async Task InviteTeammateWithReadOnlyPrivilegesAsync(string email, CancellationToken cancellationToken = default)
- {
- if (string.IsNullOrEmpty(email)) throw new ArgumentNullException(nameof(email));
-
- var scopes = await _client.GetCurrentScopes(true, cancellationToken).ConfigureAwait(true);
- scopes = scopes.Where(s => s.EndsWith(".read", System.StringComparison.OrdinalIgnoreCase)).ToArray();
-
- var data = new StrongGridJsonObject();
- data.AddProperty("email", email);
- data.AddProperty("scopes", scopes);
- data.AddProperty("is_admin", false);
-
- return await _client
- .PostAsync(_endpoint)
- .WithJsonBody(data)
- .WithCancellationToken(cancellationToken)
- .AsObject()
- .ConfigureAwait(false);
+ return InviteAsync(email, scopes, false, cancellationToken);
}
///
@@ -232,17 +186,7 @@ public async Task InviteTeammateWithReadOnlyPrivilegesAsync(
///
public Task InviteTeammateAsAdminAsync(string email, CancellationToken cancellationToken = default)
{
- if (string.IsNullOrEmpty(email)) throw new ArgumentNullException(nameof(email));
-
- var data = new StrongGridJsonObject();
- data.AddProperty("email", email);
- data.AddProperty("is_admin", true);
-
- return _client
- .PostAsync(_endpoint)
- .WithJsonBody(data)
- .WithCancellationToken(cancellationToken)
- .AsObject();
+ return InviteAsync(email, null, true, cancellationToken);
}
///
@@ -317,5 +261,21 @@ public Task DeleteTeammateAsync(string username, CancellationToken cancellationT
.WithCancellationToken(cancellationToken)
.AsMessage();
}
+
+ private Task InviteAsync(string email, IEnumerable scopes, bool isAdmin, CancellationToken cancellationToken = default)
+ {
+ if (string.IsNullOrEmpty(email)) throw new ArgumentNullException(nameof(email));
+
+ var data = new StrongGridJsonObject();
+ data.AddProperty("email", email);
+ data.AddProperty("scopes", scopes);
+ data.AddProperty("is_admin", isAdmin);
+
+ return _client
+ .PostAsync(_endpoint)
+ .WithJsonBody(data)
+ .WithCancellationToken(cancellationToken)
+ .AsObject();
+ }
}
}
diff --git a/Source/StrongGrid/StrongGrid.csproj b/Source/StrongGrid/StrongGrid.csproj
index f4ac6916..62b0756f 100644
--- a/Source/StrongGrid/StrongGrid.csproj
+++ b/Source/StrongGrid/StrongGrid.csproj
@@ -36,15 +36,15 @@
-
+
-
+
-
+
diff --git a/Source/StrongGrid/Utilities/DiagnosticHandler.cs b/Source/StrongGrid/Utilities/DiagnosticHandler.cs
index c7732f60..f29a2d3a 100644
--- a/Source/StrongGrid/Utilities/DiagnosticHandler.cs
+++ b/Source/StrongGrid/Utilities/DiagnosticHandler.cs
@@ -18,6 +18,25 @@ namespace StrongGrid.Utilities
///
internal class DiagnosticHandler : IHttpFilter
{
+ internal class DiagnosticInfo
+ {
+ public WeakReference RequestReference { get; set; }
+
+ public string Diagnostic { get; set; }
+
+ public long RequestTimestamp { get; set; }
+
+ public long ResponseTimestamp { get; set; }
+
+ public DiagnosticInfo(WeakReference requestReference, string diagnostic, long requestTimestamp, long responseTimestamp)
+ {
+ RequestReference = requestReference;
+ Diagnostic = diagnostic;
+ RequestTimestamp = requestTimestamp;
+ ResponseTimestamp = responseTimestamp;
+ }
+ }
+
#region FIELDS
internal const string DIAGNOSTIC_ID_HEADER_NAME = "StrongGrid-Diagnostic-Id";
@@ -29,7 +48,7 @@ internal class DiagnosticHandler : IHttpFilter
#region PROPERTIES
- internal static ConcurrentDictionary RequestReference, string Diagnostic, long RequestTimestamp, long ResponseTimeStamp)> DiagnosticsInfo { get; } = new ConcurrentDictionary, string, long, long)>();
+ internal static ConcurrentDictionary DiagnosticsInfo { get; } = new();
#endregion
@@ -64,7 +83,7 @@ public void OnRequest(IRequest request)
LogContent(diagnostic, httpRequest.Content);
// Add the diagnostic info to our cache
- DiagnosticsInfo.TryAdd(diagnosticId, (new WeakReference(request.Message), diagnostic.ToString(), Stopwatch.GetTimestamp(), long.MinValue));
+ DiagnosticsInfo.TryAdd(diagnosticId, new DiagnosticInfo(new WeakReference(request.Message), diagnostic.ToString(), Stopwatch.GetTimestamp(), long.MinValue));
}
/// Method invoked just after the HTTP response is received. This method can modify the incoming HTTP response.
@@ -76,7 +95,7 @@ public void OnResponse(IResponse response, bool httpErrorAsException)
var httpResponse = response.Message;
var diagnosticId = response.Message.RequestMessage.Headers.GetValue(DIAGNOSTIC_ID_HEADER_NAME);
- if (DiagnosticsInfo.TryGetValue(diagnosticId, out (WeakReference RequestReference, string Diagnostic, long RequestTimestamp, long ResponseTimestamp) diagnosticInfo))
+ if (DiagnosticsInfo.TryGetValue(diagnosticId, out DiagnosticInfo diagnosticInfo))
{
var updatedDiagnostic = new StringBuilder(diagnosticInfo.Diagnostic);
try
@@ -118,8 +137,8 @@ public void OnResponse(IResponse response, bool httpErrorAsException)
DiagnosticsInfo.TryUpdate(
diagnosticId,
- (diagnosticInfo.RequestReference, updatedDiagnostic.ToString(), diagnosticInfo.RequestTimestamp, responseTimestamp),
- (diagnosticInfo.RequestReference, diagnosticInfo.Diagnostic, diagnosticInfo.RequestTimestamp, diagnosticInfo.ResponseTimestamp));
+ new DiagnosticInfo(diagnosticInfo.RequestReference, updatedDiagnostic.ToString(), diagnosticInfo.RequestTimestamp, responseTimestamp),
+ diagnosticInfo);
}
}
@@ -130,7 +149,7 @@ public void OnResponse(IResponse response, bool httpErrorAsException)
#region PRIVATE METHODS
- private void LogHeaders(StringBuilder diagnostic, HttpHeaders httpHeaders)
+ private static void LogHeaders(StringBuilder diagnostic, HttpHeaders httpHeaders)
{
if (httpHeaders != null)
{
@@ -148,7 +167,7 @@ private void LogHeaders(StringBuilder diagnostic, HttpHeaders httpHeaders)
}
}
- private void LogContent(StringBuilder diagnostic, HttpContent httpContent)
+ private static void LogContent(StringBuilder diagnostic, HttpContent httpContent)
{
if (httpContent == null)
{
@@ -172,14 +191,14 @@ private void LogContent(StringBuilder diagnostic, HttpContent httpContent)
}
}
- private void Cleanup()
+ private static void Cleanup()
{
try
{
// Remove diagnostic information for requests that have been garbage collected
foreach (string key in DiagnosticHandler.DiagnosticsInfo.Keys.ToArray())
{
- if (DiagnosticHandler.DiagnosticsInfo.TryGetValue(key, out (WeakReference RequestReference, string Diagnostic, long RequestTimeStamp, long ResponseTimestamp) diagnosticInfo))
+ if (DiagnosticHandler.DiagnosticsInfo.TryGetValue(key, out DiagnosticInfo diagnosticInfo))
{
if (!diagnosticInfo.RequestReference.TryGetTarget(out HttpRequestMessage request))
{
diff --git a/Source/StrongGrid/Utilities/SendGridMultipartFormDataParser.cs b/Source/StrongGrid/Utilities/SendGridMultipartFormDataParser.cs
new file mode 100644
index 00000000..5a46c351
--- /dev/null
+++ b/Source/StrongGrid/Utilities/SendGridMultipartFormDataParser.cs
@@ -0,0 +1,94 @@
+using HttpMultipartParser;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.Json;
+using System.Threading.Tasks;
+
+namespace StrongGrid.Utilities
+{
+ internal class SendGridMultipartFormDataParser : IMultipartFormDataParser
+ {
+ private readonly List _files;
+ private readonly List _parameters;
+ private readonly List> _charsets;
+
+ public IReadOnlyList Files => _files.AsReadOnly();
+
+ public IReadOnlyList Parameters => _parameters.AsReadOnly();
+
+ public IReadOnlyList> Charsets => _charsets.AsReadOnly();
+
+ private SendGridMultipartFormDataParser()
+ {
+ _files = new List();
+ _parameters = new List();
+ _charsets = new List>();
+ }
+
+ public static SendGridMultipartFormDataParser Parse(Stream stream)
+ {
+ var parser = MultipartFormBinaryDataParser.Parse(stream, Encoding.UTF8);
+ return ConvertToSendGridParser(parser);
+ }
+
+ public static async Task ParseAsync(Stream stream)
+ {
+ var parser = await MultipartFormBinaryDataParser.ParseAsync(stream, Encoding.UTF8).ConfigureAwait(false);
+ return ConvertToSendGridParser(parser);
+ }
+
+ public string GetParameterValue(string name, string defaultValue)
+ {
+ var parameter = _parameters.FirstOrDefault(p => p.Name == name);
+ return parameter?.Data ?? defaultValue;
+ }
+
+ private static SendGridMultipartFormDataParser ConvertToSendGridParser(MultipartFormBinaryDataParser parser)
+ {
+ var charsetsParameter = parser.Parameters.FirstOrDefault(p => p.Name.Equals("charsets", StringComparison.OrdinalIgnoreCase));
+ var charsets = JsonDocument.Parse(charsetsParameter?.ToString(Encoding.UTF8) ?? "{}")
+ .RootElement.EnumerateObject()
+ .Select(p => new KeyValuePair(p.Name, p.Value.GetString()))
+ .ToArray();
+
+ var encodings = charsets.ToDictionary(p => p.Key, p => GetEncodingFromName(p.Value));
+
+ var sendGridParser = new SendGridMultipartFormDataParser();
+ foreach (var parameter in parser.Parameters)
+ {
+ // Get the encoding specified by SendGrid for this parameter
+ encodings.TryGetValue(parameter.Name, out Encoding encoding);
+ encoding ??= Encoding.UTF8;
+
+ sendGridParser._parameters.Add(new ParameterPart(parameter.Name, parameter.ToString(encoding)));
+ }
+
+ sendGridParser._files.AddRange(parser.Files);
+ sendGridParser._charsets.AddRange(charsets);
+
+ return sendGridParser;
+ }
+
+ private static Encoding GetEncodingFromName(string encodingName)
+ {
+ try
+ {
+ return Encoding.GetEncoding(encodingName);
+ }
+ catch (ArgumentException)
+ {
+ // ArgumentException is thrown when an "unusual" code page was used to encode a section of the email
+ // For example: {"to":"UTF-8","subject":"UTF-8","from":"UTF-8","text":"iso-8859-10"}
+ // We can see that 'iso-8859-10' was used to encode the "Text" but this encoding is not supported in
+ // .net (neither dotnet full nor dotnet core). Therefore we fallback on UTF-8. This is obviously not
+ // perfect because UTF-8 may or may not be able to handle all the encoded characters, but it's better
+ // than simply erroring out.
+ // See https://github.com/Jericho/StrongGrid/issues/341 for discussion.
+ return Encoding.UTF8;
+ }
+ }
+ }
+}
diff --git a/Source/StrongGrid/Warmup/WarmupEngine.cs b/Source/StrongGrid/Warmup/WarmupEngine.cs
index 2b82ff1c..450a0f05 100644
--- a/Source/StrongGrid/Warmup/WarmupEngine.cs
+++ b/Source/StrongGrid/Warmup/WarmupEngine.cs
@@ -62,7 +62,8 @@ public Task PrepareWithNewIpAddressesAsync(int count, string[] subusers, Cancell
var result = _client.IpAddresses.AddAsync(count, subusers, false, cancellationToken).Result;
var ipAddresses = result.IpAddresses.Select(r => r.Address).ToArray();
return ipAddresses;
- }, cancellationToken);
+ },
+ cancellationToken);
}
///
diff --git a/Source/StrongGrid/WebhookParser.cs b/Source/StrongGrid/WebhookParser.cs
index b1350d21..ea9a5e7d 100644
--- a/Source/StrongGrid/WebhookParser.cs
+++ b/Source/StrongGrid/WebhookParser.cs
@@ -1,4 +1,3 @@
-using HttpMultipartParser;
using StrongGrid.Json;
using StrongGrid.Models.Webhooks;
using StrongGrid.Utilities;
@@ -164,51 +163,18 @@ public async Task ParseInboundEmailWebhookAsync(Stream stream)
// Therefore, we must make a copy of the stream if it doesn't allow changing the position
if (!stream.CanSeek)
{
- using (var ms = Utils.MemoryStreamManager.GetStream())
- {
- await stream.CopyToAsync(ms).ConfigureAwait(false);
- return await ParseInboundEmailWebhookAsync(ms).ConfigureAwait(false);
- }
+ using var ms = Utils.MemoryStreamManager.GetStream();
+ await stream.CopyToAsync(ms).ConfigureAwait(false);
+ return await ParseInboundEmailWebhookAsync(ms).ConfigureAwait(false);
}
// It's important to rewind the stream
stream.Position = 0;
// Asynchronously parse the multipart content received from SendGrid
- var parser = await MultipartFormDataParser.ParseAsync(stream, Encoding.UTF8).ConfigureAwait(false);
-
- // Convert the 'charset' from a string into array of KeyValuePair
- var charsets = JsonDocument.Parse(parser.GetParameterValue("charsets", "{}"))
- .RootElement.EnumerateObject()
- .Select(prop => new KeyValuePair(prop.Name, prop.Value.GetString()))
- .ToArray();
-
- // Create a dictionary of parsers, one parser for each desired encoding.
- // This is necessary because MultipartFormDataParser can only handle one
- // encoding and SendGrid can use different encodings for parameters such
- // as "from", "to", "text" and "html".
- var encodedParsers = charsets
- .Select(c => c.Value)
- .Select(GetEncodingFromName)
- .Distinct()
- .Where(encoding => !encoding.Equals(Encoding.UTF8))
- .Select(async encoding =>
- {
- stream.Position = 0; // It's important to rewind the stream
- return new
- {
- Encoding = encoding,
- Parser = await MultipartFormDataParser.ParseAsync(stream, encoding).ConfigureAwait(false)
- };
- })
- .Select(r => r.Result)
- .Union(new[]
- {
- new { Encoding = Encoding.UTF8, Parser = parser }
- })
- .ToDictionary(ep => ep.Encoding, ep => ep.Parser);
+ var parser = await SendGridMultipartFormDataParser.ParseAsync(stream).ConfigureAwait(false);
- return ParseInboundEmail(encodedParsers, charsets);
+ return ParseInboundEmail(parser);
}
///
@@ -223,103 +189,26 @@ public InboundEmail ParseInboundEmailWebhook(Stream stream)
// Therefore, we must make a copy of the stream if it doesn't allow changing the position
if (!stream.CanSeek)
{
- using (var ms = Utils.MemoryStreamManager.GetStream())
- {
- stream.CopyTo(ms);
- return ParseInboundEmailWebhook(ms);
- }
+ using var ms = Utils.MemoryStreamManager.GetStream();
+ stream.CopyTo(ms);
+ return ParseInboundEmailWebhook(ms);
}
// It's important to rewind the stream
stream.Position = 0;
// Parse the multipart content received from SendGrid
- var parser = MultipartFormDataParser.Parse(stream, Encoding.UTF8);
-
- // Convert the 'charset' from a string into array of KeyValuePair
- var charsets = JsonDocument.Parse(parser.GetParameterValue("charsets", "{}"))
- .RootElement.EnumerateObject()
- .Select(prop => new KeyValuePair(prop.Name, prop.Value.GetString()))
- .ToArray();
-
- // Create a dictionary of parsers, one parser for each desired encoding.
- // This is necessary because MultipartFormDataParser can only handle one
- // encoding and SendGrid can use different encodings for parameters such
- // as "from", "to", "text" and "html".
- var encodedParsers = charsets
- .Select(c => c.Value)
- .Select(GetEncodingFromName)
- .Distinct()
- .Where(encoding => !encoding.Equals(Encoding.UTF8))
- .Select(encoding =>
- {
- stream.Position = 0; // It's important to rewind the stream
- return new
- {
- Encoding = encoding,
- Parser = MultipartFormDataParser.Parse(stream, encoding)
- };
- })
- .Union(new[]
- {
- new { Encoding = Encoding.UTF8, Parser = parser }
- })
- .ToDictionary(ep => ep.Encoding, ep => ep.Parser);
+ var parser = SendGridMultipartFormDataParser.Parse(stream);
- return ParseInboundEmail(encodedParsers, charsets);
+ return ParseInboundEmail(parser);
}
#endregion
#region PRIVATE METHODS
- private static Encoding GetEncoding(string parameterName, IEnumerable> charsets)
- {
- var encoding = charsets.Where(c => c.Key == parameterName);
- if (!encoding.Any()) return Encoding.UTF8;
-
- var encodingName = encoding.First().Value;
- return GetEncodingFromName(encodingName);
- }
-
- private static Encoding GetEncodingFromName(string encodingName)
+ private static InboundEmail ParseInboundEmail(SendGridMultipartFormDataParser parser)
{
- try
- {
- return Encoding.GetEncoding(encodingName);
- }
- catch (ArgumentException)
- {
- // ArgumentException is thrown when an "unusual" code page was used to encode a section of the email
- // For example: {"to":"UTF-8","subject":"UTF-8","from":"UTF-8","text":"iso-8859-10"}
- // We can see that 'iso-8859-10' was used to encode the "Text" but this encoding is not supported in
- // .net (neither dotnet full nor dotnet core). Therefore we fallback on UTF-8. This is obviously not
- // perfect because UTF-8 may or may not be able to handle all the encoded characters, but it's better
- // than simply erroring out.
- // See https://github.com/Jericho/StrongGrid/issues/341 for discussion.
- return Encoding.UTF8;
- }
- }
-
- private static MultipartFormDataParser GetEncodedParser(string parameterName, IEnumerable> charsets, IDictionary encodedParsers)
- {
- var encoding = GetEncoding(parameterName, charsets);
- var parser = encodedParsers[encoding];
- return parser;
- }
-
- private static string GetEncodedValue(string parameterName, IEnumerable> charsets, IDictionary encodedParsers, string defaultValue = null)
- {
- var parser = GetEncodedParser(parameterName, charsets, encodedParsers);
- var value = parser.GetParameterValue(parameterName, defaultValue);
- return value;
- }
-
- private static InboundEmail ParseInboundEmail(IDictionary encodedParsers, KeyValuePair[] charsets)
- {
- // Get the default UTF8 parser
- var parser = encodedParsers.Single(p => p.Key.Equals(Encoding.UTF8)).Value;
-
// Convert the 'headers' from a string into array of KeyValuePair
var headers = parser
.GetParameterValue("headers", string.Empty)
@@ -358,33 +247,33 @@ private static InboundEmail ParseInboundEmail(IDictionary(parser.GetParameterValue("envelope", "{}"), JsonFormatter.DeserializerOptions);
// Convert the 'from' from a string into an email address
- var rawFrom = GetEncodedValue("from", charsets, encodedParsers, string.Empty);
+ var rawFrom = parser.GetParameterValue("from", string.Empty);
var from = MailAddressParser.ParseEmailAddress(rawFrom);
// Convert the 'to' from a string into an array of email addresses
- var rawTo = GetEncodedValue("to", charsets, encodedParsers, string.Empty);
+ var rawTo = parser.GetParameterValue("to", string.Empty);
var to = MailAddressParser.ParseEmailAddresses(rawTo);
// Convert the 'cc' from a string into an array of email addresses
- var rawCc = GetEncodedValue("cc", charsets, encodedParsers, string.Empty);
+ var rawCc = parser.GetParameterValue("cc", string.Empty);
var cc = MailAddressParser.ParseEmailAddresses(rawCc);
// Arrange the InboundEmail
var inboundEmail = new InboundEmail
{
Attachments = attachments,
- Charsets = charsets,
- Dkim = GetEncodedValue("dkim", charsets, encodedParsers, null),
+ Charsets = parser.Charsets.ToArray(),
+ Dkim = parser.GetParameterValue("dkim", null),
Envelope = envelope,
From = from,
Headers = headers,
- Html = GetEncodedValue("html", charsets, encodedParsers, null),
- SenderIp = GetEncodedValue("sender_ip", charsets, encodedParsers, null),
- SpamReport = GetEncodedValue("spam_report", charsets, encodedParsers, null),
- SpamScore = GetEncodedValue("spam_score", charsets, encodedParsers, null),
- Spf = GetEncodedValue("SPF", charsets, encodedParsers, null),
- Subject = GetEncodedValue("subject", charsets, encodedParsers, null),
- Text = GetEncodedValue("text", charsets, encodedParsers, null),
+ Html = parser.GetParameterValue("html", null),
+ SenderIp = parser.GetParameterValue("sender_ip", null),
+ SpamReport = parser.GetParameterValue("spam_report", null),
+ SpamScore = parser.GetParameterValue("spam_score", null),
+ Spf = parser.GetParameterValue("SPF", null),
+ Subject = parser.GetParameterValue("subject", null),
+ Text = parser.GetParameterValue("text", null),
To = to,
Cc = cc,
RawEmail = rawEmail
diff --git a/build.cake b/build.cake
index 766b3a4c..18022cd1 100644
--- a/build.cake
+++ b/build.cake
@@ -2,13 +2,13 @@
#tool dotnet:?package=GitVersion.Tool&version=5.11.1
#tool dotnet:?package=coveralls.net&version=4.0.1
#tool nuget:?package=GitReleaseManager&version=0.13.0
-#tool nuget:?package=ReportGenerator&version=5.1.11
+#tool nuget:?package=ReportGenerator&version=5.1.13
#tool nuget:?package=xunit.runner.console&version=2.4.2
#tool nuget:?package=Codecov&version=1.13.0
// Install addins.
#addin nuget:?package=Cake.Coveralls&version=1.1.0
-#addin nuget:?package=Cake.Git&version=2.0.0
+#addin nuget:?package=Cake.Git&version=3.0.0
#addin nuget:?package=Cake.Codecov&version=1.0.1
@@ -297,7 +297,7 @@ Task("Run-Code-Coverage")
Task("Upload-Coverage-Result-Coveralls")
.IsDependentOn("Run-Code-Coverage")
- .OnError(exception => Information($"ONERROR: Failed to upload coverage result to Coveralls: {exception.Message}"))
+ .OnError(exception => Information($"ONERROR: Failed to upload coverage result to Coveralls: {exception.Message}"))
.Does(() =>
{
//CoverallsNet(new FilePath($"{codeCoverageDir}coverage.{DefaultFramework}.xml"), CoverallsNetReportType.OpenCover, new CoverallsNetSettings()
@@ -308,7 +308,7 @@ Task("Upload-Coverage-Result-Coveralls")
Task("Upload-Coverage-Result-Codecov")
.IsDependentOn("Run-Code-Coverage")
- .OnError(exception => Information($"ONERROR: Failed to upload coverage result to Codecov: {exception.Message}"))
+ .OnError(exception => Information($"ONERROR: Failed to upload coverage result to Codecov: {exception.Message}"))
.Does(() =>
{
//Codecov($"{codeCoverageDir}coverage.{DefaultFramework}.xml", codecovToken);