Skip to content

Commit aa8c190

Browse files
committed
Add EmailBatch overloads
1 parent 0f58989 commit aa8c190

File tree

7 files changed

+300
-7
lines changed

7 files changed

+300
-7
lines changed

src/Resend/EmailBatchResponse.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Resend;
4+
5+
/// <summary />
6+
public class EmailBatchResponse
7+
{
8+
/// <summary>
9+
/// List of message identifiers.
10+
/// </summary>
11+
[JsonPropertyName( "data" )]
12+
public List<EmailBatchReceipt> Data { get; set; } = default!;
13+
14+
/// <summary>
15+
/// List of validation errors.
16+
/// </summary>
17+
/// <remarks>
18+
/// Only present in permissive validation mode, otherwise it is null.
19+
/// </remarks>
20+
[JsonPropertyName( "errors" )]
21+
public List<EmailBatchError>? Errors { get; set; }
22+
}
23+
24+
25+
/// <summary />
26+
public class EmailBatchReceipt
27+
{
28+
/// <summary>
29+
/// Message identifier.
30+
/// </summary>
31+
[JsonPropertyName( "id" )]
32+
public Guid Id { get; set; } = default!;
33+
}
34+
35+
36+
/// <summary />
37+
public class EmailBatchError
38+
{
39+
/// <summary>
40+
/// Index of the email in the batch request
41+
/// </summary>
42+
[JsonPropertyName( "index" )]
43+
public int Index { get; set; }
44+
45+
/// <summary>
46+
/// Error message identifying the validation error
47+
/// </summary>
48+
[JsonPropertyName( "message" )]
49+
public string Message { get; set; } = default!;
50+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Resend;
4+
5+
/// <summary>
6+
/// Determines behavior when sending batch emails.
7+
/// </summary>
8+
[JsonConverter( typeof( JsonStringEnumValueConverter<EmailBatchValidationMode> ) )]
9+
public enum EmailBatchValidationMode
10+
{
11+
/// <summary>
12+
/// Sends the batch only if all emails in the request are valid.
13+
/// </summary>
14+
Strict = 1,
15+
16+
/// <summary>
17+
/// Processes all emails, allowing for partial success and returning
18+
/// validation errors if present.
19+
/// </summary>
20+
Permissive,
21+
}

src/Resend/IResend.cs

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public interface IResend
5858

5959

6060
/// <summary>
61-
/// Send a batch of emails.
61+
/// Send a batch of emails, if and only if all messages are valid.
6262
/// </summary>
6363
/// <param name="emails">
6464
/// List of emails.
@@ -74,8 +74,9 @@ public interface IResend
7474

7575

7676
/// <summary>
77-
/// Send a batch of emails using idempotency key, such that retries do not yield
78-
/// duplicate submissions.
77+
/// Send a batch of emails using idempotency key, such that retries do not yield
78+
/// duplicate submissions. Emails are only send if and only if all messages are
79+
/// valid.
7980
/// </summary>
8081
/// <param name="idempotencyKey">
8182
/// Idempotency key.
@@ -93,6 +94,47 @@ public interface IResend
9394
Task<ResendResponse<List<Guid>>> EmailBatchAsync( string idempotencyKey, IEnumerable<EmailMessage> emails, CancellationToken cancellationToken = default );
9495

9596

97+
/// <summary>
98+
/// Send a batch of emails.
99+
/// </summary>
100+
/// <param name="emails">
101+
/// List of emails.
102+
/// </param>
103+
/// <param name="validationMode">
104+
/// Validation mode.
105+
/// </param>
106+
/// <param name="cancellationToken">
107+
/// Cancellation token.
108+
/// </param>
109+
/// <returns>
110+
/// List of email identifiers.
111+
/// </returns>
112+
/// <see href="https://resend.com/docs/api-reference/emails/send-batch-emails"/>
113+
Task<ResendResponse<EmailBatchResponse>> EmailBatchAsync( IEnumerable<EmailMessage> emails, EmailBatchValidationMode validationMode, CancellationToken cancellationToken = default );
114+
115+
116+
/// <summary>
117+
/// Send a batch of emails.
118+
/// </summary>
119+
/// <param name="idempotencyKey">
120+
/// Idempotency key.
121+
/// </param>
122+
/// <param name="emails">
123+
/// List of emails.
124+
/// </param>
125+
/// <param name="validationMode">
126+
/// Validation mode.
127+
/// </param>
128+
/// <param name="cancellationToken">
129+
/// Cancellation token.
130+
/// </param>
131+
/// <returns>
132+
/// List of email identifiers.
133+
/// </returns>
134+
/// <see href="https://resend.com/docs/api-reference/emails/send-batch-emails"/>
135+
Task<ResendResponse<EmailBatchResponse>> EmailBatchAsync( string idempotencyKey, IEnumerable<EmailMessage> emails, EmailBatchValidationMode validationMode, CancellationToken cancellationToken = default );
136+
137+
96138
/// <summary>
97139
/// Reschedule an email.
98140
/// </summary>

src/Resend/ResendClient.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ public ResendClient( IOptionsSnapshot<ResendClientOptions> options, HttpClient h
9999
var path = $"/emails/batch";
100100
var req = new HttpRequestMessage( HttpMethod.Post, path );
101101
req.Content = JsonContent.Create( emails );
102+
req.Headers.Add( "x-batch-validation", "strict" );
102103

103104
return Execute<ListOf<ObjectId>, List<Guid>>( req, ( x ) => x.Data.Select( y => y.Id ).ToList(), cancellationToken );
104105
}
@@ -111,11 +112,51 @@ public ResendClient( IOptionsSnapshot<ResendClientOptions> options, HttpClient h
111112
var req = new HttpRequestMessage( HttpMethod.Post, path );
112113
req.Content = JsonContent.Create( emails );
113114
req.Headers.Add( IdempotencyKey, idempotencyKey );
115+
req.Headers.Add( "x-batch-validation", "strict" );
114116

115117
return Execute<ListOf<ObjectId>, List<Guid>>( req, ( x ) => x.Data.Select( y => y.Id ).ToList(), cancellationToken );
116118
}
117119

118120

121+
/// <inheritdoc />
122+
public Task<ResendResponse<EmailBatchResponse>> EmailBatchAsync( IEnumerable<EmailMessage> emails, EmailBatchValidationMode validationMode, CancellationToken cancellationToken = default )
123+
{
124+
var mode = validationMode switch
125+
{
126+
EmailBatchValidationMode.Strict => "strict",
127+
EmailBatchValidationMode.Permissive => "permissive",
128+
_ => throw new NotImplementedException( $"Validation mode {validationMode} is not implemented" )
129+
};
130+
131+
var path = $"/emails/batch";
132+
var req = new HttpRequestMessage( HttpMethod.Post, path );
133+
req.Content = JsonContent.Create( emails );
134+
req.Headers.Add( "x-batch-validation", mode );
135+
136+
return Execute<EmailBatchResponse, EmailBatchResponse>( req, ( x ) => x, cancellationToken );
137+
}
138+
139+
140+
/// <inheritdoc />
141+
public Task<ResendResponse<EmailBatchResponse>> EmailBatchAsync( string idempotencyKey, IEnumerable<EmailMessage> emails, EmailBatchValidationMode validationMode, CancellationToken cancellationToken = default )
142+
{
143+
var mode = validationMode switch
144+
{
145+
EmailBatchValidationMode.Strict => "strict",
146+
EmailBatchValidationMode.Permissive => "permissive",
147+
_ => throw new NotImplementedException( $"Validation mode {validationMode} is not implemented" )
148+
};
149+
150+
var path = $"/emails/batch";
151+
var req = new HttpRequestMessage( HttpMethod.Post, path );
152+
req.Content = JsonContent.Create( emails );
153+
req.Headers.Add( IdempotencyKey, idempotencyKey );
154+
req.Headers.Add( "x-batch-validation", mode );
155+
156+
return Execute<EmailBatchResponse, EmailBatchResponse>( req, ( x ) => x, cancellationToken );
157+
}
158+
159+
119160
/// <inheritdoc />
120161
public Task<ResendResponse> EmailRescheduleAsync( Guid emailId, DateTimeOrHuman rescheduleFor, CancellationToken cancellationToken = default )
121162
{

tools/Resend.ApiServer/Controllers/EmailController.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,19 +62,26 @@ public EmailReceipt EmailRetrieve( [FromRoute] Guid id )
6262
/// <summary />
6363
[HttpPost]
6464
[Route( "emails/batch" )]
65-
public ListOf<ObjectId> EmailBatch(
65+
public EmailBatchResponse EmailBatch(
6666
[FromHeader( Name = "Idempotency-Key" )] string? idempotencyKey,
67+
[FromHeader( Name = "x-batch-validation" )] string? validationMode,
6768
[FromBody] List<EmailMessage> messages )
6869
{
6970
_logger.LogDebug( "EmailBatch" );
7071

7172
if ( idempotencyKey != null )
7273
_logger.LogDebug( "With {IdempotencyKey}", idempotencyKey );
7374

74-
var list = new ListOf<ObjectId>();
75-
list.Data = messages.Select( x => new ObjectId()
75+
if ( validationMode != null )
76+
_logger.LogDebug( "With {ValidationMode}", validationMode );
77+
78+
79+
/*
80+
*
81+
*/
82+
var list = new EmailBatchResponse();
83+
list.Data = messages.Select( x => new EmailBatchReceipt()
7684
{
77-
Object = "email",
7885
Id = Guid.NewGuid(),
7986
} ).ToList();
8087

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
using McMaster.Extensions.CommandLineUtils;
2+
using Spectre.Console;
3+
using System.Text.Json;
4+
5+
namespace Resend.Cli.Email;
6+
7+
/// <summary />
8+
[Command( "batch", Description = "Sends a batch of emails" )]
9+
public class EmailBatchCommand
10+
{
11+
private readonly IResend _resend;
12+
13+
14+
/// <summary />
15+
[Argument( 0, Description = "Input JSON file" )]
16+
[FileExists]
17+
public string? InputFile { get; set; }
18+
19+
/// <summary />
20+
[Option( "-k|--key", CommandOptionType.SingleValue, Description = "Idempotency key" )]
21+
public string? IdempotencyKey { get; set; }
22+
23+
/// <summary />
24+
[Option( "-m|--mode", CommandOptionType.SingleValue, Description = "Validation mode" )]
25+
public EmailBatchValidationMode ValidationMode { get; set; } = EmailBatchValidationMode.Strict;
26+
27+
/// <summary />
28+
[Option( "-j|--json", CommandOptionType.NoValue, Description = "Emit output as JSON array" )]
29+
public bool InJson { get; set; }
30+
31+
32+
/// <summary />
33+
public EmailBatchCommand( IResend resend )
34+
{
35+
_resend = resend;
36+
}
37+
38+
39+
/// <summary />
40+
public async Task<int> OnExecuteAsync()
41+
{
42+
/*
43+
*
44+
*/
45+
string json;
46+
47+
if ( Console.IsInputRedirected == true && this.InputFile == null )
48+
{
49+
json = await Console.In.ReadToEndAsync();
50+
}
51+
else
52+
{
53+
var ifile = this.InputFile ?? "emails.json";
54+
55+
if ( File.Exists( ifile ) == false )
56+
{
57+
var of = Console.ForegroundColor;
58+
Console.ForegroundColor = ConsoleColor.Red;
59+
Console.WriteLine( "The file '{0}' does not exist.", ifile );
60+
Console.ForegroundColor = of;
61+
62+
return 1;
63+
}
64+
65+
json = File.ReadAllText( ifile );
66+
}
67+
68+
List<EmailMessage> messages = JsonSerializer.Deserialize<List<EmailMessage>>( json )!;
69+
70+
71+
/*
72+
*
73+
*/
74+
ResendResponse<EmailBatchResponse> res;
75+
76+
if ( this.IdempotencyKey != null )
77+
res = await _resend.EmailBatchAsync( this.IdempotencyKey, messages, this.ValidationMode );
78+
else
79+
res = await _resend.EmailBatchAsync( messages, this.ValidationMode );
80+
81+
82+
/*
83+
*
84+
*/
85+
if ( this.InJson == true )
86+
{
87+
var jso = new JsonSerializerOptions() { WriteIndented = true };
88+
var ojson = JsonSerializer.Serialize( res.Content, jso );
89+
90+
Console.WriteLine( ojson );
91+
}
92+
else
93+
{
94+
if ( res.Content.Data.Count > 0 )
95+
{
96+
var table = new Table();
97+
table.Border = TableBorder.SimpleHeavy;
98+
table.AddColumn( "Id" );
99+
100+
foreach ( var d in res.Content.Data )
101+
{
102+
table.AddRow(
103+
new Markup( d.Id.ToString() )
104+
);
105+
}
106+
107+
AnsiConsole.Write( table );
108+
}
109+
110+
if ( res.Content.Errors != null )
111+
{
112+
var table = new Table();
113+
table.Border = TableBorder.SimpleHeavy;
114+
table.AddColumn( "Ix" );
115+
table.AddColumn( "Error" );
116+
117+
foreach ( var d in res.Content.Errors )
118+
{
119+
table.AddRow(
120+
new Markup( d.Index.ToString() ),
121+
new Markup( d.Message )
122+
);
123+
}
124+
125+
AnsiConsole.Write( table );
126+
}
127+
}
128+
129+
return 0;
130+
}
131+
}

tools/Resend.Cli/EmailCommand.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ namespace Resend.Cli;
44

55
/// <summary />
66
[Command( "email", Description = "Send emails" )]
7+
[Subcommand( typeof( Email.EmailBatchCommand ) )]
78
[Subcommand( typeof( Email.EmailCancelCommand ) )]
89
[Subcommand( typeof( Email.EmailRescheduleCommand ) )]
910
[Subcommand( typeof( Email.EmailRetrieveCommand ) )]

0 commit comments

Comments
 (0)