Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions src/Resend/IResend.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1093,6 +1093,112 @@ public interface IResend

#endregion

#region Suppressions

/// <summary>
/// Adds an email address to the suppression list.
/// </summary>
/// <param name="email">Email address to suppress.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>New suppression identifier.</returns>
/// <see href="https://resend.com/docs/api-reference/suppressions/add-suppression"/>
Task<ResendResponse<Guid>> SuppressionAddAsync( string email, CancellationToken cancellationToken = default );

/// <summary>
/// Lists suppressions.
/// </summary>
/// <param name="query">Pagination query, optionally filtered by origin.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Page of suppressions.</returns>
/// <remarks>
/// List entries do not carry the <c>object</c> discriminator; use
/// <see cref="SuppressionRetrieveAsync"/> for the full <see cref="Suppression"/>.
/// </remarks>
/// <see href="https://resend.com/docs/api-reference/suppressions/list-suppressions"/>
Task<ResendResponse<PaginatedResult<SuppressionSummary>>> SuppressionListAsync( SuppressionListQuery? query = null, CancellationToken cancellationToken = default );

/// <summary>
/// Retrieves a single suppression.
/// </summary>
/// <param name="suppressionIdOrEmail">Suppression identifier or the suppressed email address.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Suppression.</returns>
/// <exception cref="ArgumentException">
/// Thrown when <paramref name="suppressionIdOrEmail"/> is null, empty or whitespace.
/// </exception>
/// <see href="https://resend.com/docs/api-reference/suppressions/get-suppression"/>
Task<ResendResponse<Suppression>> SuppressionRetrieveAsync( string suppressionIdOrEmail, CancellationToken cancellationToken = default );

/// <summary>
/// Removes a single suppression, allowing Resend to deliver to the address again.
/// </summary>
/// <param name="suppressionIdOrEmail">Suppression identifier or the suppressed email address.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Outcome of the removal.</returns>
/// <exception cref="ArgumentException">
/// Thrown when <paramref name="suppressionIdOrEmail"/> is null, empty or whitespace.
/// </exception>
/// <see href="https://resend.com/docs/api-reference/suppressions/remove-suppression"/>
Task<ResendResponse<SuppressionRemoveResult>> SuppressionRemoveAsync( string suppressionIdOrEmail, CancellationToken cancellationToken = default );

/// <summary>
/// Adds up to 100 email addresses to the suppression list at once.
/// </summary>
/// <param name="emails">Email addresses to suppress.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>New suppression identifiers.</returns>
/// <remarks>
/// Fewer identifiers may be returned than addresses sent: the API lowercases, trims and
/// dedupes the addresses, so case variants of one address collapse into a single entry.
/// Do not pair the result with the input by index. Re-adding an already-suppressed
/// address is not an error -- it returns the existing identifier.
/// </remarks>
/// <see href="https://resend.com/docs/api-reference/suppressions/add-suppressions"/>
Task<ResendResponse<List<Guid>>> SuppressionBatchAddAsync( IEnumerable<string> emails, CancellationToken cancellationToken = default );

/// <summary>
/// Removes up to 100 suppressions at once, by email address.
/// </summary>
/// <param name="emails">Suppressed email addresses to remove.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Outcome of each removal.</returns>
/// <remarks>
/// The API accepts either email addresses or identifiers in a single call, never both;
/// use <see cref="SuppressionBatchRemoveAsync(IEnumerable{Guid}, CancellationToken)"/> to
/// remove by identifier.
/// <para>
/// Only addresses that were actually suppressed appear in the result, and the addresses
/// are lowercased, trimmed and deduped first -- so the result may be shorter than the
/// input and must not be paired with it by index. Unlike
/// <see cref="SuppressionRemoveAsync"/>, an address that was not suppressed is silently
/// absent rather than a not-found error.
/// </para>
/// </remarks>
/// <see href="https://resend.com/docs/api-reference/suppressions/remove-suppressions"/>
Task<ResendResponse<List<SuppressionRemoveResult>>> SuppressionBatchRemoveAsync( IEnumerable<string> emails, CancellationToken cancellationToken = default );

/// <summary>
/// Removes up to 100 suppressions at once, by identifier.
/// </summary>
/// <param name="suppressionIds">Suppression identifiers to remove.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Outcome of each removal.</returns>
/// <remarks>
/// The API accepts either email addresses or identifiers in a single call, never both;
/// use <see cref="SuppressionBatchRemoveAsync(IEnumerable{string}, CancellationToken)"/> to
/// remove by email address.
/// <para>
/// Only identifiers that were actually suppressed appear in the result, so it may be
/// shorter than the input and must not be paired with it by index. Unlike
/// <see cref="SuppressionRemoveAsync"/>, an identifier that was not suppressed is
/// silently absent rather than a not-found error.
/// </para>
/// </remarks>
/// <see href="https://resend.com/docs/api-reference/suppressions/remove-suppressions"/>
Task<ResendResponse<List<SuppressionRemoveResult>>> SuppressionBatchRemoveAsync( IEnumerable<Guid> suppressionIds, CancellationToken cancellationToken = default );

#endregion

#region Automations and events

/// <summary>
Expand Down
63 changes: 63 additions & 0 deletions src/Resend/Json/JsonStringEnumValue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System.Reflection;

namespace Resend;

