Subversion Repositories SmartDukaan

Rev

Rev 36250 | Blame | Compare with Previous | Last modification | View Log | RSS feed

package com.spice.profitmandi.service.mail;

import com.spice.profitmandi.common.util.Utils;
import com.spice.profitmandi.dao.entity.mail.MailOutbox;
import com.spice.profitmandi.dao.entity.mail.MailOutboxAttachment;
import com.spice.profitmandi.dao.repository.mail.MailOutboxRepository;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.InputStreamSource;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.time.LocalDateTime;
import java.util.List;

@Service
public class MailOutboxService {

    private static final Logger LOGGER = LogManager.getLogger(MailOutboxService.class);

    public static final String SENDER_SENDGRID = "SENDGRID";
    public static final String SENDER_GOOGLE = "GOOGLE";
    public static final String SENDER_RELAY = "RELAY";

    @Value("${prod}")
    private boolean prod;

    /**
     * In dev/staging, mails are sent to this address instead of actual recipients.
     * Set to empty to throw an exception (forces developer to set their email).
     */
    @Value("${mail.outbox.dev.recipient:}")
    private String devRecipient;

    @Autowired
    private MailOutboxRepository mailOutboxRepository;

    @Autowired
    @Qualifier("googleMailSender")
    private JavaMailSender googleMailSender;

    @Autowired
    @Qualifier("gmailRelaySender")
    private JavaMailSender gmailRelaySender;

    // ---- Default sender (SendGrid) convenience methods ----

    public void queueMail(String[] emailTo, String[] cc, String subject, String body, String source) {
        queueMail(emailTo, cc, null, subject, body, false, source, SENDER_SENDGRID, (AttachmentData[]) null);
    }

    public void queueMail(String emailTo, String[] cc, String subject, String body, String source) {
        queueMail(new String[]{emailTo}, cc, null, subject, body, false, source, SENDER_SENDGRID, (AttachmentData[]) null);
    }

    public void queueMail(String[] emailTo, String[] cc, String subject, String body, boolean html, String source) {
        queueMail(emailTo, cc, null, subject, body, html, source, SENDER_SENDGRID, (AttachmentData[]) null);
    }

    public void queueMail(String[] emailTo, String[] cc, String[] bcc, String subject, String body, boolean html, String source) {
        queueMail(emailTo, cc, bcc, subject, body, html, source, SENDER_SENDGRID, (AttachmentData[]) null);
    }

    public void queueMailWithAttachments(String[] emailTo, String[] cc, String subject, String body, String source, Utils.Attachment... attachments) {
        queueMailWithAttachments(emailTo, cc, null, subject, body, false, source, SENDER_SENDGRID, attachments);
    }

    public void queueMailWithAttachments(String[] emailTo, String[] cc, String[] bcc, String subject, String body, boolean html, String source, Utils.Attachment... attachments) {
        queueMailWithAttachments(emailTo, cc, bcc, subject, body, html, source, SENDER_SENDGRID, attachments);
    }

    // ---- Google sender convenience methods ----

    public void queueMailViaGoogle(String[] emailTo, String[] cc, String subject, String body, String source) {
        queueMail(emailTo, cc, null, subject, body, false, source, SENDER_GOOGLE, (AttachmentData[]) null);
    }

    public void queueMailViaGoogle(String emailTo, String[] cc, String subject, String body, String source) {
        queueMail(new String[]{emailTo}, cc, null, subject, body, false, source, SENDER_GOOGLE, (AttachmentData[]) null);
    }

    public void queueMailViaGoogle(String[] emailTo, String[] cc, String subject, String body, boolean html, String source) {
        queueMail(emailTo, cc, null, subject, body, html, source, SENDER_GOOGLE, (AttachmentData[]) null);
    }

    public void queueMailViaGoogle(String[] emailTo, String[] cc, String[] bcc, String subject, String body, boolean html, String source) {
        queueMail(emailTo, cc, bcc, subject, body, html, source, SENDER_GOOGLE, (AttachmentData[]) null);
    }

    public void queueMailWithAttachmentsViaGoogle(String[] emailTo, String[] cc, String subject, String body, String source, Utils.Attachment... attachments) {
        queueMailWithAttachments(emailTo, cc, null, subject, body, false, source, SENDER_GOOGLE, attachments);
    }

    public void queueMailWithAttachmentsViaGoogle(String[] emailTo, String[] cc, String[] bcc, String subject, String body, boolean html, String source, Utils.Attachment... attachments) {
        queueMailWithAttachments(emailTo, cc, bcc, subject, body, html, source, SENDER_GOOGLE, attachments);
    }

