99import it .aboutbits .springboot .emailservice .lib .jpa .EmailRepository ;
1010import it .aboutbits .springboot .emailservice .lib .model .Email ;
1111import it .aboutbits .springboot .emailservice .lib .model .EmailAttachment ;
12+ import it .aboutbits .springboot .emailservice .lib .model .EmailContent ;
1213import jakarta .mail .MessagingException ;
1314import jakarta .validation .Valid ;
1415import lombok .extern .slf4j .Slf4j ;
1516import org .jspecify .annotations .NullMarked ;
1617import org .jspecify .annotations .Nullable ;
1718import org .springframework .core .io .ByteArrayResource ;
18- import org .springframework .mail .MailException ;
1919import org .springframework .mail .javamail .JavaMailSender ;
2020import org .springframework .mail .javamail .MimeMessageHelper ;
21+ import org .springframework .transaction .annotation .Transactional ;
2122import org .springframework .validation .annotation .Validated ;
2223
2324import java .io .IOException ;
25+ import java .time .Duration ;
2426import java .time .OffsetDateTime ;
2527import java .util .HashSet ;
2628import java .util .List ;
29+ import java .util .Optional ;
2730import java .util .Set ;
2831
2932
@@ -35,17 +38,23 @@ public class ManageEmail {
3538 private final JavaMailSender mailSender ;
3639 private final AttachmentDataSource attachmentDataSource ;
3740 private final EmailMapper emailMapper ;
41+ private final int maxAttempts ;
42+ private final Duration schedulerInterval ;
3843
3944 public ManageEmail (
4045 EmailRepository emailRepository ,
4146 JavaMailSender mailSender ,
4247 AttachmentDataSource attachmentDataSource ,
43- final EmailMapper emailMapper
48+ EmailMapper emailMapper ,
49+ int maxAttempts ,
50+ Duration schedulerInterval
4451 ) {
4552 this .emailRepository = emailRepository ;
4653 this .mailSender = mailSender ;
4754 this .attachmentDataSource = attachmentDataSource ;
4855 this .emailMapper = emailMapper ;
56+ this .maxAttempts = maxAttempts ;
57+ this .schedulerInterval = schedulerInterval ;
4958 }
5059
5160 public EmailDto schedule (@ Valid EmailParameter parameter ) throws EmailException {
@@ -69,32 +78,62 @@ public EmailDto sendOrFail(@Valid EmailParameter parameter) throws EmailExceptio
6978 throw new EmailException (e );
7079 }
7180
72- var savedEmail = send (email );
81+ email .setState (EmailState .SENDING );
82+ email .setExecutionStartTime (OffsetDateTime .now ());
83+ email .setExecutionEndTime (null );
84+ email .setErrorMessage (null );
85+ email .incrementAttempts ();
86+ emailRepository .save (email );
7387
74- if (savedEmail .hasFailed ()) {
88+ try {
89+ sendMail (email );
90+ email .setState (EmailState .SENT );
91+ email .setExecutionEndTime (OffsetDateTime .now ());
92+ } catch (MessagingException | AttachmentException | IOException | RuntimeException e ) {
93+ log .error ("Failed to send email: {}" , email .getId (), e );
94+ email .setState (EmailState .ERROR );
95+ email .setExecutionEndTime (OffsetDateTime .now ());
96+ email .setErrorMessage (e .getMessage ());
97+ }
98+
99+ var savedEmail = emailRepository .save (email );
100+
101+ if (savedEmail .getState () == EmailState .ERROR ) {
75102 throw new EmailException ("Failed to send email [id=%s, providerMessage=%s]"
76103 .formatted (savedEmail .getId (), savedEmail .getErrorMessage ()));
77104 }
78105
79106 return emailMapper .toDto (savedEmail );
80107 }
81108
82- Email send (Email email ) {
83- if (EmailState .SENT .equals (email .getState ())) {
84- return email ;
85- }
109+ // Atomically transitions the row from PENDING/stale-SENDING into SENDING
110+ @ Transactional
111+ Optional <Email > tryClaimForSend (long id , OffsetDateTime staleSendingBefore ) {
112+ var claimed = emailRepository .claimForSend (id , OffsetDateTime .now (), staleSendingBefore );
113+ return claimed == 0 ? Optional .empty () : emailRepository .findById (id );
114+ }
86115
116+ // Actually try sending the claimed email
117+ Email completeClaimedSend (Email email ) {
87118 try {
88119 sendMail (email );
89120 email .setState (EmailState .SENT );
90- email .setErrorMessage ( "" );
91- email .setSentAt ( OffsetDateTime . now () );
92- } catch (MailException | MessagingException | AttachmentException | IOException e ) {
93- log . error ( "Failed to send email: " + email . getId (), e );
121+ email .setExecutionEndTime ( OffsetDateTime . now () );
122+ email .setErrorMessage ( null );
123+ } catch (Exception e ) {
124+ email . setExecutionEndTime ( OffsetDateTime . now () );
94125 email .setErrorMessage (e .getMessage ());
95- email .setState (EmailState .ERROR );
126+ if (email .getAttempts () > maxAttempts ) {
127+ log .error ("Failed to send email: {}" , email .getId (), e );
128+ email .setState (EmailState .ERROR );
129+ } else {
130+ log .warn ("Failed to send email: {}; Will be tried again" , email .getId (), e );
131+ email .setState (EmailState .PENDING );
132+ email .setScheduledAt (
133+ OffsetDateTime .now ().plus (schedulerInterval .multipliedBy (email .getAttempts () * 2L ))
134+ );
135+ }
96136 }
97-
98137 return emailRepository .save (email );
99138 }
100139
@@ -106,49 +145,17 @@ void cleanupAttachments(final Email email) throws AttachmentException {
106145 emailRepository .save (email );
107146 }
108147
109- private Email fromParameter (EmailParameter parameter ) throws AttachmentException {
110- var emailData = parameter .email ();
111-
112- final var email = new Email ();
113- email .setState (EmailState .PENDING );
114- email .setScheduledAt (parameter .scheduledAt ());
115- email .setSubject (emailData .subject ());
116- email .setTextBody (emailData .textBody ());
117- email .setHtmlBody (emailData .htmlBody ());
118- email .setRecipients (emailData .recipients ());
119- email .setFromAddress (emailData .fromAddress ());
120- email .setFromName (emailData .fromName ());
121- email .setReplyToAddress (emailData .replyToAddress ());
122- email .setReplyToName (emailData .replyToName ());
123-
124- var attachments = new HashSet <EmailAttachment >();
125- for (var attachment : parameter .email ().attachments ()) {
126- var reference = attachmentDataSource .storeAttachmentPayload (attachment .payload ());
127-
128- var emailAttachment = new EmailAttachment ();
129- emailAttachment .setEmail (email );
130- emailAttachment .setContentType (attachment .contentType ());
131- emailAttachment .setFileName (attachment .fileName ());
132- emailAttachment .setFileReference (reference );
133-
134- attachments .add (emailAttachment );
135- }
136-
137- email .setAttachments (attachments );
138-
139- return email ;
140- }
141-
142148 private void sendMail (Email email ) throws MessagingException , IOException , AttachmentException {
149+ var content = email .getContent ();
143150 sendMail (
144- email . getFromAddress (),
145- email . getFromName (),
146- email . getReplyToAddress (),
147- email . getReplyToName (),
148- email . getRecipients (),
149- email . getSubject (),
150- email . getHtmlBody (),
151- email . getTextBody (),
151+ content . fromAddress (),
152+ content . fromName (),
153+ content . replyToAddress (),
154+ content . replyToName (),
155+ content . recipients (),
156+ content . subject (),
157+ content . htmlBody (),
158+ content . textBody (),
152159 email .getAttachments ()
153160 );
154161 }
@@ -197,4 +204,39 @@ private void sendMail(
197204
198205 mailSender .send (message );
199206 }
207+
208+ private Email fromParameter (EmailParameter parameter ) throws AttachmentException {
209+ var emailData = parameter .email ();
210+
211+ final var email = new Email ();
212+ email .setState (EmailState .PENDING );
213+ email .setScheduledAt (parameter .scheduledAt ());
214+ email .setContent (new EmailContent (
215+ emailData .subject (),
216+ emailData .fromAddress (),
217+ emailData .fromName (),
218+ emailData .replyToAddress (),
219+ emailData .replyToName (),
220+ emailData .recipients (),
221+ emailData .textBody (),
222+ emailData .htmlBody ()
223+ ));
224+
225+ var attachments = new HashSet <EmailAttachment >();
226+ for (var attachment : parameter .email ().attachments ()) {
227+ var reference = attachmentDataSource .storeAttachmentPayload (attachment .payload ());
228+
229+ var emailAttachment = new EmailAttachment ();
230+ emailAttachment .setEmail (email );
231+ emailAttachment .setContentType (attachment .contentType ());
232+ emailAttachment .setFileName (attachment .fileName ());
233+ emailAttachment .setFileReference (reference );
234+
235+ attachments .add (emailAttachment );
236+ }
237+
238+ email .setAttachments (attachments );
239+
240+ return email ;
241+ }
200242}
0 commit comments