/// <summary>
/// Wire values of <typeparamref name="T"/>, as declared by <see cref="JsonStringValueAttribute"/>.
/// </summary>
internal static class JsonStringEnumValue<T>
where T : struct, Enum
{
/// <summary />
static JsonStringEnumValue()
{
var tt = typeof( T );

var names = tt.GetEnumNames();
var values = tt.GetEnumValues();

var fwd = new Dictionary<T, string>( names.Length );
var rev = new Dictionary<string, T>( names.Length );

for ( var i = 0; i < names.Length; i++ )
{
var name = names[ i ];
var field = tt.GetField( name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static )!;

var str = field.GetCustomAttribute<JsonStringValueAttribute>()?.Value ?? name;
var val = (T) values.GetValue( i )!;

fwd.Add( val, str );
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
rev.Add( str, val );
}

Forward = fwd;
Reverse = rev;
}


/// <summary />
public static IReadOnlyDictionary<T, string> Forward { get; }


/// <summary />
public static IReadOnlyDictionary<string, T> Reverse { get; }


/// <summary>
/// Wire value of <paramref name="value"/>, for use outside of JSON serialization -- eg. in
/// query string parameters.
/// </summary>
/// <param name="value">Enumeration value.</param>
/// <returns>Wire value.</returns>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when <paramref name="value"/> is not a member of <typeparamref name="T"/>.
/// </exception>
public static string Of( T value )
{
if ( Forward.TryGetValue( value, out var str ) == false )
throw new ArgumentOutOfRangeException( nameof( value ), $"Invalid '{typeof( T ).Name}' value: '{value}'" );

return str;
}
}
41 changes: 3 additions & 38 deletions src/Resend/Json/JsonStringEnumValueConverter.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Reflection;
using System.Text.Json;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Resend;
Expand All @@ -8,38 +7,6 @@ namespace Resend;
public class JsonStringEnumValueConverter<T> : JsonConverter<T>
where T : struct, Enum
{
private readonly Dictionary<T, string> _fwd;
private readonly Dictionary<string, T> _rev;


/// <summary />
public JsonStringEnumValueConverter()
{
var tt = typeof( T );

var names = tt.GetEnumNames();
var values = tt.GetEnumValues();

var fwd = new Dictionary<T, string>( names.Length );
var rev = new Dictionary<string, T>( names.Length );

for ( var i = 0; i < names.Length; i++ )
{
var name = names[ i ];
var field = tt.GetField( name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static )!;

var str = field.GetCustomAttribute<JsonStringValueAttribute>()?.Value ?? name;
var val = (T) values.GetValue( i )!;

fwd.Add( val, str );
rev.Add( str, val );
}

_fwd = fwd;
_rev = rev;
}


/// <inheritdoc />
public override T Read( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options )
{
Expand All @@ -52,7 +19,7 @@ public override T Read( ref Utf8JsonReader reader, Type typeToConvert, JsonSeria
/*
*
*/
if ( _rev.TryGetValue( json, out var rev ) == false )
if ( JsonStringEnumValue<T>.Reverse.TryGetValue( json, out var rev ) == false )
throw new JsonException( $"SE002: Invalid value: '{json}'" );

return rev;
Expand All @@ -62,11 +29,9 @@ public override T Read( ref Utf8JsonReader reader, Type typeToConvert, JsonSeria
/// <inheritdoc />
public override void Write( Utf8JsonWriter writer, T value, JsonSerializerOptions options )
{
if ( _fwd.ContainsKey( value ) == false )
if ( JsonStringEnumValue<T>.Forward.TryGetValue( value, out var str ) == false )
throw new JsonException( $"SE003: Invalid '{typeof( T ).Name}' value: '{value}'" );

var str = _fwd[ value ];

writer.WriteStringValue( str );
}
}
15 changes: 15 additions & 0 deletions src/Resend/Payloads/SuppressionAddRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;

namespace Resend.Payloads;

/// <summary>
/// Request object to add a suppression.
/// </summary>
public class SuppressionAddRequest
{
/// <summary>
/// Email address to suppress.
/// </summary>
[JsonPropertyName( "email" )]
public string Email { get; set; } = default!;
}
15 changes: 15 additions & 0 deletions src/Resend/Payloads/SuppressionBatchAddRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;

namespace Resend.Payloads;

/// <summary>
/// Request object to add up to 100 suppressions at once.
/// </summary>
public class SuppressionBatchAddRequest
{
/// <summary>
/// Email addresses to suppress.
/// </summary>
[JsonPropertyName( "emails" )]
public List<string> Emails { get; set; } = default!;
}
28 changes: 28 additions & 0 deletions src/Resend/Payloads/SuppressionBatchRemoveRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.Text.Json.Serialization;

namespace Resend.Payloads;

/// <summary>
/// Request object to remove up to 100 suppressions at once.
/// </summary>
/// <remarks>
/// The API accepts either <see cref="Emails"/> or <see cref="Ids"/>, never both, and
/// rejects an explicit null -- hence the unset one is omitted from the payload rather
/// than written as null.
/// </remarks>
public class SuppressionBatchRemoveRequest
{
/// <summary>
/// Email addresses to remove from the suppression list.
/// </summary>
[JsonPropertyName( "emails" )]
[JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingNull )]
public List<string>? Emails { get; set; }

/// <summary>
/// Suppression identifiers to remove from the suppression list.
/// </summary>
[JsonPropertyName( "ids" )]
[JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingNull )]
public List<Guid>? Ids { get; set; }
}
Loading
Loading