    // ---- Google Workspace Relay sender convenience methods ----

    public void queueMailViaRelay(String[] emailTo, String[] cc, String subject, String body, String source) {
        queueMail(emailTo, cc, null, subject, body, false, source, SENDER_RELAY, (AttachmentData[]) null);
    }

    public void queueMailViaRelay(String emailTo, String[] cc, String subject, String body, String source) {
        queueMail(new String[]{emailTo}, cc, null, subject, body, false, source, SENDER_RELAY, (AttachmentData[]) null);
    }

    public void queueMailViaRelay(String[] emailTo, String[] cc, String subject, String body, boolean html, String source) {
        queueMail(emailTo, cc, null, subject, body, html, source, SENDER_RELAY, (AttachmentData[]) null);
    }

    public void queueMailViaRelay(String[] emailTo, String[] cc, String[] bcc, String subject, String body, boolean html, String source) {
        queueMail(emailTo, cc, bcc, subject, body, html, source, SENDER_RELAY, (AttachmentData[]) null);
    }

    public void queueMailWithAttachmentsViaRelay(String[] emailTo, String[] cc, String subject, String body, String source, Utils.Attachment... attachments) {
        queueMailWithAttachments(emailTo, cc, null, subject, body, false, source, SENDER_RELAY, attachments);
    }

    public void queueMailWithAttachmentsViaRelay(String[] emailTo, String[] cc, String[] bcc, String subject, String body, boolean html, String source, Utils.Attachment... attachments) {
        queueMailWithAttachments(emailTo, cc, bcc, subject, body, html, source, SENDER_RELAY, attachments);
    }

    // ---- Internal methods ----

    private void queueMailWithAttachments(String[] emailTo, String[] cc, String[] bcc, String subject, String body, boolean html, String source, String senderType, Utils.Attachment... attachments) {
        AttachmentData[] attachmentDataArray = null;
        if (attachments != null && attachments.length > 0) {
            attachmentDataArray = new AttachmentData[attachments.length];
            for (int i = 0; i < attachments.length; i++) {
                try {
                    byte[] data = readInputStreamSource(attachments[i].getInputStreamSource());
                    attachmentDataArray[i] = new AttachmentData(attachments[i].getFileName(), data, null);
                } catch (IOException e) {
                    LOGGER.error("Failed to read attachment: {}", attachments[i].getFileName(), e);
                    attachmentDataArray[i] = new AttachmentData(attachments[i].getFileName(), new byte[0], null);
                }
            }
        }
        queueMail(emailTo, cc, bcc, subject, body, html, source, senderType, attachmentDataArray);
    }

    public void queueMailWithFiles(String[] emailTo, String[] cc, String[] bcc, String subject, String body, String source, File... files) {
        AttachmentData[] attachmentDataArray = null;
        if (files != null && files.length > 0) {
            attachmentDataArray = new AttachmentData[files.length];
            for (int i = 0; i < files.length; i++) {
                try {
                    byte[] data = Files.readAllBytes(files[i].toPath());
                    String contentType = Files.probeContentType(files[i].toPath());
                    attachmentDataArray[i] = new AttachmentData(files[i].getName(), data, contentType);
                } catch (IOException e) {
                    LOGGER.error("Failed to read file attachment: {}", files[i].getName(), e);
                    attachmentDataArray[i] = new AttachmentData(files[i].getName(), new byte[0], null);
                }
            }
        }
        queueMail(emailTo, cc, bcc, subject, body, false, source, SENDER_SENDGRID, attachmentDataArray);
    }

