1+ using Azure ;
2+ using Azure . Communication . Email ;
13using Jordnaer . Extensions ;
24using Jordnaer . Features . Email ;
35using Jordnaer . Features . Metrics ;
46using MassTransit ;
57using Polly ;
68using Polly . CircuitBreaker ;
79using Polly . Retry ;
8- using SendGrid ;
9- using SendGrid . Helpers . Mail ;
10- using Response = SendGrid . Response ;
1110
1211namespace Jordnaer . Consumers ;
1312
1413public class SendEmailConsumer (
15- ILogger < SendMessageConsumer > logger ,
16- ISendGridClient sendGridClient )
14+ ILogger < SendEmailConsumer > logger ,
15+ EmailClient emailClient )
1716 : IConsumer < SendEmail >
1817{
19- private static readonly ResiliencePipeline < Response > Retry =
20- new ResiliencePipelineBuilder < Response > ( )
21- . AddRetry ( new RetryStrategyOptions < Response >
18+ private static readonly ResiliencePipeline < EmailSendOperation > Retry =
19+ new ResiliencePipelineBuilder < EmailSendOperation > ( )
20+ . AddRetry ( new RetryStrategyOptions < EmailSendOperation >
2221 {
23- ShouldHandle = new PredicateBuilder < Response > ( )
22+ ShouldHandle = new PredicateBuilder < EmailSendOperation > ( )
2423 . Handle < Exception > ( )
25- . HandleResult ( x => ! x . IsSuccessStatusCode ) ,
24+ . HandleResult ( x => x . HasValue is false )
25+ . HandleResult ( x => x . GetRawResponse ( ) . IsError ) ,
2626 BackoffType = DelayBackoffType . Exponential ,
2727 Delay = TimeSpan . FromSeconds ( 1 ) ,
2828 UseJitter = true ,
29- MaxDelay = TimeSpan . FromSeconds ( 30 ) ,
30- MaxRetryAttempts = 8 ,
31- Name = "SendGridRetry "
29+ MaxDelay = TimeSpan . FromMinutes ( 5 ) ,
30+ MaxRetryAttempts = 15 ,
31+ Name = "AzureEmailRetry "
3232 } )
33- . AddCircuitBreaker ( new CircuitBreakerStrategyOptions < Response >
33+ . AddCircuitBreaker ( new CircuitBreakerStrategyOptions < EmailSendOperation >
3434 {
3535 FailureRatio = 0.20 ,
3636 SamplingDuration = TimeSpan . FromSeconds ( 30 ) ,
@@ -39,62 +39,73 @@ public class SendEmailConsumer(
3939 } )
4040 . Build ( ) ;
4141
42- private static readonly TrackingSettings DefaultTrackingSettings = new ( )
43- {
44- ClickTracking = new ClickTracking { Enable = false } ,
45- Ganalytics = new Ganalytics { Enable = false } ,
46- OpenTracking = new OpenTracking { Enable = false } ,
47- SubscriptionTracking = new SubscriptionTracking { Enable = false }
48- } ;
49-
5042 public async Task Consume ( ConsumeContext < SendEmail > consumeContext )
5143 {
5244 logger . LogFunctionBegan ( ) ;
53-
5445 var message = consumeContext . Message ;
55- var email = new SendGridMessage
56- {
57- From = message . From ?? EmailConstants . ContactEmail , // Must be from a verified email
58- Subject = message . Subject ,
59- HtmlContent = message . HtmlContent
60- } ;
6146
62- if ( message . To ? . Count is > 0 )
63- {
64- email . AddTos ( message . To ) ;
65- }
47+ var emailMessage = new EmailMessage ( senderAddress : message . From ? . Email ?? EmailConstants . ContactEmail . Email ,
48+ content : new EmailContent ( message . Subject )
49+ {
50+ Html = message . HtmlContent
51+ } ,
52+ recipients : new EmailRecipients (
53+ to : message . To ? . Select ( ConvertToEmailAddress ) . ToList ( ) ,
54+ bcc : message . Bcc ? . Select ( ConvertToEmailAddress ) . ToList ( )
55+ ) )
56+ { UserEngagementTrackingDisabled = message . DisableUserEngagementTracking } ;
6657
58+ // Set reply-to if specified
6759 if ( message . ReplyTo is not null )
6860 {
69- email . ReplyTo = message . ReplyTo ;
61+ emailMessage . ReplyTo . Add ( ConvertToEmailAddress ( message . ReplyTo ) ) ;
7062 }
7163
72- if ( message . Bcc ? . Count is > 0 )
64+ try
7365 {
74- email . AddBccs ( message . Bcc ) ;
75- }
66+ var response = await Retry . ExecuteAsync (
67+ async token => await emailClient . SendAsync ( WaitUntil . Completed , emailMessage , token ) ,
68+ consumeContext . CancellationToken ) ;
7669
77- email . TrackingSettings = message . TrackingSettings ?? DefaultTrackingSettings ;
70+ var rawResponse = response . GetRawResponse ( ) ;
7871
79- var response = await Retry . ExecuteAsync (
80- async token => await sendGridClient . SendEmailAsync ( email , token ) ,
81- consumeContext . CancellationToken ) ;
72+ if ( rawResponse . IsError )
73+ {
74+ logger . LogError ( "Failed to send email to {Recipients}. " +
75+ "StatusCode: {StatusCode}. " +
76+ "Response: {Response}. " +
77+ "Email: {Email}" ,
78+ message . GetAllRecipients ( ) ,
79+ rawResponse . Status ,
80+ rawResponse . ReasonPhrase ,
81+ message ) ;
82+ }
83+ else
84+ {
85+ logger . LogInformation ( "Email(s) sent to: {Recipients}. MessageId: {MessageId}" ,
86+ message . GetAllRecipients ( ) , response . Id ) ;
8287
83- if ( ! response . IsSuccessStatusCode )
84- {
85- logger . LogError ( "Failed to send email to {@Recipient}. " +
86- "StatusCode: {StatusCode}. " +
87- "Response: {Response}. " +
88- "Email: {Email}" ,
89- message . To , response . StatusCode . ToString ( ) ,
90- await response . Body . ReadAsStringAsync ( ) , message ) ;
88+ JordnaerMetrics . EmailsSentCounter . Add ( 1 ) ;
89+ }
9190 }
92- else
91+ catch ( RequestFailedException exception )
9392 {
94- logger . LogInformation ( "Email sent to {@Recipient}. Subject: {Subject}" ,
95- message . To ? . Select ( x => x . Email ) , message . Subject ) ;
96-
97- JordnaerMetrics . EmailsSentCounter . Add ( 1 ) ;
93+ logger . LogError ( exception , "Failed to send email to {Recipients}. " +
94+ "ErrorCode: {ErrorCode}. " +
95+ "Message: {Message}. " +
96+ "Sender: {Email}" ,
97+ message . GetAllRecipients ( ) ,
98+ exception . ErrorCode ,
99+ exception . Message ,
100+ message . From ? . ToString ( ) ) ;
101+ throw ;
98102 }
99103 }
104+
105+ private static EmailAddress ConvertToEmailAddress ( EmailRecipient recipient )
106+ {
107+ return string . IsNullOrWhiteSpace ( recipient . DisplayName )
108+ ? new EmailAddress ( recipient . Email )
109+ : new EmailAddress ( recipient . Email , recipient . DisplayName ) ;
110+ }
100111}
0 commit comments