Subversion Repositories SmartDukaan

Rev

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