    /**
     * Core method: persists mail + attachments in current transaction, triggers async send after commit.
     */
    private void queueMail(String[] emailTo, String[] cc, String[] bcc, String subject, String body, boolean html, String source, String senderType, AttachmentData... attachments) {
        MailOutbox mail = new MailOutbox();
        mail.setEmailTo(String.join(",", emailTo));
        if (cc != null && cc.length > 0) {
            mail.setEmailCc(String.join(",", cc));
        }
        if (bcc != null && bcc.length > 0) {
            mail.setEmailBcc(String.join(",", bcc));
        }
        mail.setSubject(subject != null ? subject : "");
        mail.setBody(body != null ? body : "");
        mail.setHtml(html);
        mail.setStatus("PENDING");
        mail.setRetryCount(0);
        mail.setCreatedAt(LocalDateTime.now());
        mail.setSource(source);
        mail.setSenderType(senderType);

        mailOutboxRepository.persist(mail);

        if (attachments != null) {
            for (AttachmentData att : attachments) {
                if (att != null && att.data.length > 0) {
                    MailOutboxAttachment attachment = new MailOutboxAttachment();
                    attachment.setMailOutboxId(mail.getId());
                    attachment.setFileName(att.fileName);
                    attachment.setFileData(att.data);
                    attachment.setContentType(att.contentType);
                    mailOutboxRepository.persistAttachment(attachment);
                }
            }
        }

    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void processPendingMails() {
        List<MailOutbox> pendingMails = mailOutboxRepository.selectPending();
        LOGGER.info("Processing {} pending mails", pendingMails.size());
        for (MailOutbox mail : pendingMails) {
            sendAndUpdateStatus(mail);
        }
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void cleanupOldMails(int daysOld) {
        mailOutboxRepository.deleteOldSentMails(daysOld);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void cleanupOldMails(int daysOld, String senderType) {
        mailOutboxRepository.deleteOldSentMailsBySenderType(daysOld, senderType);
    }

    private void sendAndUpdateStatus(MailOutbox mail) {
        try {
            List<MailOutboxAttachment> attachments = mailOutboxRepository.selectAttachmentsByMailId(mail.getId());
            sendSmtp(mail, attachments);
            mail.setStatus("SENT");
            mail.setSentAt(LocalDateTime.now());
            mail.setErrorMessage(null);
        } catch (Exception e) {
            LOGGER.error("Failed to send mail id={}, subject={}", mail.getId(), mail.getSubject(), e);
            mail.setStatus("FAILED");
            mail.setRetryCount(mail.getRetryCount() + 1);
            String errorMsg = e.getMessage();
            if (errorMsg != null && errorMsg.length() > 1000) {
                errorMsg = errorMsg.substring(0, 1000);
            }
            mail.setErrorMessage(errorMsg);
        }
    }

    private void sendSmtp(MailOutbox mail, List<MailOutboxAttachment> attachments) throws Exception {
        if (!prod) {
            if (devRecipient == null || devRecipient.trim().isEmpty()) {
                throw new IllegalStateException("Non-prod environment: set mail.outbox.dev.recipient in properties to your email before sending mails");
            }
            LOGGER.info("Non-prod: redirecting mail [subject={}] from [{}] to dev recipient [{}]", mail.getSubject(), mail.getEmailTo(), devRecipient);
        }

        JavaMailSender sender = resolveSender(mail.getSenderType());
        MimeMessage message = sender.createMimeMessage();
        boolean hasAttachments = attachments != null && !attachments.isEmpty();
        MimeMessageHelper helper = new MimeMessageHelper(message, hasAttachments);

        if (prod) {
            helper.setTo(mail.getEmailTo().split(","));
            if (mail.getEmailCc() != null && !mail.getEmailCc().isEmpty()) {
                helper.setCc(mail.getEmailCc().split(","));
            }
            if (mail.getEmailBcc() != null && !mail.getEmailBcc().isEmpty()) {
                helper.setBcc(mail.getEmailBcc().split(","));
            }
        } else {
            helper.setTo(devRecipient.trim());
        }
        helper.setSubject(mail.getSubject());
        helper.setText(mail.getBody(), mail.isHtml());

        String fromEmail = SENDER_GOOGLE.equals(mail.getSenderType()) ? "sdtech@smartdukaan.com" : "noreply@smartdukaan.com";
        String fromName = "SmartDukaan Care";
        helper.setFrom(new InternetAddress(fromEmail, fromName));

        if (hasAttachments) {
            for (MailOutboxAttachment att : attachments) {
                helper.addAttachment(att.getFileName(), new ByteArrayResource(att.getFileData()));
            }
        }

        sender.send(message);
    }

    private JavaMailSender resolveSender(String senderType) {
        if (SENDER_GOOGLE.equals(senderType)) {
            return googleMailSender;
        }
        // SendGrid and Relay both route through Google Workspace Relay
        return gmailRelaySender;
    }

    private byte[] readInputStreamSource(InputStreamSource source) throws IOException {
        try (InputStream is = source.getInputStream()) {
            return readAllBytes(is);
        }
    }

    private byte[] readAllBytes(InputStream is) throws IOException {
        java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
        byte[] buffer = new byte[8192];
        int len;
        while ((len = is.read(buffer)) != -1) {
            baos.write(buffer, 0, len);
        }
        return baos.toByteArray();
    }

    public static class AttachmentData {
        final String fileName;
        final byte[] data;
        final String contentType;

        public AttachmentData(String fileName, byte[] data, String contentType) {
            this.fileName = fileName;
            this.data = data;
            this.contentType = contentType;
        }
    }
}