Java tutorial
/* * Copyright (c) 2008-2016 Haulmont. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.haulmont.cuba.core.app; import com.haulmont.cuba.core.EntityManager; import com.haulmont.cuba.core.Persistence; import com.haulmont.cuba.core.Transaction; import com.haulmont.cuba.core.TypedQuery; import com.haulmont.cuba.core.entity.FileDescriptor; import com.haulmont.cuba.core.entity.SendingAttachment; import com.haulmont.cuba.core.entity.SendingMessage; import com.haulmont.cuba.core.global.*; import com.haulmont.cuba.core.sys.AppContext; import com.haulmont.cuba.security.app.Authentication; import com.sun.mail.smtp.SMTPAddressFailedException; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.time.DateUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.task.TaskExecutor; import org.springframework.mail.MailSendException; import org.springframework.stereotype.Component; import javax.annotation.Nullable; import javax.annotation.Resource; import javax.inject.Inject; import javax.mail.internet.AddressException; import java.io.Serializable; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.RejectedExecutionException; @Component(EmailerAPI.NAME) public class Emailer implements EmailerAPI { protected static final String BODY_FILE_EXTENSION = "txt"; private static final Logger log = LoggerFactory.getLogger(Emailer.class); protected EmailerConfig config; protected volatile int callCount = 0; @Resource(name = "mailSendTaskExecutor") protected TaskExecutor mailSendTaskExecutor; @Inject protected UserSessionSource userSessionSource; @Inject protected TimeSource timeSource; @Inject protected Persistence persistence; @Inject protected Metadata metadata; @Inject protected Authentication authentication; @Inject protected EmailSenderAPI emailSender; @Inject protected Resources resources; @Inject protected FileStorageAPI fileStorage; @Inject public void setConfig(Configuration configuration) { this.config = configuration.getConfig(EmailerConfig.class); } protected String getEmailerLogin() { return config.getEmailerUserLogin(); } @Override public void sendEmail(String addresses, String caption, String body, EmailAttachment... attachments) throws EmailException { sendEmail(new EmailInfo(addresses, caption, null, body, attachments)); } @Override public void sendEmail(EmailInfo info) throws EmailException { prepareEmailInfo(info); persistAndSendEmail(info); } @Override public List<SendingMessage> sendEmailAsync(EmailInfo info) { //noinspection UnnecessaryLocalVariable List<SendingMessage> result = sendEmailAsync(info, null, null); return result; } @Override public List<SendingMessage> sendEmailAsync(EmailInfo info, Integer attemptsCount, Date deadline) { prepareEmailInfo(info); List<SendingMessage> messages = splitEmail(info, attemptsCount, deadline); persistMessages(messages, SendingStatus.QUEUE); return messages; } protected void prepareEmailInfo(EmailInfo emailInfo) { processBodyTemplate(emailInfo); if (emailInfo.getFrom() == null) { String defaultFromAddress = config.getFromAddress(); if (defaultFromAddress == null) { throw new IllegalStateException("cuba.email.fromAddress not set in the system"); } emailInfo.setFrom(defaultFromAddress); } } protected void processBodyTemplate(EmailInfo info) { String templatePath = info.getTemplatePath(); if (templatePath == null) { return; } Map<String, Serializable> params = info.getTemplateParameters() == null ? Collections.<String, Serializable>emptyMap() : info.getTemplateParameters(); String templateContents = resources.getResourceAsString(templatePath); if (templateContents == null) { throw new IllegalArgumentException("Could not find template by path: " + templatePath); } String body = TemplateHelper.processTemplate(templateContents, params); info.setBody(body); } protected List<SendingMessage> splitEmail(EmailInfo info, @Nullable Integer attemptsCount, @Nullable Date deadline) { List<SendingMessage> sendingMessageList = new ArrayList<>(); String[] splitAddresses = info.getAddresses().split("[,;]"); for (String address : splitAddresses) { address = address.trim(); if (StringUtils.isNotBlank(address)) { SendingMessage sendingMessage = convertToSendingMessage(address, info.getFrom(), info.getCaption(), info.getBody(), info.getHeaders(), info.getAttachments(), attemptsCount, deadline); sendingMessageList.add(sendingMessage); } } return sendingMessageList; } protected void sendSendingMessage(SendingMessage sendingMessage) { Objects.requireNonNull(sendingMessage, "sendingMessage is null"); Objects.requireNonNull(sendingMessage.getAddress(), "sendingMessage.address is null"); Objects.requireNonNull(sendingMessage.getCaption(), "sendingMessage.caption is null"); Objects.requireNonNull(sendingMessage.getContentText(), "sendingMessage.contentText is null"); Objects.requireNonNull(sendingMessage.getFrom(), "sendingMessage.from is null"); try { emailSender.sendEmail(sendingMessage); markAsSent(sendingMessage); } catch (Exception e) { log.warn("Unable to send email to '" + sendingMessage.getAddress() + "'", e); if (isNeedToRetry(e)) { returnToQueue(sendingMessage); } else { markAsNonSent(sendingMessage); } } } protected void persistAndSendEmail(EmailInfo emailInfo) throws EmailException { Objects.requireNonNull(emailInfo.getAddresses(), "addresses are null"); Objects.requireNonNull(emailInfo.getCaption(), "caption is null"); Objects.requireNonNull(emailInfo.getBody(), "body is null"); Objects.requireNonNull(emailInfo.getFrom(), "from is null"); List<SendingMessage> messages = splitEmail(emailInfo, null, null); List<String> failedAddresses = new ArrayList<>(); List<String> errorMessages = new ArrayList<>(); for (SendingMessage sendingMessage : messages) { SendingMessage persistedMessage = persistMessageIfPossible(sendingMessage); try { emailSender.sendEmail(sendingMessage); if (persistedMessage != null) { markAsSent(persistedMessage); } } catch (Exception e) { log.warn("Unable to send email to '" + sendingMessage.getAddress() + "'", e); failedAddresses.add(sendingMessage.getAddress()); errorMessages.add(e.getMessage()); if (persistedMessage != null) { markAsNonSent(persistedMessage); } } } if (!failedAddresses.isEmpty()) { throw new EmailException(failedAddresses, errorMessages); } } /* * Try to persist message and catch all errors to allow actual delivery * in case of database or file storage failure. */ @Nullable protected SendingMessage persistMessageIfPossible(SendingMessage sendingMessage) { // A copy of sendingMessage is created // to avoid additional overhead to load body and attachments back from FS try { SendingMessage clonedMessage = createClone(sendingMessage); persistMessages(Collections.singletonList(clonedMessage), SendingStatus.SENDING); return clonedMessage; } catch (Exception e) { log.error("Failed to persist message " + sendingMessage.getCaption(), e); return null; } } protected SendingMessage createClone(SendingMessage srcMessage) { SendingMessage clonedMessage = metadata.getTools().copy(srcMessage); List<SendingAttachment> clonedList = new ArrayList<>(); for (SendingAttachment srcAttach : srcMessage.getAttachments()) { SendingAttachment clonedAttach = (SendingAttachment) metadata.getTools().copy(srcAttach); clonedAttach.setMessage(null); clonedAttach.setMessage(clonedMessage); clonedList.add(clonedAttach); } clonedMessage.setAttachments(clonedList); return clonedMessage; } @Override public String processQueuedEmails() { if (applicationNotStartedYet()) { return null; } int callsToSkip = config.getDelayCallCount(); if (callCount < callsToSkip) { callCount++; return null; } String resultMessage; try { authentication.begin(getEmailerLogin()); try { resultMessage = sendQueuedEmails(); } finally { authentication.end(); } } catch (Throwable e) { log.error("Error", e); resultMessage = e.getMessage(); } return resultMessage; } protected boolean applicationNotStartedYet() { return !AppContext.isStarted(); } protected String sendQueuedEmails() { List<SendingMessage> messagesToSend = loadEmailsToSend(); for (SendingMessage msg : messagesToSend) { submitExecutorTask(msg); } if (messagesToSend.isEmpty()) { return ""; } return String.format("Processed %d emails", messagesToSend.size()); } protected boolean shouldMarkNotSent(SendingMessage sendingMessage) { Date deadline = sendingMessage.getDeadline(); if (deadline != null && deadline.before(timeSource.currentTimestamp())) { return true; } Integer messageAttemptsLimit = sendingMessage.getAttemptsCount(); int defaultLimit = config.getDefaultSendingAttemptsCount(); int attemptsLimit = messageAttemptsLimit != null ? messageAttemptsLimit : defaultLimit; //noinspection UnnecessaryLocalVariable boolean res = sendingMessage.getAttemptsMade() != null && sendingMessage.getAttemptsMade() >= attemptsLimit; return res; } protected void submitExecutorTask(SendingMessage msg) { try { Runnable mailSendTask = new EmailSendTask(msg); mailSendTaskExecutor.execute(mailSendTask); } catch (RejectedExecutionException e) { returnToQueue(msg); } catch (Exception e) { log.error("Exception while sending email: ", e); if (isNeedToRetry(e)) { returnToQueue(msg); } else { markAsNonSent(msg); } } } protected List<SendingMessage> loadEmailsToSend() { Date sendTimeoutTime = DateUtils.addSeconds(timeSource.currentTimestamp(), -config.getSendingTimeoutSec()); List<SendingMessage> emailsToSend = new ArrayList<>(); try (Transaction tx = persistence.createTransaction()) { EntityManager em = persistence.getEntityManager(); TypedQuery<SendingMessage> query = em.createQuery("select sm from sys$SendingMessage sm" + " where sm.status = :statusQueue or (sm.status = :statusSending and sm.updateTs < :time)" + " order by sm.createTs", SendingMessage.class); query.setParameter("statusQueue", SendingStatus.QUEUE.getId()); query.setParameter("time", sendTimeoutTime); query.setParameter("statusSending", SendingStatus.SENDING.getId()); View view = metadata.getViewRepository().getView(SendingMessage.class, "sendingMessage.loadFromQueue"); view.setLoadPartialEntities(true); // because SendingAttachment.content has FetchType.LAZY query.setView(view); query.setMaxResults(config.getMessageQueueCapacity()); List<SendingMessage> resList = query.getResultList(); for (SendingMessage msg : resList) { if (shouldMarkNotSent(msg)) { msg.setStatus(SendingStatus.NOTSENT); } else { msg.setStatus(SendingStatus.SENDING); emailsToSend.add(msg); } } tx.commit(); } for (SendingMessage message : emailsToSend) { loadBodyAndAttachments(message); } return emailsToSend; } @Override public String loadContentText(SendingMessage sendingMessage) { SendingMessage msg; try (Transaction tx = persistence.createTransaction()) { EntityManager em = persistence.getEntityManager(); msg = em.reload(sendingMessage, "sendingMessage.loadContentText"); tx.commit(); } Objects.requireNonNull(msg, "Sending message not found: " + sendingMessage.getId()); if (msg.getContentTextFile() != null) { byte[] bodyContent; try { bodyContent = fileStorage.loadFile(msg.getContentTextFile()); } catch (FileStorageException e) { throw new RuntimeException(e); } //noinspection UnnecessaryLocalVariable String res = bodyTextFromByteArray(bodyContent); return res; } else { return msg.getContentText(); } } protected void loadBodyAndAttachments(SendingMessage message) { try { if (message.getContentTextFile() != null) { byte[] bodyContent = fileStorage.loadFile(message.getContentTextFile()); String body = bodyTextFromByteArray(bodyContent); message.setContentText(body); } for (SendingAttachment attachment : message.getAttachments()) { if (attachment.getContentFile() != null) { byte[] content = fileStorage.loadFile(attachment.getContentFile()); attachment.setContent(content); } } } catch (FileStorageException e) { log.error("Failed to load body or attachments for " + message); } } protected void persistMessages(List<SendingMessage> sendingMessageList, SendingStatus status) { MessagePersistingContext context = new MessagePersistingContext(); try { try (Transaction tx = persistence.createTransaction()) { EntityManager em = persistence.getEntityManager(); for (SendingMessage message : sendingMessageList) { message.setStatus(status); try { persistSendingMessage(em, message, context); } catch (FileStorageException e) { throw new RuntimeException("Failed to store message " + message.getCaption(), e); } } tx.commit(); } context.finished(); } finally { removeOrphanFiles(context); } } protected void removeOrphanFiles(MessagePersistingContext context) { for (FileDescriptor file : context.files) { try { fileStorage.removeFile(file); } catch (Exception e) { log.error("Failed to remove file " + file); } } } protected void persistSendingMessage(EntityManager em, SendingMessage message, MessagePersistingContext context) throws FileStorageException { boolean useFileStorage = config.isFileStorageUsed(); if (useFileStorage) { byte[] bodyBytes = bodyTextToBytes(message); FileDescriptor contentTextFile = createBodyFileDescriptor(message, bodyBytes); fileStorage.saveFile(contentTextFile, bodyBytes); context.files.add(contentTextFile); em.persist(contentTextFile); message.setContentTextFile(contentTextFile); message.setContentText(null); } em.persist(message); for (SendingAttachment attachment : message.getAttachments()) { if (useFileStorage) { FileDescriptor contentFile = createAttachmentFileDescriptor(attachment); fileStorage.saveFile(contentFile, attachment.getContent()); context.files.add(contentFile); em.persist(contentFile); attachment.setContentFile(contentFile); attachment.setContent(null); } em.persist(attachment); } } protected FileDescriptor createAttachmentFileDescriptor(SendingAttachment attachment) { FileDescriptor contentFile = metadata.create(FileDescriptor.class); contentFile.setCreateDate(timeSource.currentTimestamp()); contentFile.setName(attachment.getName()); contentFile.setExtension(FilenameUtils.getExtension(attachment.getName())); contentFile.setSize((long) attachment.getContent().length); return contentFile; } protected FileDescriptor createBodyFileDescriptor(SendingMessage message, byte[] bodyBytes) { FileDescriptor contentTextFile = metadata.create(FileDescriptor.class); contentTextFile.setCreateDate(timeSource.currentTimestamp()); contentTextFile.setName("Email_" + message.getId() + "." + BODY_FILE_EXTENSION); contentTextFile.setExtension(BODY_FILE_EXTENSION); contentTextFile.setSize((long) bodyBytes.length); return contentTextFile; } protected void returnToQueue(SendingMessage sendingMessage) { try (Transaction tx = persistence.createTransaction()) { EntityManager em = persistence.getEntityManager(); SendingMessage msg = em.merge(sendingMessage); msg.setAttemptsMade(msg.getAttemptsMade() + 1); msg.setStatus(SendingStatus.QUEUE); tx.commit(); } catch (Exception e) { log.error("Error returning message to '{}' to the queue", sendingMessage.getAddress(), e); } } protected void markAsSent(SendingMessage sendingMessage) { try (Transaction tx = persistence.createTransaction()) { EntityManager em = persistence.getEntityManager(); SendingMessage msg = em.merge(sendingMessage); msg.setStatus(SendingStatus.SENT); msg.setAttemptsMade(msg.getAttemptsMade() + 1); msg.setDateSent(timeSource.currentTimestamp()); tx.commit(); } catch (Exception e) { log.error("Error marking message to '{}' as sent", sendingMessage.getAddress(), e); } } protected void markAsNonSent(SendingMessage sendingMessage) { try (Transaction tx = persistence.createTransaction()) { EntityManager em = persistence.getEntityManager(); SendingMessage msg = em.merge(sendingMessage); msg.setStatus(SendingStatus.NOTSENT); msg.setAttemptsMade(msg.getAttemptsMade() + 1); tx.commit(); } catch (Exception e) { log.error("Error marking message to '{}' as not sent", sendingMessage.getAddress(), e); } } protected SendingMessage convertToSendingMessage(String address, String from, String caption, String body, @Nullable List<EmailHeader> headers, @Nullable EmailAttachment[] attachments, @Nullable Integer attemptsCount, @Nullable Date deadline) { SendingMessage sendingMessage = metadata.create(SendingMessage.class); sendingMessage.setAddress(address); sendingMessage.setFrom(from); sendingMessage.setContentText(body); sendingMessage.setCaption(caption); sendingMessage.setAttemptsCount(attemptsCount); sendingMessage.setDeadline(deadline); sendingMessage.setAttemptsMade(0); if (attachments != null && attachments.length > 0) { StringBuilder attachmentsName = new StringBuilder(); List<SendingAttachment> sendingAttachments = new ArrayList<>(attachments.length); for (EmailAttachment ea : attachments) { attachmentsName.append(ea.getName()).append(";"); SendingAttachment sendingAttachment = toSendingAttachment(ea); sendingAttachment.setMessage(sendingMessage); sendingAttachments.add(sendingAttachment); } sendingMessage.setAttachments(sendingAttachments); sendingMessage.setAttachmentsName(attachmentsName.toString()); } else { sendingMessage.setAttachments(Collections.<SendingAttachment>emptyList()); } if (headers != null && !headers.isEmpty()) { StringBuilder headersLine = new StringBuilder(); for (EmailHeader header : headers) { headersLine.append(header.toString()).append(SendingMessage.HEADERS_SEPARATOR); } sendingMessage.setHeaders(headersLine.toString()); } else { sendingMessage.setHeaders(null); } replaceRecipientIfNecessary(sendingMessage); return sendingMessage; } protected void replaceRecipientIfNecessary(SendingMessage msg) { if (config.getSendAllToAdmin()) { String adminAddress = config.getAdminAddress(); log.warn(String.format("Replacing actual email recipient '%s' by admin address '%s'", msg.getAddress(), adminAddress)); msg.setAddress(adminAddress); } } protected SendingAttachment toSendingAttachment(EmailAttachment ea) { SendingAttachment sendingAttachment = metadata.create(SendingAttachment.class); sendingAttachment.setContent(ea.getData()); sendingAttachment.setContentId(ea.getContentId()); sendingAttachment.setName(ea.getName()); sendingAttachment.setEncoding(ea.getEncoding()); sendingAttachment.setDisposition(ea.getDisposition()); return sendingAttachment; } protected byte[] bodyTextToBytes(SendingMessage message) { byte[] bodyBytes = message.getContentText().getBytes(StandardCharsets.UTF_8); return bodyBytes; } protected String bodyTextFromByteArray(byte[] bodyContent) { return new String(bodyContent, StandardCharsets.UTF_8); } protected boolean isNeedToRetry(Exception e) { if (e instanceof MailSendException) { if (e.getCause() instanceof SMTPAddressFailedException) { return false; } } else if (e instanceof AddressException) { return false; } return true; } @Override public void migrateEmailsToFileStorage(List<SendingMessage> messages) { try (Transaction tx = persistence.createTransaction()) { EntityManager em = persistence.getEntityManager(); for (SendingMessage msg : messages) { migrateMessage(em, msg); } tx.commit(); } } @Override public void migrateAttachmentsToFileStorage(List<SendingAttachment> attachments) { try (Transaction tx = persistence.createTransaction()) { EntityManager em = persistence.getEntityManager(); for (SendingAttachment attachment : attachments) { migrateAttachment(em, attachment); } tx.commit(); } } protected void migrateMessage(EntityManager em, SendingMessage msg) { msg = em.merge(msg); byte[] bodyBytes = bodyTextToBytes(msg); FileDescriptor bodyFile = createBodyFileDescriptor(msg, bodyBytes); try { fileStorage.saveFile(bodyFile, bodyBytes); } catch (FileStorageException e) { throw new RuntimeException(e); } em.persist(bodyFile); msg.setContentTextFile(bodyFile); msg.setContentText(null); } protected void migrateAttachment(EntityManager em, SendingAttachment attachment) { attachment = em.merge(attachment); FileDescriptor contentFile = createAttachmentFileDescriptor(attachment); try { fileStorage.saveFile(contentFile, attachment.getContent()); } catch (FileStorageException e) { throw new RuntimeException(e); } em.persist(contentFile); attachment.setContentFile(contentFile); attachment.setContent(null); } protected static class EmailSendTask implements Runnable { private SendingMessage sendingMessage; private static final Logger log = LoggerFactory.getLogger(EmailSendTask.class); public EmailSendTask(SendingMessage message) { sendingMessage = message; } @Override public void run() { try { Authentication authentication = AppBeans.get(Authentication.NAME); Emailer emailer = AppBeans.get(EmailerAPI.NAME); authentication.begin(emailer.getEmailerLogin()); try { emailer.sendSendingMessage(sendingMessage); } finally { authentication.end(); } } catch (Exception e) { log.error("Exception while sending email: ", e); } } } protected static class MessagePersistingContext { public final List<FileDescriptor> files = new ArrayList<>(); public void finished() { files.clear(); } } }