Subversion Repositories SmartDukaan

Rev

Rev 36249 | 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;
35956 amit 12
import org.springframework.core.io.ByteArrayResource;
13
import org.springframework.core.io.InputStreamSource;
14
import org.springframework.mail.javamail.JavaMailSender;
15
import org.springframework.mail.javamail.MimeMessageHelper;
16
import org.springframework.stereotype.Service;
17
import org.springframework.transaction.annotation.Propagation;
18
import org.springframework.transaction.annotation.Transactional;
19
 
20
import javax.mail.internet.InternetAddress;
21
import javax.mail.internet.MimeMessage;
22
import java.io.File;
23
import java.io.IOException;
24
import java.io.InputStream;
25
import java.nio.file.Files;
26
import java.time.LocalDateTime;
27
import java.util.List;
28
 
29
@Service
30
public class MailOutboxService {
31
 
32
    private static final Logger LOGGER = LogManager.getLogger(MailOutboxService.class);
33
 
34
    public static final String SENDER_SENDGRID = "SENDGRID";
35
    public static final String SENDER_GOOGLE = "GOOGLE";
36245 amit 36
    public static final String SENDER_RELAY = "RELAY";
35956 amit 37
 
35960 amit 38
    @Value("${prod}")
39
    private boolean prod;
40
 
41
    /**
42
     * In dev/staging, mails are sent to this address instead of actual recipients.
43
     * Set to empty to throw an exception (forces developer to set their email).
44
     */
45
    @Value("${mail.outbox.dev.recipient:}")
46
    private String devRecipient;
47
 
35956 amit 48
    @Autowired
49
    private MailOutboxRepository mailOutboxRepository;
50
 
51
    @Autowired
52
    @Qualifier("mailSender")
53
    private JavaMailSender sendgridMailSender;
54
 
55
    @Autowired
56
    @Qualifier("googleMailSender")
57
    private JavaMailSender googleMailSender;
58
 
36245 amit 59
    @Autowired
60
    @Qualifier("gmailRelaySender")
61
    private JavaMailSender gmailRelaySender;
62
 
35956 amit 63
    // ---- Default sender (SendGrid) convenience methods ----
64
 
65
    public void queueMail(String[] emailTo, String[] cc, String subject, String body, String source) {
66
        queueMail(emailTo, cc, null, subject, body, false, source, SENDER_SENDGRID, (AttachmentData[]) null);
67
    }
68
 
69
    public void queueMail(String emailTo, String[] cc, String subject, String body, String source) {
70
        queueMail(new String[]{emailTo}, cc, null, subject, body, false, source, SENDER_SENDGRID, (AttachmentData[]) null);
71
    }
72
 
73
    public void queueMail(String[] emailTo, String[] cc, String subject, String body, boolean html, String source) {
74
        queueMail(emailTo, cc, null, subject, body, html, source, SENDER_SENDGRID, (AttachmentData[]) null);
75
    }
76
 
77
    public void queueMail(String[] emailTo, String[] cc, String[] bcc, String subject, String body, boolean html, String source) {
78
        queueMail(emailTo, cc, bcc, subject, body, html, source, SENDER_SENDGRID, (AttachmentData[]) null);
79
    }
80
 
81
    public void queueMailWithAttachments(String[] emailTo, String[] cc, String subject, String body, String source, Utils.Attachment... attachments) {
82
        queueMailWithAttachments(emailTo, cc, null, subject, body, false, source, SENDER_SENDGRID, attachments);
83
    }
84
 
85
    public void queueMailWithAttachments(String[] emailTo, String[] cc, String[] bcc, String subject, String body, boolean html, String source, Utils.Attachment... attachments) {
86
        queueMailWithAttachments(emailTo, cc, bcc, subject, body, html, source, SENDER_SENDGRID, attachments);
87
    }
88
 
89
    // ---- Google sender convenience methods ----
90
 
91
    public void queueMailViaGoogle(String[] emailTo, String[] cc, String subject, String body, String source) {
92
        queueMail(emailTo, cc, null, subject, body, false, source, SENDER_GOOGLE, (AttachmentData[]) null);
93
    }
94
 
95
    public void queueMailViaGoogle(String emailTo, String[] cc, String subject, String body, String source) {
96
        queueMail(new String[]{emailTo}, cc, null, subject, body, false, source, SENDER_GOOGLE, (AttachmentData[]) null);
97
    }
98
 
99
    public void queueMailViaGoogle(String[] emailTo, String[] cc, String subject, String body, boolean html, String source) {
100
        queueMail(emailTo, cc, null, subject, body, html, source, SENDER_GOOGLE, (AttachmentData[]) null);
101
    }
102
 
103
    public void queueMailViaGoogle(String[] emailTo, String[] cc, String[] bcc, String subject, String body, boolean html, String source) {
104
        queueMail(emailTo, cc, bcc, subject, body, html, source, SENDER_GOOGLE, (AttachmentData[]) null);
105
    }
106
 
107
    public void queueMailWithAttachmentsViaGoogle(String[] emailTo, String[] cc, String subject, String body, String source, Utils.Attachment... attachments) {
108
        queueMailWithAttachments(emailTo, cc, null, subject, body, false, source, SENDER_GOOGLE, attachments);
109
    }
110
 
111
    public void queueMailWithAttachmentsViaGoogle(String[] emailTo, String[] cc, String[] bcc, String subject, String body, boolean html, String source, Utils.Attachment... attachments) {
112
        queueMailWithAttachments(emailTo, cc, bcc, subject, body, html, source, SENDER_GOOGLE, attachments);
113
    }
114
 
36245 amit 115
    // ---- Google Workspace Relay sender convenience methods ----
116
 
117
    public void queueMailViaRelay(String[] emailTo, String[] cc, String subject, String body, String source) {
118
        queueMail(emailTo, cc, null, subject, body, false, source, SENDER_RELAY, (AttachmentData[]) null);
119
    }
120
 
121
    public void queueMailViaRelay(String emailTo, String[] cc, String subject, String body, String source) {
122
        queueMail(new String[]{emailTo}, cc, null, subject, body, false, source, SENDER_RELAY, (AttachmentData[]) null);
123
    }
124
 
125
    public void queueMailViaRelay(String[] emailTo, String[] cc, String subject, String body, boolean html, String source) {
126
        queueMail(emailTo, cc, null, subject, body, html, source, SENDER_RELAY, (AttachmentData[]) null);
127
    }
128
 
129
    public void queueMailViaRelay(String[] emailTo, String[] cc, String[] bcc, String subject, String body, boolean html, String source) {
130
        queueMail(emailTo, cc, bcc, subject, body, html, source, SENDER_RELAY, (AttachmentData[]) null);
131
    }
132
 
133
    public void queueMailWithAttachmentsViaRelay(String[] emailTo, String[] cc, String subject, String body, String source, Utils.Attachment... attachments) {
134
        queueMailWithAttachments(emailTo, cc, null, subject, body, false, source, SENDER_RELAY, attachments);
135
    }
136
 
137
    public void queueMailWithAttachmentsViaRelay(String[] emailTo, String[] cc, String[] bcc, String subject, String body, boolean html, String source, Utils.Attachment... attachments) {
138
        queueMailWithAttachments(emailTo, cc, bcc, subject, body, html, source, SENDER_RELAY, attachments);
139
    }
140
 
35956 amit 141
    // ---- Internal methods ----
142
 
143
    private void queueMailWithAttachments(String[] emailTo, String[] cc, String[] bcc, String subject, String body, boolean html, String source, String senderType, Utils.Attachment... attachments) {
144
        AttachmentData[] attachmentDataArray = null;
145
        if (attachments != null && attachments.length > 0) {
146
            attachmentDataArray = new AttachmentData[attachments.length];
147
            for (int i = 0; i < attachments.length; i++) {
148
                try {
149
                    byte[] data = readInputStreamSource(attachments[i].getInputStreamSource());
150
                    attachmentDataArray[i] = new AttachmentData(attachments[i].getFileName(), data, null);
151
                } catch (IOException e) {
152
                    LOGGER.error("Failed to read attachment: {}", attachments[i].getFileName(), e);
153
                    attachmentDataArray[i] = new AttachmentData(attachments[i].getFileName(), new byte[0], null);
154
                }
155
            }
156
        }
157
        queueMail(emailTo, cc, bcc, subject, body, html, source, senderType, attachmentDataArray);
158
    }
159
 
160
    public void queueMailWithFiles(String[] emailTo, String[] cc, String[] bcc, String subject, String body, String source, File... files) {
161
        AttachmentData[] attachmentDataArray = null;
162
        if (files != null && files.length > 0) {
163
            attachmentDataArray = new AttachmentData[files.length];
164
            for (int i = 0; i < files.length; i++) {
165
                try {
166
                    byte[] data = Files.readAllBytes(files[i].toPath());
167
                    String contentType = Files.probeContentType(files[i].toPath());
168
                    attachmentDataArray[i] = new AttachmentData(files[i].getName(), data, contentType);
169
                } catch (IOException e) {
170
                    LOGGER.error("Failed to read file attachment: {}", files[i].getName(), e);
171
                    attachmentDataArray[i] = new AttachmentData(files[i].getName(), new byte[0], null);
172
                }
173
            }
174
        }
175
        queueMail(emailTo, cc, bcc, subject, body, false, source, SENDER_SENDGRID, attachmentDataArray);
176
    }
177
 
178
    /**
179
     * Core method: persists mail + attachments in current transaction, triggers async send after commit.
180
     */
181
    private void queueMail(String[] emailTo, String[] cc, String[] bcc, String subject, String body, boolean html, String source, String senderType, AttachmentData... attachments) {
182
        MailOutbox mail = new MailOutbox();
183
        mail.setEmailTo(String.join(",", emailTo));
184
        if (cc != null && cc.length > 0) {
185
            mail.setEmailCc(String.join(",", cc));
186
        }
187
        if (bcc != null && bcc.length > 0) {
188
            mail.setEmailBcc(String.join(",", bcc));
189
        }
190
        mail.setSubject(subject != null ? subject : "");
191
        mail.setBody(body != null ? body : "");
192
        mail.setHtml(html);
193
        mail.setStatus("PENDING");
194
        mail.setRetryCount(0);
195
        mail.setCreatedAt(LocalDateTime.now());
196
        mail.setSource(source);
197
        mail.setSenderType(senderType);
198
 
199
        mailOutboxRepository.persist(mail);
200
 
201
        if (attachments != null) {
202
            for (AttachmentData att : attachments) {
203
                if (att != null && att.data.length > 0) {
204
                    MailOutboxAttachment attachment = new MailOutboxAttachment();
205
                    attachment.setMailOutboxId(mail.getId());
206
                    attachment.setFileName(att.fileName);
207
                    attachment.setFileData(att.data);
208
                    attachment.setContentType(att.contentType);
209
                    mailOutboxRepository.persistAttachment(attachment);
210
                }
211
            }
212
        }
213
 
214
    }
215
 
216
    @Transactional(propagation = Propagation.REQUIRES_NEW)
217
    public void processPendingMails() {
218
        List<MailOutbox> pendingMails = mailOutboxRepository.selectPending();
219
        LOGGER.info("Processing {} pending mails", pendingMails.size());
220
        for (MailOutbox mail : pendingMails) {
221
            sendAndUpdateStatus(mail);
222
        }
223
    }
224
 
225
    @Transactional(propagation = Propagation.REQUIRES_NEW)
226
    public void cleanupOldMails(int daysOld) {
227
        mailOutboxRepository.deleteOldSentMails(daysOld);
228
    }
229
 
36250 amit 230
    @Transactional(propagation = Propagation.REQUIRES_NEW)
231
    public void cleanupOldMails(int daysOld, String senderType) {
232
        mailOutboxRepository.deleteOldSentMailsBySenderType(daysOld, senderType);
233
    }
234
 
35956 amit 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
        }
36249 amit 298
        // SendGrid and Relay both route through Google Workspace Relay
299
        return gmailRelaySender;
35956 amit 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
}