Subversion Repositories SmartDukaan

Rev

Rev 36399 | Rev 37473 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
35956 amit 1
package com.spice.profitmandi.service.mail;
2
 
3
import com.spice.profitmandi.common.util.Utils;
4
import com.spice.profitmandi.dao.entity.mail.MailOutbox;
5
import com.spice.profitmandi.dao.entity.mail.MailOutboxAttachment;
6
import com.spice.profitmandi.dao.repository.mail.MailOutboxRepository;
7
import org.apache.logging.log4j.LogManager;
8
import org.apache.logging.log4j.Logger;
9
import org.springframework.beans.factory.annotation.Autowired;
10
import org.springframework.beans.factory.annotation.Qualifier;
35960 amit 11
import org.springframework.beans.factory.annotation.Value;
37361 amit 12
import org.springframework.context.annotation.Lazy;
35956 amit 13
import org.springframework.core.io.ByteArrayResource;
14
import org.springframework.core.io.InputStreamSource;
15
import org.springframework.mail.javamail.JavaMailSender;
16
import org.springframework.mail.javamail.MimeMessageHelper;
17
import org.springframework.stereotype.Service;
18
import org.springframework.transaction.annotation.Propagation;
19
import org.springframework.transaction.annotation.Transactional;
20
 
21
import javax.mail.internet.InternetAddress;
22
import javax.mail.internet.MimeMessage;
23
import java.io.File;
24
import java.io.IOException;
25
import java.io.InputStream;
26
import java.nio.file.Files;
27
import java.time.LocalDateTime;
37361 amit 28
import java.util.ArrayList;
35956 amit 29
import java.util.List;
30
 
