Subversion Repositories SmartDukaan

Rev

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