Subversion Repositories SmartDukaan

Rev

Rev 35960 | Rev 36067 | 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;
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.scheduling.annotation.Async;
18
import org.springframework.stereotype.Service;
19
import org.springframework.transaction.annotation.Propagation;
20
import org.springframework.transaction.annotation.Transactional;
21
import org.springframework.transaction.support.TransactionSynchronization;
22
import org.springframework.transaction.support.TransactionSynchronizationManager;
23
 
24
import javax.mail.internet.InternetAddress;
25
import javax.mail.internet.MimeMessage;
26
import java.io.File;
27
import java.io.IOException;
28
import java.io.InputStream;
29
import java.nio.file.Files;
30
import java.time.LocalDateTime;
31
import java.util.List;
32
 
33
@Service
34
public class MailOutboxService {
35
 
36
    private static final Logger LOGGER = LogManager.getLogger(MailOutboxService.class);
37
 
38
    public static final String SENDER_SENDGRID = "SENDGRID";
39
    public static final String SENDER_GOOGLE = "GOOGLE";
40
 
35960 amit 41
    @Value("${prod}")
42
    private boolean prod;
43
 
44
    /**
45
     * In dev/staging, mails are sent to this address instead of actual recipients.
46
     * Set to empty to throw an exception (forces developer to set their email).
47
     */
48
    @Value("${mail.outbox.dev.recipient:}")
49
    private String devRecipient;
50
 
35956 amit 51
    @Autowired
52
    private MailOutboxRepository mailOutboxRepository;
53
 
54
    @Autowired
35960 amit 55
    @Lazy
56
    private MailOutboxService self;
57
 
58
    @Autowired
35956 amit 59
    @Qualifier("mailSender")
60
    private JavaMailSender sendgridMailSender;
61
 
62
    @Autowired
63
    @Qualifier("googleMailSender")
64
    private JavaMailSender googleMailSender;
65
 
66
    // ---- Default sender (SendGrid) convenience methods ----
67
 
68
    public void queueMail(String[] emailTo, String[] cc, String subject, String body, String source) {
69
        queueMail(emailTo, cc, null, subject, body, false, source, SENDER_SENDGRID, (AttachmentData[]) null);
70
    }
71
 
72
    public void queueMail(String emailTo, String[] cc, String subject, String body, String source) {
73
        queueMail(new String[]{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, boolean html, String source) {
77
        queueMail(emailTo, cc, null, subject, body, html, source, SENDER_SENDGRID, (AttachmentData[]) null);
78
    }
79
 
80
    public void queueMail(String[] emailTo, String[] cc, String[] bcc, String subject, String body, boolean html, String source) {
81
        queueMail(emailTo, cc, bcc, subject, body, html, source, SENDER_SENDGRID, (AttachmentData[]) null);
82
    }
83
 
84
    public void queueMailWithAttachments(String[] emailTo, String[] cc, String subject, String body, String source, Utils.Attachment... attachments) {
85
        queueMailWithAttachments(emailTo, cc, null, subject, body, false, source, SENDER_SENDGRID, attachments);
86
    }
87
 
88
    public void queueMailWithAttachments(String[] emailTo, String[] cc, String[] bcc, String subject, String body, boolean html, String source, Utils.Attachment... attachments) {
89
        queueMailWithAttachments(emailTo, cc, bcc, subject, body, html, source, SENDER_SENDGRID, attachments);
90
    }
91
 
92
    // ---- Google sender convenience methods ----
93
 
94
    public void queueMailViaGoogle(String[] emailTo, String[] cc, String subject, String body, String source) {
95
        queueMail(emailTo, cc, null, subject, body, false, source, SENDER_GOOGLE, (AttachmentData[]) null);
96
    }
97
 
98
    public void queueMailViaGoogle(String emailTo, String[] cc, String subject, String body, String source) {
99
        queueMail(new String[]{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, boolean html, String source) {
103
        queueMail(emailTo, cc, null, subject, body, html, source, SENDER_GOOGLE, (AttachmentData[]) null);
104
    }
105
 
106
    public void queueMailViaGoogle(String[] emailTo, String[] cc, String[] bcc, String subject, String body, boolean html, String source) {
107
        queueMail(emailTo, cc, bcc, subject, body, html, source, SENDER_GOOGLE, (AttachmentData[]) null);
108
    }
109
 
110
    public void queueMailWithAttachmentsViaGoogle(String[] emailTo, String[] cc, String subject, String body, String source, Utils.Attachment... attachments) {
111
        queueMailWithAttachments(emailTo, cc, null, subject, body, false, source, SENDER_GOOGLE, attachments);
112
    }
113
 
114
    public void queueMailWithAttachmentsViaGoogle(String[] emailTo, String[] cc, String[] bcc, String subject, String body, boolean html, String source, Utils.Attachment... attachments) {
115
        queueMailWithAttachments(emailTo, cc, bcc, subject, body, html, source, SENDER_GOOGLE, attachments);
116
    }
117
 
118
    // ---- Internal methods ----
119
 
120
    private void queueMailWithAttachments(String[] emailTo, String[] cc, String[] bcc, String subject, String body, boolean html, String source, String senderType, Utils.Attachment... attachments) {
121
        AttachmentData[] attachmentDataArray = null;
122
        if (attachments != null && attachments.length > 0) {
123
            attachmentDataArray = new AttachmentData[attachments.length];
124
            for (int i = 0; i < attachments.length; i++) {
125
                try {
126
                    byte[] data = readInputStreamSource(attachments[i].getInputStreamSource());
127
                    attachmentDataArray[i] = new AttachmentData(attachments[i].getFileName(), data, null);
128
                } catch (IOException e) {
129
                    LOGGER.error("Failed to read attachment: {}", attachments[i].getFileName(), e);
130
                    attachmentDataArray[i] = new AttachmentData(attachments[i].getFileName(), new byte[0], null);
131
                }
132
            }
133
        }
134
        queueMail(emailTo, cc, bcc, subject, body, html, source, senderType, attachmentDataArray);
135
    }
136
 
137
    public void queueMailWithFiles(String[] emailTo, String[] cc, String[] bcc, String subject, String body, String source, File... files) {
138
        AttachmentData[] attachmentDataArray = null;
139
        if (files != null && files.length > 0) {
140
            attachmentDataArray = new AttachmentData[files.length];
141
            for (int i = 0; i < files.length; i++) {
142
                try {
143
                    byte[] data = Files.readAllBytes(files[i].toPath());
144
                    String contentType = Files.probeContentType(files[i].toPath());
145
                    attachmentDataArray[i] = new AttachmentData(files[i].getName(), data, contentType);
146
                } catch (IOException e) {
147
                    LOGGER.error("Failed to read file attachment: {}", files[i].getName(), e);
148
                    attachmentDataArray[i] = new AttachmentData(files[i].getName(), new byte[0], null);
149
                }
150
            }
151
        }
152
        queueMail(emailTo, cc, bcc, subject, body, false, source, SENDER_SENDGRID, attachmentDataArray);
153
    }
154
 
155
    /**
156
     * Core method: persists mail + attachments in current transaction, triggers async send after commit.
157
     */
158
    private void queueMail(String[] emailTo, String[] cc, String[] bcc, String subject, String body, boolean html, String source, String senderType, AttachmentData... attachments) {
159
        MailOutbox mail = new MailOutbox();
160
        mail.setEmailTo(String.join(",", emailTo));
161
        if (cc != null && cc.length > 0) {
162
            mail.setEmailCc(String.join(",", cc));
163
        }
164
        if (bcc != null && bcc.length > 0) {
165
            mail.setEmailBcc(String.join(",", bcc));
166
        }
167
        mail.setSubject(subject != null ? subject : "");
168
        mail.setBody(body != null ? body : "");
169
        mail.setHtml(html);
170
        mail.setStatus("PENDING");
171
        mail.setRetryCount(0);
172
        mail.setCreatedAt(LocalDateTime.now());
173
        mail.setSource(source);
174
        mail.setSenderType(senderType);
175
 
176
        mailOutboxRepository.persist(mail);
177
 
178
        if (attachments != null) {
179
            for (AttachmentData att : attachments) {
180
                if (att != null && att.data.length > 0) {
181
                    MailOutboxAttachment attachment = new MailOutboxAttachment();
182
                    attachment.setMailOutboxId(mail.getId());
183
                    attachment.setFileName(att.fileName);
184
                    attachment.setFileData(att.data);
185
                    attachment.setContentType(att.contentType);
186
                    mailOutboxRepository.persistAttachment(attachment);
187
                }
188
            }
189
        }
190
 
191
        if (TransactionSynchronizationManager.isSynchronizationActive()) {
192
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
193
                @Override
194
                public void afterCommit() {
35960 amit 195
                    self.sendMailAsync(mail.getId());
35956 amit 196
                }
197
            });
198
        } else {
199
            // No active transaction — send immediately async
35960 amit 200
            self.sendMailAsync(mail.getId());
35956 amit 201
        }
202
    }
203
 
204
    @Async("mailOutboxExecutor")
205
    @Transactional(propagation = Propagation.REQUIRES_NEW)
206
    public void sendMailAsync(long mailOutboxId) {
207
        try {
36059 amit 208
            MailOutbox mail = mailOutboxRepository.findById(mailOutboxId);
209
            if (mail == null) {
210
                LOGGER.warn("Mail not found for id={}", mailOutboxId);
35956 amit 211
                return;
212
            }
36059 amit 213
            if (!"PENDING".equals(mail.getStatus())) {
214
                return;
215
            }
35956 amit 216
            sendAndUpdateStatus(mail);
217
        } catch (Exception e) {
218
            LOGGER.error("Async mail send failed for mailOutboxId={}", mailOutboxId, e);
219
        }
220
    }
221
 
222
    @Transactional(propagation = Propagation.REQUIRES_NEW)
223
    public void processPendingMails() {
224
        List<MailOutbox> pendingMails = mailOutboxRepository.selectPending();
225
        LOGGER.info("Processing {} pending mails", pendingMails.size());
226
        for (MailOutbox mail : pendingMails) {
227
            sendAndUpdateStatus(mail);
228
        }
229
    }
230
 
231
    @Transactional(propagation = Propagation.REQUIRES_NEW)
232
    public void cleanupOldMails(int daysOld) {
233
        mailOutboxRepository.deleteOldSentMails(daysOld);
234
    }
235
 
236
    private void sendAndUpdateStatus(MailOutbox mail) {
237
        try {
238
            List<MailOutboxAttachment> attachments = mailOutboxRepository.selectAttachmentsByMailId(mail.getId());
239
            sendSmtp(mail, attachments);
240
            mail.setStatus("SENT");
241
            mail.setSentAt(LocalDateTime.now());
242
            mail.setErrorMessage(null);
243
        } catch (Exception e) {
244
            LOGGER.error("Failed to send mail id={}, subject={}", mail.getId(), mail.getSubject(), e);
245
            mail.setStatus("FAILED");
246
            mail.setRetryCount(mail.getRetryCount() + 1);
247
            String errorMsg = e.getMessage();
248
            if (errorMsg != null && errorMsg.length() > 1000) {
249
                errorMsg = errorMsg.substring(0, 1000);
250
            }
251
            mail.setErrorMessage(errorMsg);
252
        }
253
    }
254
 
255
    private void sendSmtp(MailOutbox mail, List<MailOutboxAttachment> attachments) throws Exception {
35960 amit 256
        if (!prod) {
257
            if (devRecipient == null || devRecipient.trim().isEmpty()) {
258
                throw new IllegalStateException("Non-prod environment: set mail.outbox.dev.recipient in properties to your email before sending mails");
259
            }
260
            LOGGER.info("Non-prod: redirecting mail [subject={}] from [{}] to dev recipient [{}]", mail.getSubject(), mail.getEmailTo(), devRecipient);
261
        }
262
 
35956 amit 263
        JavaMailSender sender = resolveSender(mail.getSenderType());
264
        MimeMessage message = sender.createMimeMessage();
265
        boolean hasAttachments = attachments != null && !attachments.isEmpty();
266
        MimeMessageHelper helper = new MimeMessageHelper(message, hasAttachments);
267
 
35960 amit 268
        if (prod) {
269
            helper.setTo(mail.getEmailTo().split(","));
270
            if (mail.getEmailCc() != null && !mail.getEmailCc().isEmpty()) {
271
                helper.setCc(mail.getEmailCc().split(","));
272
            }
273
            if (mail.getEmailBcc() != null && !mail.getEmailBcc().isEmpty()) {
274
                helper.setBcc(mail.getEmailBcc().split(","));
275
            }
276
        } else {
277
            helper.setTo(devRecipient.trim());
35956 amit 278
        }
279
        helper.setSubject(mail.getSubject());
280
        helper.setText(mail.getBody(), mail.isHtml());
281
 
282
        String fromEmail = SENDER_GOOGLE.equals(mail.getSenderType()) ? "sdtech@smartdukaan.com" : "noreply@smartdukaan.com";
283
        String fromName = "SmartDukaan Care";
284
        helper.setFrom(new InternetAddress(fromEmail, fromName));
285
 
286
        if (hasAttachments) {
287
            for (MailOutboxAttachment att : attachments) {
288
                helper.addAttachment(att.getFileName(), new ByteArrayResource(att.getFileData()));
289
            }
290
        }
291
 
292
        sender.send(message);
293
    }
294
 
295
    private JavaMailSender resolveSender(String senderType) {
296
        if (SENDER_GOOGLE.equals(senderType)) {
297
            return googleMailSender;
298
        }
299
        return sendgridMailSender;
300
    }
301
 
302
    private byte[] readInputStreamSource(InputStreamSource source) throws IOException {
303
        try (InputStream is = source.getInputStream()) {
304
            return readAllBytes(is);
305
        }
306
    }
307
 
308
    private byte[] readAllBytes(InputStream is) throws IOException {
309
        java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
310
        byte[] buffer = new byte[8192];
311
        int len;
312
        while ((len = is.read(buffer)) != -1) {
313
            baos.write(buffer, 0, len);
314
        }
315
        return baos.toByteArray();
316
    }
317
 
318
    public static class AttachmentData {
319
        final String fileName;
320
        final byte[] data;
321
        final String contentType;
322
 
323
        public AttachmentData(String fileName, byte[] data, String contentType) {
324
            this.fileName = fileName;
325
            this.data = data;
326
            this.contentType = contentType;
327
        }
328
    }
329
}