31
@Service
32
public class MailOutboxService {
33
 
34
    private static final Logger LOGGER = LogManager.getLogger(MailOutboxService.class);
35
 
36
    public static final String SENDER_SENDGRID = "SENDGRID";
37
    public static final String SENDER_GOOGLE = "GOOGLE";
36245 amit 38
    public static final String SENDER_RELAY = "RELAY";
35956 amit 39
 
35960 amit 40
    @Value("${prod}")
41
    private boolean prod;
42
 
43
    /**
44
     * In dev/staging, mails are sent to this address instead of actual recipients.
45
     * Set to empty to throw an exception (forces developer to set their email).
46
     */
47
    @Value("${mail.outbox.dev.recipient:}")
48
    private String devRecipient;
49
 
35956 amit 50
    @Autowired
51
    private MailOutboxRepository mailOutboxRepository;
52
 
37361 amit 53
    /**
54
     * Self-reference so each mail is sent in its own transaction. Calling the
55
     * @Transactional methods directly would bypass the proxy and put the whole
56
     * batch back into a single transaction.
57
     */
35956 amit 58
    @Autowired
37361 amit 59
    @Lazy
60
    private MailOutboxService self;
61
 
62
    @Autowired
35956 amit 63
    @Qualifier("googleMailSender")
64
    private JavaMailSender googleMailSender;
65
 
36245 amit 66
    @Autowired
67
    @Qualifier("gmailRelaySender")
68
    private JavaMailSender gmailRelaySender;
69
 
35956 amit 70
    // ---- Default sender (SendGrid) convenience methods ----
71
 
72
    public void queueMail(String[] emailTo, String[] cc, String subject, String body, String source) {
73
        queueMail(emailTo, cc, null, subject, body, false, source, SENDER_SENDGRID, (AttachmentData[]) null);
74
    }
75
 
76
    public void queueMail(String emailTo, String[] cc, String subject, String body, String source) {
77
        queueMail(new String[]{emailTo}, cc, null, subject, body, false, source, SENDER_SENDGRID, (AttachmentData[]) null);
78
    }
79
 
80
    public void queueMail(String[] emailTo, String[] cc, String subject, String body, boolean html, String source) {
81
        queueMail(emailTo, cc, null, subject, body, html, source, SENDER_SENDGRID, (AttachmentData[]) null);
82
    }
83
 
84
    public void queueMail(String[] emailTo, String[] cc, String[] bcc, String subject, String body, boolean html, String source) {
85
        queueMail(emailTo, cc, bcc, subject, body, html, source, SENDER_SENDGRID, (AttachmentData[]) null);
86
    }
87
 
88
    public void queueMailWithAttachments(String[] emailTo, String[] cc, String subject, String body, String source, Utils.Attachment... attachments) {
89
        queueMailWithAttachments(emailTo, cc, null, subject, body, false, source, SENDER_SENDGRID, attachments);
90
    }
91
 
92
    public void queueMailWithAttachments(String[] emailTo, String[] cc, String[] bcc, String subject, String body, boolean html, String source, Utils.Attachment... attachments) {
93
        queueMailWithAttachments(emailTo, cc, bcc, subject, body, html, source, SENDER_SENDGRID, attachments);
94
    }
95
 
96
    // ---- Google sender convenience methods ----
97
 
98
    public void queueMailViaGoogle(String[] emailTo, String[] cc, String subject, String body, String source) {
99
        queueMail(emailTo, cc, null, subject, body, false, source, SENDER_GOOGLE, (AttachmentData[]) null);
100
    }
101
 
102
    public void queueMailViaGoogle(String emailTo, String[] cc, String subject, String body, String source) {
103
        queueMail(new String[]{emailTo}, cc, null, subject, body, false, source, SENDER_GOOGLE, (AttachmentData[]) null);
104
    }
105
 
106
    public void queueMailViaGoogle(String[] emailTo, String[] cc, String subject, String body, boolean html, String source) {
107
        queueMail(emailTo, cc, null, subject, body, html, source, SENDER_GOOGLE, (AttachmentData[]) null);
108
    }
109
 
110
    public void queueMailViaGoogle(String[] emailTo, String[] cc, String[] bcc, String subject, String body, boolean html, String source) {
111
        queueMail(emailTo, cc, bcc, subject, body, html, source, SENDER_GOOGLE, (AttachmentData[]) null);
112
    }
113
 
114
    public void queueMailWithAttachmentsViaGoogle(String[] emailTo, String[] cc, String subject, String body, String source, Utils.Attachment... attachments) {
115
        queueMailWithAttachments(emailTo, cc, null, subject, body, false, source, SENDER_GOOGLE, attachments);
116
    }
117
 
118
    public void queueMailWithAttachmentsViaGoogle(String[] emailTo, String[] cc, String[] bcc, String subject, String body, boolean html, String source, Utils.Attachment... attachments) {
119
        queueMailWithAttachments(emailTo, cc, bcc, subject, body, html, source, SENDER_GOOGLE, attachments);
120
    }
121
 
36245 amit 122
    // ---- Google Workspace Relay sender convenience methods ----
123
 
124
    public void queueMailViaRelay(String[] emailTo, String[] cc, String subject, String body, String source) {
125
        queueMail(emailTo, cc, null, subject, body, false, source, SENDER_RELAY, (AttachmentData[]) null);
126
    }
127
 
128
    public void queueMailViaRelay(String emailTo, String[] cc, String subject, String body, String source) {
129
        queueMail(new String[]{emailTo}, cc, null, subject, body, false, source, SENDER_RELAY, (AttachmentData[]) null);
130
    }
131
 
132
    public void queueMailViaRelay(String[] emailTo, String[] cc, String subject, String body, boolean html, String source) {
133
        queueMail(emailTo, cc, null, subject, body, html, source, SENDER_RELAY, (AttachmentData[]) null);
134
    }
135
 
136
    public void queueMailViaRelay(String[] emailTo, String[] cc, String[] bcc, String subject, String body, boolean html, String source) {
137
        queueMail(emailTo, cc, bcc, subject, body, html, source, SENDER_RELAY, (AttachmentData[]) null);
138
    }
139
 
140
    public void queueMailWithAttachmentsViaRelay(String[] emailTo, String[] cc, String subject, String body, String source, Utils.Attachment... attachments) {
141
        queueMailWithAttachments(emailTo, cc, null, subject, body, false, source, SENDER_RELAY, attachments);
142
    }
143
 
144
    public void queueMailWithAttachmentsViaRelay(String[] emailTo, String[] cc, String[] bcc, String subject, String body, boolean html, String source, Utils.Attachment... attachments) {
145
        queueMailWithAttachments(emailTo, cc, bcc, subject, body, html, source, SENDER_RELAY, attachments);
146
    }
147
 
35956 amit 148
    // ---- Internal methods ----
149
 
150
    private void queueMailWithAttachments(String[] emailTo, String[] cc, String[] bcc, String subject, String body, boolean html, String source, String senderType, Utils.Attachment... attachments) {
151
        AttachmentData[] attachmentDataArray = null;
152
        if (attachments != null && attachments.length > 0) {
153
            attachmentDataArray = new AttachmentData[attachments.length];
154
            for (int i = 0; i < attachments.length; i++) {
155
                try {
156
                    byte[] data = readInputStreamSource(attachments[i].getInputStreamSource());
157
                    attachmentDataArray[i] = new AttachmentData(attachments[i].getFileName(), data, null);
158
                } catch (IOException e) {
159
                    LOGGER.error("Failed to read attachment: {}", attachments[i].getFileName(), e);
160
                    attachmentDataArray[i] = new AttachmentData(attachments[i].getFileName(), new byte[0], null);
161
                }
162
            }
163
        }
164
        queueMail(emailTo, cc, bcc, subject, body, html, source, senderType, attachmentDataArray);
165
    }
166
 
167
    public void queueMailWithFiles(String[] emailTo, String[] cc, String[] bcc, String subject, String body, String source, File... files) {
168
        AttachmentData[] attachmentDataArray = null;
169
        if (files != null && files.length > 0) {
170
            attachmentDataArray = new AttachmentData[files.length];
171
            for (int i = 0; i < files.length; i++) {
172
                try {
173
                    byte[] data = Files.readAllBytes(files[i].toPath());
174
                    String contentType = Files.probeContentType(files[i].toPath());
175
                    attachmentDataArray[i] = new AttachmentData(files[i].getName(), data, contentType);
176
                } catch (IOException e) {
177
                    LOGGER.error("Failed to read file attachment: {}", files[i].getName(), e);
178
                    attachmentDataArray[i] = new AttachmentData(files[i].getName(), new byte[0], null);
179
                }
180
            }
181
        }
182
        queueMail(emailTo, cc, bcc, subject, body, false, source, SENDER_SENDGRID, attachmentDataArray);
183
    }
184
 
185
    /**
186
     * Core method: persists mail + attachments in current transaction, triggers async send after commit.
187
     */
188
    private void queueMail(String[] emailTo, String[] cc, String[] bcc, String subject, String body, boolean html, String source, String senderType, AttachmentData... attachments) {
189
        MailOutbox mail = new MailOutbox();
190
        mail.setEmailTo(String.join(",", emailTo));
191
        if (cc != null && cc.length > 0) {
192
            mail.setEmailCc(String.join(",", cc));
193
        }
194
        if (bcc != null && bcc.length > 0) {
195
            mail.setEmailBcc(String.join(",", bcc));
196
        }
197
        mail.setSubject(subject != null ? subject : "");
198
        mail.setBody(body != null ? body : "");
199
        mail.setHtml(html);
200
        mail.setStatus("PENDING");
201
        mail.setRetryCount(0);
202
        mail.setCreatedAt(LocalDateTime.now());
203
        mail.setSource(source);
204
        mail.setSenderType(senderType);
205
 
206
        mailOutboxRepository.persist(mail);
207
 
208
        if (attachments != null) {
209
            for (AttachmentData att : attachments) {
210
                if (att != null && att.data.length > 0) {
211
                    MailOutboxAttachment attachment = new MailOutboxAttachment();
212
                    attachment.setMailOutboxId(mail.getId());
213
                    attachment.setFileName(att.fileName);
214
                    attachment.setFileData(att.data);
215
                    attachment.setContentType(att.contentType);
216
                    mailOutboxRepository.persistAttachment(attachment);
217
                }
218
            }
219
        }
220
 
221
    }
222
 
223
    public void processPendingMails() {
37361 amit 224
        List<Long> pendingIds = self.selectPendingIds();
225
        LOGGER.info("Processing {} pending mails", pendingIds.size());
226
        for (Long mailId : pendingIds) {
227
            try {
228
                self.sendOne(mailId);
229
            } catch (Exception e) {
230
                LOGGER.error("Unexpected error processing mail id={}", mailId, e);
231
            }
232
        }
233
    }
234
 
235
    @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true)
