Skip to content

Commit 597670c

Browse files
Merge branch 'main' into 69-add-emaillistasync-method
2 parents 5863e8e + 8ee0029 commit 597670c

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
@@ -73,7 +73,7 @@ public interface IResend
7373

7474

7575
/// <summary>
76-
/// Send a batch of emails.
76+
/// Send a batch of emails, if and only if all messages are valid.
7777
/// </summary>
7878
/// <param name="emails">
7979
/// List of emails.
@@ -89,8 +89,9 @@ public interface IResend
8989

9090

9191
/// <summary>
92-
/// Send a batch of emails using idempotency key, such that retries do not yield
93-
/// duplicate submissions.
92+
/// Send a batch of emails using idempotency key, such that retries do not yield
93+
/// duplicate submissions. Emails are only send if and only if all messages are
94+
/// valid.
9495
/// </summary>
9596
/// <param name="idempotencyKey">
9697
/// Idempotency key.
@@ -108,6 +109,47 @@ public interface IResend
108109
Task<ResendResponse<List<Guid>>> EmailBatchAsync( string idempotencyKey, IEnumerable<EmailMessage> emails, CancellationToken cancellationToken = default );
109110

110111

112+
/// <summary>
113+
/// Send a batch of emails.
114+
/// </summary>
115+
/// <param name="emails">
116+
/// List of emails.
117+
/// </param>
118+
/// <param name="validationMode">
119+
/// Validation mode.
120+
/// </param>
121+
/// <param name="cancellationToken">
122+
/// Cancellation token.
123+
/// </param>
124+
/// <returns>
125+
/// List of email identifiers.
126+
/// </returns>
127+
/// <see href="https://resend.com/docs/api-reference/emails/send-batch-emails"/>
128+
Task<ResendResponse<EmailBatchResponse>> EmailBatchAsync( IEnumerable<EmailMessage> emails, EmailBatchValidationMode validationMode, CancellationToken cancellationToken = default );
129+
130+
131+
/// <summary>
132+
/// Send a batch of emails.
133+
/// </summary>
134+
/// <param name="idempotencyKey">
135+
/// Idempotency key.
136+
/// </param>
137+
/// <param name="emails">
138+
/// List of emails.
139+
/// </param>
140+
/// <param name="validationMode">
141+
/// Validation mode.
142+
/// </param>
143+
/// <param name="cancellationToken">
144+
/// Cancellation token.
145+
/// </param>
146+
/// <returns>
147+
/// List of email identifiers.
148+
/// </returns>
149+
/// <see href="https://resend.com/docs/api-reference/emails/send-batch-emails"/>
150+
Task<ResendResponse<EmailBatchResponse>> EmailBatchAsync( string idempotencyKey, IEnumerable<EmailMessage> emails, EmailBatchValidationMode validationMode, CancellationToken cancellationToken = default );
151+
152+
111153
/// <summary>
112154
/// Reschedule an email.
113155
/// </summary>

src/Resend/ResendClient.cs

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

132133
return Execute<ListOf<ObjectId>, List<Guid>>( req, ( x ) => x.Data.Select( y => y.Id ).ToList(), cancellationToken );
133134
}
@@ -140,11 +141,51 @@ public ResendClient( IOptionsSnapshot<ResendClientOptions> options, HttpClient h
140141
var req = new HttpRequestMessage( HttpMethod.Post, path );
141142
req.Content = JsonContent.Create( emails );
142143
req.Headers.Add( IdempotencyKey, idempotencyKey );
144+
req.Headers.Add( "x-batch-validation", "strict" );
143145

144146
return Execute<ListOf<ObjectId>, List<Guid>>( req, ( x ) => x.Data.Select( y => y.Id ).ToList(), cancellationToken );
145147
}
146148

147149

150+
/// <inheritdoc />
151+
public Task<ResendResponse<EmailBatchResponse>> EmailBatchAsync( IEnumerable<EmailMessage> emails, EmailBatchValidationMode validationMode, CancellationToken cancellationToken = default )
152+
{
153+
var mode = validationMode switch
154+
{
155+
EmailBatchValidationMode.Strict => "strict",
156+
EmailBatchValidationMode.Permissive => "permissive",
157+
_ => throw new NotImplementedException( $"Validation mode {validationMode} is not implemented" )
158+
};
159+
160+
var path = $"/emails/batch";
161+
var req = new HttpRequestMessage( HttpMethod.Post, path );
162+
req.Content = JsonContent.Create( emails );
163+
req.Headers.Add( "x-batch-validation", mode );
164+
165+
return Execute<EmailBatchResponse, EmailBatchResponse>( req, ( x ) => x, cancellationToken );
166+
}
167+
168+
169+
/// <inheritdoc />
170+
public Task<ResendResponse<EmailBatchResponse>> EmailBatchAsync( string idempotencyKey, IEnumerable<EmailMessage> emails, EmailBatchValidationMode validationMode, CancellationToken cancellationToken = default )
171+
{
172+
var mode = validationMode switch
173+
{
174+
EmailBatchValidationMode.Strict => "strict",
175+
EmailBatchValidationMode.Permissive => "permissive",
176+
_ => throw new NotImplementedException( $"Validation mode {validationMode} is not implemented" )
177+
};
178+
179+
var path = $"/emails/batch";
180+
var req = new HttpRequestMessage( HttpMethod.Post, path );
181+
req.Content = JsonContent.Create( emails );
182+
req.Headers.Add( IdempotencyKey, idempotencyKey );
183+
req.Headers.Add( "x-batch-validation", mode );
184+
185+
return Execute<EmailBatchResponse, EmailBatchResponse>( req, ( x ) => x, cancellationToken );
186+
}
187+
188+
148189
/// <inheritdoc />
149190
public Task<ResendResponse> EmailRescheduleAsync( Guid emailId, DateTimeOrHuman rescheduleFor, CancellationToken cancellationToken = default )
150191
{

tools/Resend.ApiServer/Controllers/EmailController.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,19 +105,26 @@ public PaginatedResult<EmailReceipt> EmailList(
105105
/// <summary />
106106
[HttpPost]
107107
[Route( "emails/batch" )]
108-
public ListOf<ObjectId> EmailBatch(
108+
public EmailBatchResponse EmailBatch(
109109
[FromHeader( Name = "Idempotency-Key" )] string? idempotencyKey,
110+
[FromHeader( Name = "x-batch-validation" )] string? validationMode,
110111
[FromBody] List<EmailMessage> messages )
111112
{
112113
_logger.LogDebug( "EmailBatch" );
113114

114115
if ( idempotencyKey != null )
115116
_logger.LogDebug( "With {IdempotencyKey}", idempotencyKey );
116117

117-
var list = new ListOf<ObjectId>();
118-
list.Data = messages.Select( x => new ObjectId()
118+
if ( validationMode != null )
119+
_logger.LogDebug( "With {ValidationMode}", validationMode );
120+
121+
122+
/*
123+
*
124+
*/
125+
var list = new EmailBatchResponse();
126+
list.Data = messages.Select( x => new EmailBatchReceipt()
119127
{
120-
Object = "email",
121128
Id = Guid.NewGuid(),
122129
} ).ToList();
123130

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.EmailListCommand ) )]
910
[Subcommand( typeof( Email.EmailRescheduleCommand ) )]

0 commit comments

Comments
 (0)