Skip to content

Commit

Permalink
Merge branch 'release/0.97.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
Jericho committed Jan 12, 2023
2 parents 21518aa + 778e9dd commit dbb7d45
Show file tree
Hide file tree
Showing 18 changed files with 360 additions and 354 deletions.
2 changes: 1 addition & 1 deletion Source/StrongGrid.Benchmark/StrongGrid.Benchmark.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.2" />
<PackageReference Include="BenchmarkDotNet" Version="0.13.3" />
<PackageReference Include="RichardSzalay.MockHttp" Version="6.0.0" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<PackageReference Include="Logzio.DotNet.NLog" Version="1.0.13" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.2.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.2.1" />
</ItemGroup>

<ItemGroup>
Expand Down
4 changes: 2 additions & 2 deletions Source/StrongGrid.UnitTests/StrongGrid.UnitTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
<PackageReference Include="Moq" Version="4.18.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="RichardSzalay.MockHttp" Version="6.0.0" />
<PackageReference Include="Shouldly" Version="4.1.0" />
<PackageReference Include="xunit" Version="2.4.2" />
Expand Down
33 changes: 14 additions & 19 deletions Source/StrongGrid/Extensions/Internal.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using HttpMultipartParser;
using Pathoschild.Http.Client;
using StrongGrid.Json;
using StrongGrid.Models;
Expand All @@ -19,6 +18,7 @@
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using static StrongGrid.Utilities.DiagnosticHandler;

namespace StrongGrid
{
Expand All @@ -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);

/// <summary>
/// Converts a 'unix time', which is expressed as the number of seconds (or milliseconds) since
Expand Down Expand Up @@ -117,9 +117,13 @@ internal static async Task<string> 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
Expand All @@ -130,7 +134,11 @@ internal static async Task<string> 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
Expand Down Expand Up @@ -184,19 +192,6 @@ internal static Encoding GetEncoding(this HttpContent content, Encoding defaultE
return encoding;
}

/// <summary>
/// Returns the value of a parameter or the default value if it doesn't exist.
/// </summary>
/// <param name="parser">The parser.</param>
/// <param name="name">The name of the parameter.</param>
/// <param name="defaultValue">The default value.</param>
/// <returns>The value of the parameter.</returns>
internal static string GetParameterValue(this MultipartFormDataParser parser, string name, string defaultValue)
{
if (parser.HasParameter(name)) return parser.GetParameterValue(name);
else return defaultValue;
}

/// <summary>Asynchronously retrieve the JSON encoded response body and convert it to an object of the desired type.</summary>
/// <typeparam name="T">The response model to deserialize into.</typeparam>
/// <param name="response">The response.</param>
Expand Down Expand Up @@ -581,14 +576,14 @@ internal static IEnumerable<KeyValuePair<string, string>> ParseQuerystring(this
return querystringParameters;
}

internal static (WeakReference<HttpRequestMessage> 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<HttpRequestMessage> 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}";
Expand Down
106 changes: 104 additions & 2 deletions Source/StrongGrid/Extensions/Public.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;

Expand Down Expand Up @@ -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);
}
}
}
Expand Down Expand Up @@ -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);
}
}
}
Expand All @@ -1334,5 +1335,106 @@ public static Task AddAddressToUnsubscribeGroupAsync(this ISuppressions suppress
{
return suppressions.AddAddressesToUnsubscribeGroupAsync(groupId, new[] { email }, onBehalfOf, cancellationToken);
}

/// <summary>
/// Generate a new API Key for billing.
/// </summary>
/// <param name="apiKeys">The ApiKeys resource.</param>
/// <param name="name">The name.</param>
/// <param name="onBehalfOf">The user to impersonate.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>
/// The <see cref="ApiKey" />.
/// </returns>
public static Task<ApiKey> 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);
}

/// <summary>
/// Generate a new API Key with the same permissions that have been granted to you.
/// </summary>
/// <param name="apiKeys">The ApiKeys resource.</param>
/// <param name="name">The name.</param>
/// <param name="onBehalfOf">The user to impersonate.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>
/// The <see cref="ApiKey" />.
/// </returns>
/// <remarks>
/// If you specify an API Key when instanciating the <see cref="Client" />, the new API Key will inherit the permissions of that API Key.
/// If you specify a username and password when instanciating the <see cref="Client" />, the new API Key will inherit the permissions of that user.
/// </remarks>
public static async Task<ApiKey> 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;
}