236
    public List<Long> selectPendingIds() {
35956 amit 237
        List<MailOutbox> pendingMails = mailOutboxRepository.selectPending();
37361 amit 238
        List<Long> ids = new ArrayList<>(pendingMails.size());
35956 amit 239
        for (MailOutbox mail : pendingMails) {
37361 amit 240
            ids.add(mail.getId());
35956 amit 241
        }
37361 amit 242
        return ids;
35956 amit 243
    }
244
 
37361 amit 245
    /**
246
     * One transaction per mail so the outcome is committed as soon as it is known.
247
     * A crash mid-batch can then lose at most the status of the mail in flight,
248
     * instead of re-sending every mail already delivered in this cycle.
249
     */
35956 amit 250
    @Transactional(propagation = Propagation.REQUIRES_NEW)
37361 amit 251
    public void sendOne(long mailId) {
252
        MailOutbox mail = mailOutboxRepository.findById(mailId);
253
        if (mail == null) {
254
            return;
255
        }
256
        sendAndUpdateStatus(mail);
257
    }
258
 
259
    @Transactional(propagation = Propagation.REQUIRES_NEW)
35956 amit 260
    public void cleanupOldMails(int daysOld) {
261
        mailOutboxRepository.deleteOldSentMails(daysOld);
262
    }
263
 
36250 amit 264
    @Transactional(propagation = Propagation.REQUIRES_NEW)
265
    public void cleanupOldMails(int daysOld, String senderType) {
266
        mailOutboxRepository.deleteOldSentMailsBySenderType(daysOld, senderType);
267
    }
