mirror of
https://gitee.com/jmix/cuba.git
synced 2024-12-04 20:28:00 +08:00
ability to store emails and attachments in file storage #PL-2368, email history: removed split #PL-2470
This commit is contained in:
parent
b842df0fc3
commit
c6612c8f2b
@ -630,6 +630,7 @@ create table SYS_SENDING_MESSAGE (
|
||||
ADDRESS_FROM varchar(100),
|
||||
CAPTION varchar(500),
|
||||
CONTENT_TEXT longvarchar,
|
||||
CONTENT_TEXT_FILE_ID varchar(36),
|
||||
DEADLINE timestamp,
|
||||
STATUS integer,
|
||||
DATE_SENT timestamp,
|
||||
@ -640,6 +641,8 @@ create table SYS_SENDING_MESSAGE (
|
||||
primary key (ID)
|
||||
)^
|
||||
|
||||
alter table SYS_SENDING_MESSAGE add constraint FK_SYS_SENDING_MESSAGE_CONTENT_FILE foreign key (CONTENT_TEXT_FILE_ID) references SYS_FILE(ID)^
|
||||
|
||||
create table SYS_SENDING_ATTACHMENT (
|
||||
ID varchar(36) not null,
|
||||
CREATE_TS timestamp,
|
||||
@ -652,6 +655,7 @@ create table SYS_SENDING_ATTACHMENT (
|
||||
--
|
||||
MESSAGE_ID varchar(36),
|
||||
CONTENT longvarbinary,
|
||||
CONTENT_FILE_ID varchar(36),
|
||||
CONTENT_ID varchar(50),
|
||||
NAME varchar(500),
|
||||
DISPOSITION varchar(50),
|
||||
@ -662,6 +666,7 @@ create table SYS_SENDING_ATTACHMENT (
|
||||
)^
|
||||
|
||||
alter table SYS_SENDING_ATTACHMENT add constraint FK_SYS_SENDING_ATTACHMENT_SENDING_MESSAGE foreign key (MESSAGE_ID) references SYS_SENDING_MESSAGE (ID)^
|
||||
alter table SYS_SENDING_ATTACHMENT add constraint FK_SYS_SENDING_ATTACHMENT_CONTENT_FILE foreign key (CONTENT_FILE_ID) references SYS_FILE (ID)^
|
||||
|
||||
CREATE INDEX SYS_SENDING_ATTACHMENT_MESSAGE_IDX
|
||||
ON SYS_SENDING_ATTACHMENT(MESSAGE_ID )^
|
||||
|
@ -612,6 +612,7 @@ create table SYS_SENDING_MESSAGE (
|
||||
ADDRESS_FROM varchar(100),
|
||||
CAPTION varchar(500),
|
||||
CONTENT_TEXT varchar(max),
|
||||
CONTENT_TEXT_FILE_ID uniqueidentifier,
|
||||
DEADLINE datetime,
|
||||
STATUS int,
|
||||
DATE_SENT datetime,
|
||||
@ -619,7 +620,8 @@ create table SYS_SENDING_MESSAGE (
|
||||
ATTEMPTS_MADE int,
|
||||
ATTACHMENTS_NAME varchar(500),
|
||||
--
|
||||
primary key nonclustered (ID)
|
||||
primary key nonclustered (ID),
|
||||
constraint FK_SYS_SENDING_MESSAGE_CONTENT_FILE foreign key (CONTENT_TEXT_FILE_ID) references SYS_FILE(ID)
|
||||
)^
|
||||
|
||||
create index IDX_SYS_SENDING_MESSAGE_STATUS on SYS_SENDING_MESSAGE (STATUS)^
|
||||
@ -642,6 +644,7 @@ create table SYS_SENDING_ATTACHMENT (
|
||||
--
|
||||
MESSAGE_ID uniqueidentifier,
|
||||
CONTENT image,
|
||||
CONTENT_FILE_ID uniqueidentifier,
|
||||
CONTENT_ID varchar(50),
|
||||
NAME varchar(500),
|
||||
DISPOSITION varchar(50),
|
||||
@ -649,7 +652,8 @@ create table SYS_SENDING_ATTACHMENT (
|
||||
|
||||
--
|
||||
primary key nonclustered (ID),
|
||||
constraint FK_SYS_SENDING_ATTACHMENT_SENDING_MESSAGE foreign key (MESSAGE_ID) references SYS_SENDING_MESSAGE (ID)
|
||||
constraint FK_SYS_SENDING_ATTACHMENT_SENDING_MESSAGE foreign key (MESSAGE_ID) references SYS_SENDING_MESSAGE (ID),
|
||||
constraint FK_SYS_SENDING_ATTACHMENT_CONTENT_FILE foreign key (CONTENT_FILE_ID) references SYS_FILE (ID)
|
||||
)^
|
||||
|
||||
create index SYS_SENDING_ATTACHMENT_MESSAGE_IDX on SYS_SENDING_ATTACHMENT (MESSAGE_ID)^
|
||||
|
@ -257,6 +257,7 @@ create table SYS_SENDING_ATTACHMENT (
|
||||
MESSAGE_ID varchar2(32),
|
||||
CONTENT blob,
|
||||
CONTENT_ID varchar2(50),
|
||||
CONTENT_FILE_ID varchar2(32),
|
||||
NAME varchar2(500),
|
||||
DISPOSITION varchar2(50),
|
||||
TEXT_ENCODING varchar2(50),
|
||||
@ -278,6 +279,7 @@ create table SYS_SENDING_MESSAGE (
|
||||
ADDRESS_FROM varchar2(100),
|
||||
CAPTION varchar2(500),
|
||||
CONTENT_TEXT clob,
|
||||
CONTENT_TEXT_FILE_ID varchar2(32),
|
||||
DEADLINE timestamp,
|
||||
STATUS integer,
|
||||
DATE_SENT timestamp,
|
||||
@ -586,8 +588,12 @@ alter table SYS_FOLDER add constraint FK_SYS_FOLDER_PARENT foreign key (PARENT_I
|
||||
|
||||
alter table SYS_SCHEDULED_EXECUTION add constraint SYS_SCHEDULED_EXECUTION_TASK foreign key (TASK_ID) references SYS_SCHEDULED_TASK(ID)^
|
||||
|
||||
alter table SYS_SENDING_MESSAGE add constraint FK_SYS_SENDING_MESSAGE_FILE foreign key (CONTENT_TEXT_FILE_ID) references SYS_FILE(ID)^
|
||||
|
||||
alter table SYS_SENDING_ATTACHMENT add constraint FK_SYS_SENDING_ATT_SEN_MES foreign key (MESSAGE_ID) references SYS_SENDING_MESSAGE(ID)^
|
||||
|
||||
alter table SYS_SENDING_ATTACHMENT add constraint FK_SYS_SENDING_ATTACH_FILE foreign key (CONTENT_FILE_ID) references SYS_FILE (ID)^
|
||||
|
||||
alter table SEC_CONSTRAINT add constraint SEC_CONSTRAINT_GROUP foreign key (GROUP_ID) references SEC_GROUP(ID)^
|
||||
|
||||
alter table SEC_ENTITY_LOG add constraint FK_SEC_ENTITY_LOG_USER foreign key (USER_ID) references SEC_USER(ID)^
|
||||
|
@ -590,6 +590,7 @@ create table SYS_SENDING_MESSAGE (
|
||||
ADDRESS_FROM varchar(100),
|
||||
CAPTION varchar(500),
|
||||
CONTENT_TEXT text,
|
||||
CONTENT_TEXT_FILE_ID uuid,
|
||||
DEADLINE timestamp,
|
||||
STATUS int,
|
||||
DATE_SENT timestamp,
|
||||
@ -597,7 +598,8 @@ create table SYS_SENDING_MESSAGE (
|
||||
ATTEMPTS_MADE int,
|
||||
ATTACHMENTS_NAME varchar(500),
|
||||
--
|
||||
primary key (ID)
|
||||
primary key (ID),
|
||||
constraint FK_SYS_SENDING_MESSAGE_CONTENT_FILE foreign key (CONTENT_TEXT_FILE_ID) references SYS_FILE(ID)
|
||||
)^
|
||||
|
||||
create index IDX_SYS_SENDING_MESSAGE_STATUS on SYS_SENDING_MESSAGE (STATUS)^
|
||||
@ -618,13 +620,15 @@ create table SYS_SENDING_ATTACHMENT (
|
||||
--
|
||||
MESSAGE_ID uuid,
|
||||
CONTENT bytea,
|
||||
CONTENT_FILE_ID uuid,
|
||||
CONTENT_ID varchar(50),
|
||||
NAME varchar(500),
|
||||
DISPOSITION varchar(50),
|
||||
TEXT_ENCODING varchar(50),
|
||||
--
|
||||
primary key (ID),
|
||||
constraint FK_SYS_SENDING_ATTACHMENT_SENDING_MESSAGE foreign key (MESSAGE_ID) references SYS_SENDING_MESSAGE (ID)
|
||||
constraint FK_SYS_SENDING_ATTACHMENT_SENDING_MESSAGE foreign key (MESSAGE_ID) references SYS_SENDING_MESSAGE (ID),
|
||||
constraint FK_SYS_SENDING_ATTACHMENT_CONTENT_FILE foreign key (CONTENT_FILE_ID) references SYS_FILE (ID)
|
||||
)^
|
||||
|
||||
create index SYS_SENDING_ATTACHMENT_MESSAGE_IDX on SYS_SENDING_ATTACHMENT (MESSAGE_ID)^
|
||||
|
@ -0,0 +1,10 @@
|
||||
-- $Id$ --
|
||||
-- Add linking columns from SendingMessage and SendingAttachment to sys$File
|
||||
|
||||
alter table SYS_SENDING_MESSAGE add column CONTENT_TEXT_FILE_ID uniqueidentifier^
|
||||
alter table SYS_SENDING_MESSAGE add constraint FK_SYS_SENDING_MESSAGE_CONTENT_FILE
|
||||
foreign key (CONTENT_TEXT_FILE_ID) references SYS_FILE(ID)^
|
||||
|
||||
alter table SYS_SENDING_ATTACHMENT add column CONTENT_FILE_ID uniqueidentifier^
|
||||
alter table SYS_SENDING_ATTACHMENT add constraint FK_SYS_SENDING_ATTACHMENT_CONTENT_FILE
|
||||
foreign key (CONTENT_FILE_ID) references SYS_FILE (ID)^
|
@ -0,0 +1,10 @@
|
||||
-- $Id$ --
|
||||
-- Add linking columns from SendingMessage and SendingAttachment to sys$File
|
||||
|
||||
alter table SYS_SENDING_MESSAGE add column CONTENT_TEXT_FILE_ID uuid;
|
||||
alter table SYS_SENDING_MESSAGE ADD constraint FK_SYS_SENDING_MESSAGE_CONTENT_FILE
|
||||
foreign key (CONTENT_TEXT_FILE_ID) references SYS_FILE(ID);
|
||||
|
||||
alter table SYS_SENDING_ATTACHMENT add column CONTENT_FILE_ID uuid;
|
||||
alter table SYS_SENDING_ATTACHMENT add constraint FK_SYS_SENDING_ATTACHMENT_CONTENT_FILE
|
||||
foreign key (CONTENT_FILE_ID) references SYS_FILE (ID);
|
@ -77,7 +77,6 @@ public class EmailSender implements EmailSenderAPI {
|
||||
textBodyPart.setContent(textPart);
|
||||
content.addBodyPart(textBodyPart);
|
||||
|
||||
if (sendingMessage.getAttachments() != null) {
|
||||
for (SendingAttachment attachment : sendingMessage.getAttachments()) {
|
||||
MimeBodyPart attachmentPart = createAttachmentPart(attachment);
|
||||
|
||||
@ -86,7 +85,6 @@ public class EmailSender implements EmailSenderAPI {
|
||||
} else
|
||||
textPart.addBodyPart(attachmentPart);
|
||||
}
|
||||
}
|
||||
|
||||
msg.setContent(content);
|
||||
msg.saveChanges();
|
||||
|
@ -22,6 +22,7 @@ public interface EmailSenderAPI {
|
||||
|
||||
/**
|
||||
* Sends email with help of {@link org.springframework.mail.javamail.JavaMailSender}.
|
||||
* Message body and attachments' content must be loaded from file storage.
|
||||
* <p/>
|
||||
* Use {@link EmailerAPI} instead if you need email to be delivered reliably and stored to email history.
|
||||
*
|
||||
|
@ -4,6 +4,7 @@
|
||||
*/
|
||||
package com.haulmont.cuba.core.app;
|
||||
|
||||
import com.haulmont.cuba.core.entity.SendingMessage;
|
||||
import com.haulmont.cuba.core.global.EmailAttachment;
|
||||
import com.haulmont.cuba.core.global.EmailException;
|
||||
import com.haulmont.cuba.core.global.EmailInfo;
|
||||
@ -39,4 +40,9 @@ public class EmailServiceBean implements EmailService {
|
||||
public void sendEmailAsync(EmailInfo info) {
|
||||
emailer.sendEmailAsync(info);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String loadContentText(SendingMessage sendingMessage) {
|
||||
return emailer.loadContentText(sendingMessage);
|
||||
}
|
||||
}
|
||||
|
@ -4,15 +4,18 @@
|
||||
*/
|
||||
package com.haulmont.cuba.core.app;
|
||||
|
||||
import com.haulmont.chile.core.model.utils.InstanceUtils;
|
||||
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 org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.commons.lang.time.DateUtils;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@ -23,6 +26,7 @@ import javax.annotation.Nullable;
|
||||
import javax.annotation.Resource;
|
||||
import javax.inject.Inject;
|
||||
import java.io.Serializable;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
|
||||
@ -33,6 +37,9 @@ import java.util.concurrent.RejectedExecutionException;
|
||||
@ManagedBean(EmailerAPI.NAME)
|
||||
public class Emailer implements EmailerAPI {
|
||||
|
||||
protected static final String BODY_STORAGE_ENCODING = "UTF-8";
|
||||
protected static final String BODY_FILE_EXTENSION = "txt";
|
||||
|
||||
private Log log = LogFactory.getLog(Emailer.class);
|
||||
|
||||
protected EmailerConfig config;
|
||||
@ -63,6 +70,9 @@ public class Emailer implements EmailerAPI {
|
||||
@Inject
|
||||
protected Resources resources;
|
||||
|
||||
@Inject
|
||||
protected FileStorageAPI fileStorage;
|
||||
|
||||
@Inject
|
||||
public void setConfig(Configuration configuration) {
|
||||
this.config = configuration.getConfig(EmailerConfig.class);
|
||||
@ -167,27 +177,59 @@ public class Emailer implements EmailerAPI {
|
||||
List<String> errorMessages = new ArrayList<>();
|
||||
|
||||
for (SendingMessage sendingMessage : messages) {
|
||||
persistMessages(Collections.singletonList(sendingMessage), SendingStatus.SENDING);
|
||||
SendingMessage persistedMessage = persistMessageIfPossible(sendingMessage);
|
||||
|
||||
try {
|
||||
emailSender.sendEmail(sendingMessage);
|
||||
markAsSent(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());
|
||||
markAsNonSent(sendingMessage);
|
||||
if (persistedMessage != null) {
|
||||
markAsNonSent(persistedMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!failedAddresses.isEmpty()) {
|
||||
throw new EmailException(
|
||||
failedAddresses.toArray(new String[failedAddresses.size()]),
|
||||
errorMessages.toArray(new String[errorMessages.size()])
|
||||
);
|
||||
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.<SendingMessage>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 = (SendingMessage) InstanceUtils.copy(srcMessage);
|
||||
List<SendingAttachment> clonedList = new ArrayList<>();
|
||||
for (SendingAttachment srcAttach : srcMessage.getAttachments()) {
|
||||
SendingAttachment clonedAttach = (SendingAttachment) InstanceUtils.copy(srcAttach);
|
||||
clonedAttach.setMessage(null);
|
||||
clonedAttach.setMessage(clonedMessage);
|
||||
clonedList.add(clonedAttach);
|
||||
}
|
||||
clonedMessage.setAttachments(clonedList);
|
||||
return clonedMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String processQueuedEmails() {
|
||||
int callsToSkip = config.getDelayCallCount();
|
||||
@ -281,30 +323,147 @@ public class Emailer implements EmailerAPI {
|
||||
}
|
||||
}
|
||||
tx.commit();
|
||||
return emailsToSend;
|
||||
} finally {
|
||||
tx.end();
|
||||
}
|
||||
|
||||
for (SendingMessage message : emailsToSend) {
|
||||
loadBodyAndAttachments(message);
|
||||
}
|
||||
return emailsToSend;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String loadContentText(SendingMessage sendingMessage) {
|
||||
SendingMessage msg;
|
||||
Transaction tx = persistence.createTransaction();
|
||||
try {
|
||||
EntityManager em = persistence.getEntityManager();
|
||||
msg = em.reload(sendingMessage, "sendingMessage.loadContentText");
|
||||
tx.commit();
|
||||
} finally {
|
||||
tx.end();
|
||||
}
|
||||
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);
|
||||
}
|
||||
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 {
|
||||
Transaction tx = persistence.createTransaction();
|
||||
try {
|
||||
EntityManager em = persistence.getEntityManager();
|
||||
for (SendingMessage message : sendingMessageList) {
|
||||
message.setStatus(status);
|
||||
|
||||
em.persist(message);
|
||||
if (message.getAttachments() != null) {
|
||||
for (SendingAttachment attachment : message.getAttachments()) {
|
||||
em.persist(attachment);
|
||||
}
|
||||
try {
|
||||
persistSendingMessage(em, message, context);
|
||||
} catch (FileStorageException e) {
|
||||
throw new RuntimeException("Failed to store message " + message.getCaption(), e);
|
||||
}
|
||||
}
|
||||
tx.commit();
|
||||
} finally {
|
||||
tx.end();
|
||||
}
|
||||
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(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(bodyBytes.length);
|
||||
return contentTextFile;
|
||||
}
|
||||
|
||||
protected void returnToQueue(SendingMessage sendingMessage) {
|
||||
@ -378,6 +537,8 @@ public class Emailer implements EmailerAPI {
|
||||
}
|
||||
sendingMessage.setAttachments(sendingAttachments);
|
||||
sendingMessage.setAttachmentsName(attachmentsName.toString());
|
||||
} else {
|
||||
sendingMessage.setAttachments(Collections.<SendingAttachment>emptyList());
|
||||
}
|
||||
|
||||
replaceRecipientIfNecessary(sendingMessage);
|
||||
@ -405,6 +566,86 @@ public class Emailer implements EmailerAPI {
|
||||
return sendingAttachment;
|
||||
}
|
||||
|
||||
protected byte[] bodyTextToBytes(SendingMessage message) {
|
||||
byte[] bodyBytes;
|
||||
try {
|
||||
bodyBytes = message.getContentText().getBytes(BODY_STORAGE_ENCODING);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return bodyBytes;
|
||||
}
|
||||
|
||||
|
||||
protected String bodyTextFromByteArray(byte[] bodyContent) {
|
||||
try {
|
||||
return new String(bodyContent, BODY_STORAGE_ENCODING);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void migrateEmailsToFileStorage(List<SendingMessage> messages) {
|
||||
Transaction tx = persistence.createTransaction();
|
||||
try {
|
||||
EntityManager em = persistence.getEntityManager();
|
||||
|
||||
for (SendingMessage msg : messages) {
|
||||
migrateMessage(em, msg);
|
||||
}
|
||||
tx.commit();
|
||||
} finally {
|
||||
tx.end();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void migrateAttachmentsToFileStorage(List<SendingAttachment> attachments) {
|
||||
Transaction tx = persistence.createTransaction();
|
||||
try {
|
||||
EntityManager em = persistence.getEntityManager();
|
||||
|
||||
for (SendingAttachment attachment : attachments) {
|
||||
migrateAttachment(em, attachment);
|
||||
}
|
||||
|
||||
tx.commit();
|
||||
} finally {
|
||||
tx.end();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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;
|
||||
@ -431,4 +672,12 @@ public class Emailer implements EmailerAPI {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected static class MessagePersistingContext {
|
||||
public final List<FileDescriptor> files = new ArrayList<>();
|
||||
|
||||
public void finished() {
|
||||
files.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
*/
|
||||
package com.haulmont.cuba.core.app;
|
||||
|
||||
import com.haulmont.cuba.core.entity.SendingAttachment;
|
||||
import com.haulmont.cuba.core.entity.SendingMessage;
|
||||
import com.haulmont.cuba.core.global.EmailAttachment;
|
||||
import com.haulmont.cuba.core.global.EmailException;
|
||||
@ -54,7 +55,6 @@ public interface EmailerAPI {
|
||||
* @param info email details
|
||||
* @param attemptsCount count of attempts to send (1 attempt per scheduler tick). If not specified,
|
||||
* {@link com.haulmont.cuba.core.app.EmailerConfig#getDefaultSendingAttemptsCount()} is used
|
||||
*
|
||||
* @param deadline Emailer tries to send message till deadline.
|
||||
* If deadline has come and message has not been sent, status of this message is changed to
|
||||
* {@link com.haulmont.cuba.core.global.SendingStatus#NOTSENT}
|
||||
@ -77,4 +77,21 @@ public interface EmailerAPI {
|
||||
* @return short message describing how many emails were sent, or error message
|
||||
*/
|
||||
String processQueuedEmails();
|
||||
|
||||
/**
|
||||
* Migrate list of existing messages to be stored in file storage, in a single transaction.
|
||||
*/
|
||||
void migrateEmailsToFileStorage(List<SendingMessage> messages);
|
||||
|
||||
/**
|
||||
* Migrate list of existing email attachments to be stored in file storage, in a single transaction.
|
||||
*/
|
||||
void migrateAttachmentsToFileStorage(List<SendingAttachment> attachments);
|
||||
|
||||
/**
|
||||
* Loads content text for given message.
|
||||
*
|
||||
* @return email content text
|
||||
*/
|
||||
String loadContentText(SendingMessage sendingMessage);
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ public interface EmailerConfig extends Config {
|
||||
@Property("cuba.email.fromAddress")
|
||||
@Default("DoNotReply@localhost")
|
||||
String getFromAddress();
|
||||
|
||||
void setFromAddress(String fromAddress);
|
||||
|
||||
/**
|
||||
@ -77,6 +78,7 @@ public interface EmailerConfig extends Config {
|
||||
@Property("cuba.email.delayCallCount")
|
||||
@Default("2")
|
||||
int getDelayCallCount();
|
||||
|
||||
void setDelayCallCount(int delayCallCount);
|
||||
|
||||
/**
|
||||
@ -107,13 +109,29 @@ public interface EmailerConfig extends Config {
|
||||
@Property("cuba.email.adminAddress")
|
||||
@Default("admin@localhost")
|
||||
String getAdminAddress();
|
||||
|
||||
void setAdminAddress(String adminAddress);
|
||||
|
||||
/**
|
||||
* If this parameter is set to true, all email messages go to <code>cuba.email.adminAddress</code>.
|
||||
* If this parameter is set to true, all email messages go to {@link #getAdminAddress()}.
|
||||
*/
|
||||
@Property("cuba.email.sendAllToAdmin")
|
||||
@DefaultBoolean(false)
|
||||
boolean getSendAllToAdmin();
|
||||
|
||||
void setSendAllToAdmin(boolean sendAllToAdmin);
|
||||
|
||||
/**
|
||||
* When turned on, email body text and attachments will be stored in file storage
|
||||
* instead of BLOB columns in database.
|
||||
* Should be used if application stores lots of emails and/or email attachments.
|
||||
*
|
||||
* @see com.haulmont.cuba.core.entity.SendingMessage#contentTextFile
|
||||
* @see com.haulmont.cuba.core.entity.SendingAttachment#contentFile
|
||||
*/
|
||||
@Property("cuba.email.useFileStorage")
|
||||
@DefaultBoolean(false)
|
||||
boolean isFileStorageUsed();
|
||||
|
||||
void setFileStorageUsed(boolean fileStorageUsed);
|
||||
}
|
||||
|
@ -5,15 +5,25 @@
|
||||
|
||||
package com.haulmont.cuba.core.jmx;
|
||||
|
||||
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.app.EmailerAPI;
|
||||
import com.haulmont.cuba.core.app.EmailerConfig;
|
||||
import com.haulmont.cuba.core.entity.SendingAttachment;
|
||||
import com.haulmont.cuba.core.entity.SendingMessage;
|
||||
import com.haulmont.cuba.core.global.Configuration;
|
||||
import com.haulmont.cuba.core.global.EmailAttachment;
|
||||
import com.haulmont.cuba.core.global.View;
|
||||
import com.haulmont.cuba.security.app.Authenticated;
|
||||
import org.apache.commons.lang.exception.ExceptionUtils;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import javax.annotation.ManagedBean;
|
||||
import javax.inject.Inject;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author krivopustov
|
||||
@ -25,8 +35,13 @@ public class Emailer implements EmailerMBean {
|
||||
@Inject
|
||||
protected EmailerAPI emailer;
|
||||
|
||||
@Inject
|
||||
protected Persistence persistence;
|
||||
|
||||
protected EmailerConfig config;
|
||||
|
||||
protected Log log = LogFactory.getLog(Emailer.class);
|
||||
|
||||
@Inject
|
||||
public void setConfiguration(Configuration configuration) {
|
||||
this.config = configuration.getConfig(EmailerConfig.class);
|
||||
@ -82,4 +97,80 @@ public class Emailer implements EmailerMBean {
|
||||
return ExceptionUtils.getStackTrace(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String migrateEmailsToFileStorage(String password) {
|
||||
if (!"do migration".equals(password)) {
|
||||
return "Wrong password";
|
||||
}
|
||||
|
||||
int processed;
|
||||
do {
|
||||
try {
|
||||
processed = migrateMessagesBatch();
|
||||
log.info(String.format("Migrated %d emails", processed));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to migrate batch", e);
|
||||
}
|
||||
} while (processed > 0);
|
||||
log.info("Finished migrating emails");
|
||||
do {
|
||||
try {
|
||||
processed = migrateAttachmentsBatch();
|
||||
log.info(String.format("Migrated %d attachments", processed));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to migrate batch", e);
|
||||
}
|
||||
} while (processed > 0);
|
||||
log.info("Finished migrating attachments");
|
||||
|
||||
return "Finished";
|
||||
}
|
||||
|
||||
protected int migrateMessagesBatch() {
|
||||
List<SendingMessage> resultList;
|
||||
Transaction tx = persistence.createTransaction();
|
||||
try {
|
||||
EntityManager em = persistence.getEntityManager();
|
||||
String qstr = "select m from sys$SendingMessage m where m.contentText is not null";
|
||||
TypedQuery<SendingMessage> query = em.createQuery(qstr, SendingMessage.class);
|
||||
query.setMaxResults(50);
|
||||
query.setViewName(View.MINIMAL);
|
||||
|
||||
resultList = query.getResultList();
|
||||
tx.commit();
|
||||
} finally {
|
||||
tx.end();
|
||||
}
|
||||
|
||||
if (!resultList.isEmpty()) {
|
||||
emailer.migrateEmailsToFileStorage(resultList);
|
||||
}
|
||||
|
||||
return resultList.size();
|
||||
}
|
||||
|
||||
protected int migrateAttachmentsBatch() {
|
||||
List<SendingAttachment> resultList;
|
||||
Transaction tx = persistence.createTransaction();
|
||||
try {
|
||||
EntityManager em = persistence.getEntityManager();
|
||||
String qstr = "select a from sys$SendingAttachment a where a.content is not null";
|
||||
TypedQuery<SendingAttachment> query = em.createQuery(qstr, SendingAttachment.class);
|
||||
query.setMaxResults(50);
|
||||
query.setViewName(View.MINIMAL);
|
||||
|
||||
resultList = query.getResultList();
|
||||
|
||||
tx.commit();
|
||||
} finally {
|
||||
tx.end();
|
||||
}
|
||||
|
||||
if (!resultList.isEmpty()) {
|
||||
emailer.migrateAttachmentsToFileStorage(resultList);
|
||||
}
|
||||
|
||||
return resultList.size();
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
*/
|
||||
package com.haulmont.cuba.core.jmx;
|
||||
|
||||
import org.springframework.jmx.export.annotation.ManagedOperation;
|
||||
import org.springframework.jmx.export.annotation.ManagedOperationParameter;
|
||||
import org.springframework.jmx.export.annotation.ManagedOperationParameters;
|
||||
|
||||
@ -28,4 +29,7 @@ public interface EmailerMBean {
|
||||
|
||||
@ManagedOperationParameters({@ManagedOperationParameter(name = "addresses", description = "")})
|
||||
String sendTestEmail(String addresses);
|
||||
|
||||
@ManagedOperation(description = "Migrate existing email history to use file storage")
|
||||
String migrateEmailsToFileStorage(String password);
|
||||
}
|
||||
|
@ -151,20 +151,20 @@
|
||||
<property name="updateTs"/>
|
||||
</view>
|
||||
|
||||
<view class="com.haulmont.cuba.core.entity.SendingMessage" name="sendingMessage.browse.detail"
|
||||
extends="sendingMessage.browse">
|
||||
<property name="contentText"/>
|
||||
</view>
|
||||
|
||||
<view class="com.haulmont.cuba.core.entity.SendingMessage" name="sendingMessage.loadFromQueue" extends="_local"
|
||||
systemProperties="true">
|
||||
<property name="attachments" view="sendingAttachment.loadFromQueue"/>
|
||||
<property name="version"/>
|
||||
<property name="contentTextFile" view="_local"/>
|
||||
</view>
|
||||
|
||||
<view class="com.haulmont.cuba.core.entity.SendingAttachment" name="sendingAttachment.loadFromQueue"
|
||||
extends="_local" systemProperties="true">
|
||||
<property name="contentFile" view="_local"/>
|
||||
</view>
|
||||
|
||||
<view class="com.haulmont.cuba.core.entity.SendingMessage" name="sendingMessage.loadContentText">
|
||||
<property name="contentTextFile" view="_local"/>
|
||||
<property name="contentText"/>
|
||||
</view>
|
||||
|
||||
<view class="com.haulmont.cuba.core.entity.CategoryAttributeValue" name="categoryAttributeValue" extends="_local">
|
||||
|
@ -5,8 +5,10 @@
|
||||
|
||||
package com.haulmont.cuba.core.app;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.haulmont.cuba.core.CubaTestCase;
|
||||
import com.haulmont.cuba.core.Transaction;
|
||||
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.CubaMailSender;
|
||||
@ -53,10 +55,19 @@ public class EmailerTest extends CubaTestCase {
|
||||
testMailSender.clearBuffer();
|
||||
}
|
||||
|
||||
public void testSynchronous() throws Exception {
|
||||
doTestSynchronous(false);
|
||||
}
|
||||
|
||||
public void testSynchronousFS() throws Exception {
|
||||
doTestSynchronous(true);
|
||||
}
|
||||
|
||||
/*
|
||||
* Test single recipient, text body, subject.
|
||||
*/
|
||||
public void testSynchronous() throws Exception {
|
||||
private void doTestSynchronous(boolean useFs) throws Exception {
|
||||
emailerConfig.setFileStorageUsed(useFs);
|
||||
testMailSender.clearBuffer();
|
||||
|
||||
EmailInfo myInfo = new EmailInfo("testemail@example.com", "Test Email", "Test Body");
|
||||
@ -93,6 +104,15 @@ public class EmailerTest extends CubaTestCase {
|
||||
}
|
||||
|
||||
public void testAsynchronous() throws Exception {
|
||||
doTestAsynchronous(false);
|
||||
}
|
||||
|
||||
public void testAsynchronousFS() throws Exception {
|
||||
doTestAsynchronous(true);
|
||||
}
|
||||
|
||||
private void doTestAsynchronous(boolean useFs) throws Exception {
|
||||
emailerConfig.setFileStorageUsed(useFs);
|
||||
testMailSender.clearBuffer();
|
||||
|
||||
String body = "Test Email Body";
|
||||
@ -183,8 +203,8 @@ public class EmailerTest extends CubaTestCase {
|
||||
emailer.sendEmail("myemail@example.com", "Test Email", "Test Body 2");
|
||||
fail("Must fail with EmailException");
|
||||
} catch (EmailException e) {
|
||||
assertEquals(1, e.getFailedAddresses().length);
|
||||
assertEquals("myemail@example.com", e.getFailedAddresses()[0]);
|
||||
assertEquals(1, e.getFailedAddresses().size());
|
||||
assertEquals("myemail@example.com", e.getFailedAddresses().get(0));
|
||||
assertTrue(testMailSender.isEmpty());
|
||||
} finally {
|
||||
testMailSender.workNormallyPlease();
|
||||
@ -229,6 +249,15 @@ public class EmailerTest extends CubaTestCase {
|
||||
}
|
||||
|
||||
public void testSentFromSecondAttempt() throws Exception {
|
||||
doTestSentFromSecondAttempt(false);
|
||||
}
|
||||
|
||||
public void testSentFromSecondAttemptFS() throws Exception {
|
||||
doTestSentFromSecondAttempt(true);
|
||||
}
|
||||
|
||||
private void doTestSentFromSecondAttempt(boolean useFs) {
|
||||
emailerConfig.setFileStorageUsed(useFs);
|
||||
testMailSender.clearBuffer();
|
||||
|
||||
String body = "Test Email Body";
|
||||
@ -261,6 +290,15 @@ public class EmailerTest extends CubaTestCase {
|
||||
}
|
||||
|
||||
public void testSeveralRecipients() throws Exception {
|
||||
doTestSeveralRecipients(false);
|
||||
}
|
||||
|
||||
public void testSeveralRecipientsFS() throws Exception {
|
||||
doTestSeveralRecipients(true);
|
||||
}
|
||||
|
||||
private void doTestSeveralRecipients(boolean useFs) throws MessagingException {
|
||||
emailerConfig.setFileStorageUsed(useFs);
|
||||
testMailSender.clearBuffer();
|
||||
|
||||
String body = "Test Email Body";
|
||||
@ -325,6 +363,15 @@ public class EmailerTest extends CubaTestCase {
|
||||
}
|
||||
|
||||
public void testTextAttachment() throws Exception {
|
||||
doTestTextAttachment(false);
|
||||
}
|
||||
|
||||
public void testTextAttachmentFS() throws Exception {
|
||||
doTestTextAttachment(true);
|
||||
}
|
||||
|
||||
private void doTestTextAttachment(boolean useFs) throws IOException, MessagingException {
|
||||
emailerConfig.setFileStorageUsed(useFs);
|
||||
testMailSender.clearBuffer();
|
||||
|
||||
String attachmentText = "Test Attachment Text";
|
||||
@ -353,6 +400,15 @@ public class EmailerTest extends CubaTestCase {
|
||||
}
|
||||
|
||||
public void testInlineImage() throws Exception {
|
||||
doTestInlineImage(false);
|
||||
}
|
||||
|
||||
public void testInlineImageFS() throws Exception {
|
||||
doTestInlineImage(true);
|
||||
}
|
||||
|
||||
private void doTestInlineImage(boolean useFs) throws IOException, MessagingException {
|
||||
emailerConfig.setFileStorageUsed(useFs);
|
||||
testMailSender.clearBuffer();
|
||||
|
||||
byte[] imageBytes = new byte[]{1, 2, 3, 4, 5};
|
||||
@ -381,13 +437,22 @@ public class EmailerTest extends CubaTestCase {
|
||||
}
|
||||
|
||||
public void testPdfAttachment() throws Exception {
|
||||
doTestPdfAttachment(false);
|
||||
}
|
||||
|
||||
public void testPdfAttachmentFS() throws Exception {
|
||||
doTestPdfAttachment(true);
|
||||
}
|
||||
|
||||
private void doTestPdfAttachment(boolean useFs) throws IOException, MessagingException {
|
||||
emailerConfig.setFileStorageUsed(useFs);
|
||||
testMailSender.clearBuffer();
|
||||
|
||||
byte[] pdfBytes = new byte[]{1, 2, 3, 4, 6};
|
||||
String fileName = "invoice.pdf";
|
||||
EmailAttachment imageAttach = new EmailAttachment(pdfBytes, fileName);
|
||||
EmailAttachment pdfAttach = new EmailAttachment(pdfBytes, fileName);
|
||||
|
||||
EmailInfo myInfo = new EmailInfo("test@example.com", "Test", null, "Test", imageAttach);
|
||||
EmailInfo myInfo = new EmailInfo("test@example.com", "Test", null, "Test", pdfAttach);
|
||||
emailer.sendEmailAsync(myInfo);
|
||||
|
||||
emailer.processQueuedEmails();
|
||||
@ -408,6 +473,68 @@ public class EmailerTest extends CubaTestCase {
|
||||
assertTrue(contentType.contains("application/pdf"));
|
||||
}
|
||||
|
||||
public void testLoadBody() throws Exception {
|
||||
doTestLoadBody(false);
|
||||
}
|
||||
|
||||
public void testLoadBodyFS() throws Exception {
|
||||
doTestLoadBody(true);
|
||||
}
|
||||
|
||||
private void doTestLoadBody(boolean useFs) throws Exception {
|
||||
emailerConfig.setFileStorageUsed(useFs);
|
||||
|
||||
String body = "Hi! This is test email. Bye.";
|
||||
EmailInfo emailInfo = new EmailInfo("test@example.com", "Test", body);
|
||||
List<SendingMessage> messages = emailer.sendEmailAsync(emailInfo);
|
||||
|
||||
SendingMessage msg = reload(messages.get(0));
|
||||
|
||||
String actualBody = emailer.loadContentText(msg);
|
||||
assertEquals(body, actualBody);
|
||||
}
|
||||
|
||||
public void testMigration() throws Exception {
|
||||
emailerConfig.setFileStorageUsed(false);
|
||||
|
||||
byte[] expectedBytes = new byte[]{1, 2, 3, 4, 6};
|
||||
EmailAttachment fileAttachment = new EmailAttachment(expectedBytes, "invoice.pdf");
|
||||
|
||||
String body = "Hi! This is test email. Bye.";
|
||||
EmailInfo emailInfo = new EmailInfo("test@example.com", "Test", body);
|
||||
emailInfo.setAttachments(new EmailAttachment[]{fileAttachment});
|
||||
|
||||
List<SendingMessage> messages = emailer.sendEmailAsync(emailInfo);
|
||||
SendingMessage msg;
|
||||
SendingAttachment attachment;
|
||||
|
||||
// check DB storage
|
||||
msg = reload(messages.get(0), "sendingMessage.loadFromQueue");
|
||||
attachment = msg.getAttachments().get(0);
|
||||
|
||||
assertNotNull(msg.getContentText());
|
||||
assertNull(msg.getContentTextFile());
|
||||
assertNotNull(attachment.getContent());
|
||||
assertNull(attachment.getContentFile());
|
||||
|
||||
emailer.migrateEmailsToFileStorage(Lists.newArrayList(msg));
|
||||
emailer.migrateAttachmentsToFileStorage(Lists.newArrayList(attachment));
|
||||
|
||||
// check file storage
|
||||
msg = reload(msg, "sendingMessage.loadFromQueue");
|
||||
attachment = msg.getAttachments().get(0);
|
||||
|
||||
assertNull(msg.getContentText());
|
||||
assertNotNull(msg.getContentTextFile());
|
||||
assertEquals(body, emailer.loadContentText(msg));
|
||||
|
||||
assertNull(attachment.getContent());
|
||||
assertNotNull(attachment.getContentFile());
|
||||
FileStorageAPI fileStorage = AppBeans.get(FileStorageAPI.NAME);
|
||||
byte[] actualBytes = fileStorage.loadFile(attachment.getContentFile());
|
||||
assertByteArrayEquals(expectedBytes, actualBytes);
|
||||
}
|
||||
|
||||
/* Utility */
|
||||
private Date getDeadlineWhichDoesntMatter() {
|
||||
return DateUtils.addHours(timeSource.currentTimestamp(), 2);
|
||||
@ -451,10 +578,10 @@ public class EmailerTest extends CubaTestCase {
|
||||
return (MimeBodyPart) textBodyPart.getBodyPart(1);
|
||||
}
|
||||
|
||||
private SendingMessage reload(SendingMessage sendingMessage) {
|
||||
private SendingMessage reload(SendingMessage sendingMessage, String... viewNames) {
|
||||
Transaction tx = persistence.createTransaction();
|
||||
try {
|
||||
sendingMessage = persistence.getEntityManager().reload(sendingMessage);
|
||||
sendingMessage = persistence.getEntityManager().reload(sendingMessage, viewNames);
|
||||
tx.commit();
|
||||
} finally {
|
||||
tx.end();
|
||||
@ -462,10 +589,10 @@ public class EmailerTest extends CubaTestCase {
|
||||
return sendingMessage;
|
||||
}
|
||||
|
||||
private void assertByteArrayEquals(byte[] data1, byte[] data2) {
|
||||
assertEquals(data1.length, data2.length);
|
||||
for (int i = 0; i < data1.length; i++) {
|
||||
assertEquals(data1[i], data2[i]);
|
||||
private void assertByteArrayEquals(byte[] expected, byte[] actual) {
|
||||
assertEquals(expected.length, actual.length);
|
||||
for (int i = 0; i < expected.length; i++) {
|
||||
assertEquals(expected[i], actual[i]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
*/
|
||||
package com.haulmont.cuba.core.app;
|
||||
|
||||
import com.haulmont.cuba.core.entity.SendingMessage;
|
||||
import com.haulmont.cuba.core.global.EmailAttachment;
|
||||
import com.haulmont.cuba.core.global.EmailException;
|
||||
import com.haulmont.cuba.core.global.EmailInfo;
|
||||
@ -28,7 +29,8 @@ public interface EmailService {
|
||||
* @param caption email subject
|
||||
* @param body email body
|
||||
* @param attachment email attachments
|
||||
* @throws com.haulmont.cuba.core.global.EmailException in case of any errors
|
||||
* @throws com.haulmont.cuba.core.global.EmailException
|
||||
* in case of any errors
|
||||
*/
|
||||
void sendEmail(String address, String caption, String body, EmailAttachment... attachment)
|
||||
throws EmailException;
|
||||
@ -58,4 +60,11 @@ public interface EmailService {
|
||||
* @param info email details
|
||||
*/
|
||||
void sendEmailAsync(EmailInfo info);
|
||||
|
||||
/**
|
||||
* Load content text for given message.
|
||||
*
|
||||
* @return email content text
|
||||
*/
|
||||
String loadContentText(SendingMessage sendingMessage);
|
||||
}
|
||||
|
@ -25,10 +25,17 @@ public class SendingAttachment extends StandardEntity {
|
||||
@Column(name = "MESSAGE_ID")
|
||||
protected SendingMessage message;
|
||||
|
||||
/**
|
||||
* Attachment data is stored either in this field or in {@link #contentFile}.
|
||||
*/
|
||||
@Basic(fetch = FetchType.LAZY)
|
||||
@Column(name = "CONTENT")
|
||||
protected byte[] content;
|
||||
|
||||
@JoinColumn(name = "CONTENT_FILE_ID")
|
||||
@OneToOne(fetch = FetchType.LAZY)
|
||||
protected FileDescriptor contentFile;
|
||||
|
||||
@Column(name = "NAME", length = 500)
|
||||
protected String name;
|
||||
|
||||
@ -88,4 +95,12 @@ public class SendingAttachment extends StandardEntity {
|
||||
public void setEncoding(String encoding) {
|
||||
this.encoding = encoding;
|
||||
}
|
||||
|
||||
public FileDescriptor getContentFile() {
|
||||
return contentFile;
|
||||
}
|
||||
|
||||
public void setContentFile(FileDescriptor contentFile) {
|
||||
this.contentFile = contentFile;
|
||||
}
|
||||
}
|
||||
|
@ -35,9 +35,16 @@ public class SendingMessage extends StandardEntity {
|
||||
@Column(name = "CAPTION")
|
||||
protected String caption;
|
||||
|
||||
/**
|
||||
* Email body is stored either in this field or in {@link #contentTextFile}.
|
||||
*/
|
||||
@Column(name = "CONTENT_TEXT")
|
||||
protected String contentText;
|
||||
|
||||
@JoinColumn(name = "CONTENT_TEXT_FILE_ID")
|
||||
@OneToOne(fetch = FetchType.LAZY)
|
||||
protected FileDescriptor contentTextFile;
|
||||
|
||||
@Column(name = "STATUS")
|
||||
protected Integer status;
|
||||
|
||||
@ -146,4 +153,12 @@ public class SendingMessage extends StandardEntity {
|
||||
public void setAttemptsMade(Integer attemptsMade) {
|
||||
this.attemptsMade = attemptsMade;
|
||||
}
|
||||
|
||||
public FileDescriptor getContentTextFile() {
|
||||
return contentTextFile;
|
||||
}
|
||||
|
||||
public void setContentTextFile(FileDescriptor contentTextFile) {
|
||||
this.contentTextFile = contentTextFile;
|
||||
}
|
||||
}
|
||||
|
@ -4,37 +4,42 @@
|
||||
*/
|
||||
package com.haulmont.cuba.core.global;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Email sending error.<br>
|
||||
* Contains failed addresses and corresponding error messages.
|
||||
*/
|
||||
public class EmailException extends Exception
|
||||
{
|
||||
private static final long serialVersionUID = -2559499596752714382L;
|
||||
@SupportedByClient
|
||||
public class EmailException extends Exception {
|
||||
|
||||
private String[] failedAddresses;
|
||||
private String[] messages;
|
||||
private static final long serialVersionUID = -9129158384759856382L;
|
||||
private final List<String> failedAddresses;
|
||||
/**
|
||||
* List of error messages which prevented email to be sent.
|
||||
*/
|
||||
private final List<String> messages;
|
||||
|
||||
public EmailException(String[] failedAddresses, String[] messages) {
|
||||
if (failedAddresses == null || messages == null || failedAddresses.length != messages.length)
|
||||
public EmailException(List<String> failedAddresses, List<String> messages) {
|
||||
if (failedAddresses == null || messages == null || failedAddresses.size() != messages.size())
|
||||
throw new IllegalArgumentException();
|
||||
|
||||
this.failedAddresses = failedAddresses;
|
||||
this.messages = messages;
|
||||
}
|
||||
|
||||
public String[] getFailedAddresses() {
|
||||
public List<String> getFailedAddresses() {
|
||||
return failedAddresses;
|
||||
}
|
||||
|
||||
public String[] getMessages() {
|
||||
public List<String> getMessages() {
|
||||
return messages;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < failedAddresses.length; i++) {
|
||||
sb.append(failedAddresses[i]).append(" : ").append(messages[i]).append("\n");
|
||||
for (int i = 0; i < failedAddresses.size(); i++) {
|
||||
sb.append(failedAddresses.get(i)).append(" : ").append(messages.get(i)).append("\n");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
@ -5,10 +5,13 @@
|
||||
|
||||
package com.haulmont.cuba.gui.app.core.sendingmessage.browse;
|
||||
|
||||
import com.haulmont.cuba.core.app.EmailService;
|
||||
import com.haulmont.cuba.core.entity.SendingMessage;
|
||||
import com.haulmont.cuba.gui.components.*;
|
||||
import com.haulmont.cuba.gui.components.AbstractWindow;
|
||||
import com.haulmont.cuba.gui.components.Component;
|
||||
import com.haulmont.cuba.gui.components.FieldGroup;
|
||||
import com.haulmont.cuba.gui.components.TextArea;
|
||||
import com.haulmont.cuba.gui.data.CollectionDatasource;
|
||||
import com.haulmont.cuba.gui.data.DataSupplier;
|
||||
import com.haulmont.cuba.gui.data.Datasource;
|
||||
import com.haulmont.cuba.gui.data.impl.DsListenerAdapter;
|
||||
import com.haulmont.cuba.gui.xml.layout.ComponentsFactory;
|
||||
@ -23,13 +26,13 @@ import java.util.UUID;
|
||||
*/
|
||||
public class SendingMessageBrowser extends AbstractWindow {
|
||||
|
||||
@Inject
|
||||
protected Datasource<SendingMessage> selectedMessageDs;
|
||||
protected static final String CONTENT_TEXT = "contentText";
|
||||
|
||||
@Inject
|
||||
protected CollectionDatasource<SendingMessage, UUID> sendingMessageDs;
|
||||
|
||||
@Inject
|
||||
protected DataSupplier dataSupplier;
|
||||
protected EmailService emailService;
|
||||
|
||||
@Inject
|
||||
protected FieldGroup fg;
|
||||
@ -39,25 +42,31 @@ public class SendingMessageBrowser extends AbstractWindow {
|
||||
|
||||
@Override
|
||||
public void init(Map<String, Object> params) {
|
||||
fg.addCustomField("contentText", new FieldGroup.CustomFieldGenerator() {
|
||||
fg.addCustomField(CONTENT_TEXT, new FieldGroup.CustomFieldGenerator() {
|
||||
@Override
|
||||
public Component generateField(Datasource datasource, String propertyId) {
|
||||
final TextArea textArea = factory.createComponent(TextArea.NAME);
|
||||
textArea.setDatasource(selectedMessageDs, "contentText");
|
||||
textArea.setRows(20);
|
||||
textArea.setHeight("350px");
|
||||
textArea.setEditable(false);
|
||||
return textArea;
|
||||
TextArea contentTextArea = factory.createComponent(TextArea.NAME);
|
||||
contentTextArea.setRows(20);
|
||||
contentTextArea.setHeight("350px");
|
||||
return contentTextArea;
|
||||
}
|
||||
});
|
||||
fg.setEditable(CONTENT_TEXT, false);
|
||||
sendingMessageDs.addListener(new DsListenerAdapter<SendingMessage>() {
|
||||
@Override
|
||||
public void itemChanged(Datasource<SendingMessage> ds, SendingMessage prevItem, SendingMessage item) {
|
||||
if (item != null) {
|
||||
item = dataSupplier.reload(item, selectedMessageDs.getView());
|
||||
}
|
||||
selectedMessageDs.setItem(item);
|
||||
selectedItemChanged(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void selectedItemChanged(SendingMessage item) {
|
||||
String contentText = null;
|
||||
if (item != null) {
|
||||
contentText = emailService.loadContentText(item);
|
||||
}
|
||||
fg.setEditable(CONTENT_TEXT, true);
|
||||
fg.setFieldValue(CONTENT_TEXT, contentText);
|
||||
fg.setEditable(CONTENT_TEXT, false);
|
||||
}
|
||||
}
|
||||
|
@ -12,17 +12,15 @@
|
||||
<collectionDatasource
|
||||
id="sendingMessageDs"
|
||||
class="com.haulmont.cuba.core.entity.SendingMessage"
|
||||
view="sendingMessage.browse" fetchMode="AUTO">
|
||||
view="sendingMessage.browse">
|
||||
<query>
|
||||
<![CDATA[select sm from sys$SendingMessage sm]]>
|
||||
</query>
|
||||
</collectionDatasource>
|
||||
<datasource id="selectedMessageDs" class="com.haulmont.cuba.core.entity.SendingMessage"
|
||||
view="sendingMessage.browse.detail"/>
|
||||
</dsContext>
|
||||
<layout>
|
||||
<split id="split" orientation="horizontal" pos="70" width="100%" height="100%">
|
||||
<vbox expand="table" spacing="true" width="100%" height="100%">
|
||||
<layout expand="hbox">
|
||||
<hbox id="hbox" spacing="true" width="100%" expand="leftBox">
|
||||
<vbox id="leftBox" expand="table" spacing="true" height="100%">
|
||||
<filter id="genericFilter" datasource="sendingMessageDs">
|
||||
<properties include=".*"/>
|
||||
</filter>
|
||||
@ -43,8 +41,7 @@
|
||||
<rows datasource="sendingMessageDs"/>
|
||||
</table>
|
||||
</vbox>
|
||||
<vbox expand="fg" margin="true">
|
||||
<fieldGroup id="fg" datasource="selectedMessageDs" width="500px" editable="false" border="visible">
|
||||
<fieldGroup id="fg" datasource="sendingMessageDs" editable="false" border="visible" width="500px">
|
||||
<column width="100%">
|
||||
<field id="address"/>
|
||||
<field id="from"/>
|
||||
@ -59,7 +56,6 @@
|
||||
<field id="attemptsMade"/>
|
||||
</column>
|
||||
</fieldGroup>
|
||||
</vbox>
|
||||
</split>
|
||||
</hbox>
|
||||
</layout>
|
||||
</window>
|
||||
|
Loading…
Reference in New Issue
Block a user