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;@Servicepublic 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;@Autowiredprivate 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 Relayreturn 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;}}}