268
 
35956 amit 269
    private void sendAndUpdateStatus(MailOutbox mail) {
270
        try {
271
            List<MailOutboxAttachment> attachments = mailOutboxRepository.selectAttachmentsByMailId(mail.getId());
272
            sendSmtp(mail, attachments);
273
            mail.setStatus("SENT");
274
            mail.setSentAt(LocalDateTime.now());
275
            mail.setErrorMessage(null);
276
        } catch (Exception e) {
277
            LOGGER.error("Failed to send mail id={}, subject={}", mail.getId(), mail.getSubject(), e);
278
            mail.setStatus("FAILED");
279
            mail.setRetryCount(mail.getRetryCount() + 1);
280
            String errorMsg = e.getMessage();
281
            if (errorMsg != null && errorMsg.length() > 1000) {
282
                errorMsg = errorMsg.substring(0, 1000);
283
            }
284
            mail.setErrorMessage(errorMsg);
285
        }
286
    }
287
 
288
    private void sendSmtp(MailOutbox mail, List<MailOutboxAttachment> attachments) throws Exception {
35960 amit 289
        if (!prod) {
290
            if (devRecipient == null || devRecipient.trim().isEmpty()) {
291
                throw new IllegalStateException("Non-prod environment: set mail.outbox.dev.recipient in properties to your email before sending mails");
292
            }
293
            LOGGER.info("Non-prod: redirecting mail [subject={}] from [{}] to dev recipient [{}]", mail.getSubject(), mail.getEmailTo(), devRecipient);
294
        }
295
 
35956 amit 296
        JavaMailSender sender = resolveSender(mail.getSenderType());
297
        MimeMessage message = sender.createMimeMessage();
298
        boolean hasAttachments = attachments != null && !attachments.isEmpty();
299
        MimeMessageHelper helper = new MimeMessageHelper(message, hasAttachments);
300
 
35960 amit 301
        if (prod) {
302
            helper.setTo(mail.getEmailTo().split(","));
303
            if (mail.getEmailCc() != null && !mail.getEmailCc().isEmpty()) {
304
                helper.setCc(mail.getEmailCc().split(","));
305
            }
306
            if (mail.getEmailBcc() != null && !mail.getEmailBcc().isEmpty()) {
307
                helper.setBcc(mail.getEmailBcc().split(","));
308
            }
309
        } else {
310
            helper.setTo(devRecipient.trim());
35956 amit 311
        }
312
        helper.setSubject(mail.getSubject());
313
        helper.setText(mail.getBody(), mail.isHtml());
314
 
315
        String fromEmail = SENDER_GOOGLE.equals(mail.getSenderType()) ? "sdtech@smartdukaan.com" : "noreply@smartdukaan.com";
316
        String fromName = "SmartDukaan Care";
317
        helper.setFrom(new InternetAddress(fromEmail, fromName));
318
 
319
        if (hasAttachments) {
320
            for (MailOutboxAttachment att : attachments) {
321
                helper.addAttachment(att.getFileName(), new ByteArrayResource(att.getFileData()));
322
            }
323
        }
324
 
37361 amit 325
        // Stable per-row Message-ID: a retry then arrives as the same message rather
326
        // than a new one, so the receiving server can collapse it. Spring preserves an
327
        // explicitly set Message-ID across MimeMessage.saveChanges().
328
        message.setHeader("Message-ID", "<mailoutbox-" + mail.getId() + "@smartdukaan.com>");
329
 
35956 amit 330
        sender.send(message);
331
    }
332
 
333
    private JavaMailSender resolveSender(String senderType) {
334
        if (SENDER_GOOGLE.equals(senderType)) {
335
            return googleMailSender;
336
        }
36249 amit 337
        // SendGrid and Relay both route through Google Workspace Relay
338
        return gmailRelaySender;
35956 amit 339
    }
340
 
341
    private byte[] readInputStreamSource(InputStreamSource source) throws IOException {
342
        try (InputStream is = source.getInputStream()) {
343
            return readAllBytes(is);
344
        }
345
    }
346
 
347
    private byte[] readAllBytes(InputStream is) throws IOException {
348
        java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
349
        byte[] buffer = new byte[8192];
350
        int len;
351
        while ((len = is.read(buffer)) != -1) {
352
            baos.write(buffer, 0, len);
353
        }
354
        return baos.toByteArray();
355
    }
356
 
357
    public static class AttachmentData {
358
        final String fileName;
359
        final byte[] data;
360
        final String contentType;
361
 
362
        public AttachmentData(String fileName, byte[] data, String contentType) {
363
            this.fileName = fileName;
364
            this.data = data;
365
            this.contentType = contentType;
366
        }
367
    }
368
}