Subversion Repositories SmartDukaan

Rev

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