/// <summary>
/// Generate a new API Key with the same "read" permissions that have ben granted to you.
/// </summary>
/// <param name="apiKeys">The ApiKeys resource.</param>
/// <param name="name">The name.</param>
/// <param name="onBehalfOf">The user to impersonate.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>
/// The <see cref="ApiKey" />.
/// </returns>
/// <remarks>
/// If you specify an API Key when instanciating the <see cref="Client" />, the new API Key will inherit the "read" permissions of that API Key.
/// If you specify a username and password when instanciating the <see cref="Client" />, the new API Key will inherit the "read" permissions of that user.
/// </remarks>
public static async Task<ApiKey> 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;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="teammates">The teammates resource.</param>
/// <param name="email">The email address of the teammate.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// The async task.
/// </returns>
/// <remarks>
/// 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.
/// </remarks>
public static async Task<TeammateInvitation> 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);
}
}
}
1 change: 1 addition & 0 deletions Source/StrongGrid/Json/EventConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ internal class EventConverter : BaseJsonConverter<Event>
{
"asm_group_id",
"attempt",
"bounce_classification",
"category",
"cert_err",
"email",
Expand Down
55 changes: 55 additions & 0 deletions Source/StrongGrid/Models/BounceClassification.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using StrongGrid.Json;
using System.Runtime.Serialization;
using System.Text.Json.Serialization;

namespace StrongGrid.Models
{
/// <summary>
/// Enumeration to indicate the type of SMTP failure.
/// </summary>
[JsonConverter(typeof(StringEnumConverter<BounceClassification>))]
public enum BounceClassification
{
/// <summary>
/// Unclassified.
/// </summary>
[EnumMember(Value = "Unclassified")]
Unclassified,

/// <summary>
/// Invalid Addres.
/// </summary>
[EnumMember(Value = "Invalid Address")]
InvalidAddress,

/// <summary>
/// Technical.
/// </summary>
[EnumMember(Value = "Technical")]
Technical,

/// <summary>
/// Content.
/// </summary>
[EnumMember(Value = "Content")]
Content,

/// <summary>
/// Reputation.
/// </summary>
[EnumMember(Value = "Reputation")]
Reputation,

/// <summary>
/// Frequency or volume.
/// </summary>
[EnumMember(Value = "Frequency or Volume Too High")]
FrequencyOrVolume,

/// <summary>
/// Mailbox Unavailable.
/// </summary>
[EnumMember(Value = "Mailbox Unavailable")]
MailboxUnavailable,
}
}
10 changes: 10 additions & 0 deletions Source/StrongGrid/Models/Webhooks/BouncedEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,15 @@ public BouncedEvent()
/// </value>
[JsonPropertyName("type")]
public BounceType Type { get; set; }

/// <summary>
/// Gets or sets a value indicating the type of SMTP failure.
/// </summary>
/// <value>The SMTP failure classification.</value>
/// <remarks>
/// See <a href="https://docs.sendgrid.com/ui/analytics-and-reporting/bounce-and-block-classifications">SendGrid' Bounce Classifications documentation </a> to understand each classification.
/// </remarks>
[JsonPropertyName("bounce_classification")]
public BounceClassification Classification { get; set; }
}
}
63 changes: 0 additions & 63 deletions Source/StrongGrid/Resources/ApiKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,69 +143,6 @@ public Task<ApiKey> UpdateAsync(string keyId, string name, Parameter<IEnumerable
.AsObject<ApiKey>();
}

/// <summary>
/// Generate a new API Key for billing.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="onBehalfOf">The user to impersonate.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>
/// The <see cref="ApiKey" />.
/// </returns>
public Task<ApiKey> 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);
}

/// <summary>
/// Generate a new API Key with the same permissions that have been granted to you.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="onBehalfOf">The user to impersonate.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>
/// The <see cref="ApiKey" />.
/// </returns>
/// <remarks>
/// If you specify an API Key when instanciating the <see cref="Client" />, the new API Key will inherit the permissions of that API Key.
/// If you specify a username and password when instanciating the <see cref="Client" />, the new API Key will inherit the permissions of that user.
/// </remarks>
public async Task<ApiKey> 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;
}

/// <summary>
/// Generate a new API Key with the same "read" permissions that have ben granted to you.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="onBehalfOf">The user to impersonate.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>
/// The <see cref="ApiKey" />.
/// </returns>
/// <remarks>
/// If you specify an API Key when instanciating the <see cref="Client" />, the new API Key will inherit the "read" permissions of that API Key.
/// If you specify a username and password when instanciating the <see cref="Client" />, the new API Key will inherit the "read" permissions of that user.
/// </remarks>
public async Task<ApiKey> 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<IEnumerable<string>> scopes)
{
var result = new StrongGridJsonObject();
Expand Down
Loading

0 comments on commit dbb7d45

Please sign in to comment.