Subversion Repositories SmartDukaan

Rev

Rev 35956 | Rev 36059 | 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 {
208
            MailOutbox mail = mailOutboxRepository.selectPending().stream()
209
                    .filter(m -> m.getId() == mailOutboxId)
210
                    .findFirst()
211
                    .orElse(null);
212
            if (mail == null || "SENT".equals(mail.getStatus())) {
213
                return;
214
            }
215
            sendAndUpdateStatus(mail);
216
        } catch (Exception e) {
217
            LOGGER.error("Async mail send failed for mailOutboxId={}", mailOutboxId, e);
218
        }
219
    }
220
 
221
    @Transactional(propagation = Propagation.REQUIRES_NEW)
222
    public void processPendingMails() {
223
        List<MailOutbox> pendingMails = mailOutboxRepository.selectPending();
224
        LOGGER.info("Processing {} pending mails", pendingMails.size());
225
        for (MailOutbox mail : pendingMails) {
226
            sendAndUpdateStatus(mail);
227
        }
228
    }
229
 
230
    @Transactional(propagation = Propagation.REQUIRES_NEW)
231
    public void cleanupOldMails(int daysOld) {
232
        mailOutboxRepository.deleteOldSentMails(daysOld);
233
    }
234
 
235
    private void sendAndUpdateStatus(MailOutbox mail) {
236
        try {
237
            List<MailOutboxAttachment> attachments = mailOutboxRepository.selectAttachmentsByMailId(mail.getId());
238
            sendSmtp(mail, attachments);
239
            mail.setStatus("SENT");
240
            mail.setSentAt(LocalDateTime.now());
241
            mail.setErrorMessage(null);
242
        } catch (Exception e) {
243
            LOGGER.error("Failed to send mail id={}, subject={}", mail.getId(), mail.getSubject(), e);
244
            mail.setStatus("FAILED");
245
            mail.setRetryCount(mail.getRetryCount() + 1);
246
            String errorMsg = e.getMessage();
247
            if (errorMsg != null && errorMsg.length() > 1000) {
248
                errorMsg = errorMsg.substring(0, 1000);
249
            }
250
            mail.setErrorMessage(errorMsg);
251
        }
252
    }
253
 
254
    private void sendSmtp(MailOutbox mail, List<MailOutboxAttachment> attachments) throws Exception {
35960 amit 255
        if (!prod) {
256
            if (devRecipient == null || devRecipient.trim().isEmpty()) {
257
                throw new IllegalStateException("Non-prod environment: set mail.outbox.dev.recipient in properties to your email before sending mails");
258
            }
259
            LOGGER.info("Non-prod: redirecting mail [subject={}] from [{}] to dev recipient [{}]", mail.getSubject(), mail.getEmailTo(), devRecipient);
260
        }
261
 
35956 amit 262
        JavaMailSender sender = resolveSender(mail.getSenderType());
263
        MimeMessage message = sender.createMimeMessage();
264
        boolean hasAttachments = attachments != null && !attachments.isEmpty();
265
        MimeMessageHelper helper = new MimeMessageHelper(message, hasAttachments);
266
 
35960 amit 267
        if (prod) {
268
            helper.setTo(mail.getEmailTo().split(","));
269
            if (mail.getEmailCc() != null && !mail.getEmailCc().isEmpty()) {
270
                helper.setCc(mail.getEmailCc().split(","));
271
            }
272
            if (mail.getEmailBcc() != null && !mail.getEmailBcc().isEmpty()) {
273
                helper.setBcc(mail.getEmailBcc().split(","));
274
            }
275
        } else {
276
            helper.setTo(devRecipient.trim());
35956 amit 277
        }
278
        helper.setSubject(mail.getSubject());
279
        helper.setText(mail.getBody(), mail.isHtml());
280
 
281
        String fromEmail = SENDER_GOOGLE.equals(mail.getSenderType()) ? "sdtech@smartdukaan.com" : "noreply@smartdukaan.com";
282
        String fromName = "SmartDukaan Care";
283
        helper.setFrom(new InternetAddress(fromEmail, fromName));
284
 
285
        if (hasAttachments) {
286
            for (MailOutboxAttachment att : attachments) {
287
                helper.addAttachment(att.getFileName(), new ByteArrayResource(att.getFileData()));
288
            }
289
        }
290
 
291
        sender.send(message);
292
    }
293
 
294
    private JavaMailSender resolveSender(String senderType) {
295
        if (SENDER_GOOGLE.equals(senderType)) {
296
            return googleMailSender;
297
        }
298
        return sendgridMailSender;
299
    }
300
 
301
    private byte[] readInputStreamSource(InputStreamSource source) throws IOException {
302
        try (InputStream is = source.getInputStream()) {
303
            return readAllBytes(is);
304
        }
305
    }
306
 
307
    private byte[] readAllBytes(InputStream is) throws IOException {
308
        java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
309
        byte[] buffer = new byte[8192];
310
        int len;
311
        while ((len = is.read(buffer)) != -1) {
312
            baos.write(buffer, 0, len);
313
        }
314
        return baos.toByteArray();
315
    }
316
 
317
    public static class AttachmentData {
318
        final String fileName;
319
        final byte[] data;
320
        final String contentType;
321
 
322
        public AttachmentData(String fileName, byte[] data, String contentType) {
323
            this.fileName = fileName;
324
            this.data = data;
325
            this.contentType = contentType;
326
        }
327
    }
328
}