diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/QStorageInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/QStorageInterface.java
index e3d3b980..99b7462d 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/QStorageInterface.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/QStorageInterface.java
@@ -57,4 +57,13 @@ public interface QStorageInterface
//////////
}
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ default String getDownloadURL(StorageInput storageInput) throws QException
+ {
+ return (null);
+ }
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/StorageAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/StorageAction.java
index 52293a03..5cbcfc7e 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/StorageAction.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/StorageAction.java
@@ -105,4 +105,16 @@ public class StorageAction
QStorageInterface storageInterface = qBackendModuleInterface.getStorageInterface();
storageInterface.makePublic(storageInput);
}
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public String getDownloadURL(StorageInput storageInput) throws QException
+ {
+ QBackendModuleInterface qBackendModuleInterface = preAction(storageInput);
+ QStorageInterface storageInterface = qBackendModuleInterface.getStorageInterface();
+ return (storageInterface.getDownloadURL(storageInput));
+ }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/messaging/email/SendEmailAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/messaging/email/SendEmailAction.java
index eed19bb2..2723be6f 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/messaging/email/SendEmailAction.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/messaging/email/SendEmailAction.java
@@ -28,20 +28,25 @@ import java.util.List;
import java.util.Properties;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.messaging.Content;
import com.kingsrook.qqq.backend.core.model.actions.messaging.MultiParty;
import com.kingsrook.qqq.backend.core.model.actions.messaging.Party;
import com.kingsrook.qqq.backend.core.model.actions.messaging.PartyRole;
import com.kingsrook.qqq.backend.core.model.actions.messaging.SendMessageInput;
import com.kingsrook.qqq.backend.core.model.actions.messaging.SendMessageOutput;
+import com.kingsrook.qqq.backend.core.model.actions.messaging.email.EmailContentRole;
import com.kingsrook.qqq.backend.core.model.actions.messaging.email.EmailPartyRole;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import jakarta.mail.Address;
import jakarta.mail.Message;
+import jakarta.mail.Multipart;
import jakarta.mail.Session;
import jakarta.mail.Transport;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
+import jakarta.mail.internet.MimeBodyPart;
import jakarta.mail.internet.MimeMessage;
+import jakarta.mail.internet.MimeMultipart;
/*******************************************************************************
@@ -60,10 +65,10 @@ public class SendEmailAction
/////////////////////////////////////////
// set up properties to make a session //
/////////////////////////////////////////
- Properties properties = System.getProperties();
+ Properties properties = new Properties();
properties.setProperty("mail.smtp.host", messagingProvider.getSmtpServer());
properties.setProperty("mail.smtp.port", messagingProvider.getSmtpPort());
- Session session = Session.getDefaultInstance(properties);
+ Session session = Session.getInstance(properties);
try
{
@@ -72,7 +77,6 @@ public class SendEmailAction
////////////////////////////////////////////
MimeMessage emailMessage = new MimeMessage(session);
emailMessage.setSubject(sendMessageInput.getSubject());
- emailMessage.setText(sendMessageInput.getContentList().get(0).getBody());
Party to = sendMessageInput.getTo();
if(to instanceof MultiParty toMultiParty)
@@ -100,6 +104,25 @@ public class SendEmailAction
addSender(emailMessage, from);
}
+ Multipart multipart = new MimeMultipart();
+ for(Content content : sendMessageInput.getContentList())
+ {
+ if(EmailContentRole.HTML.equals(content.getContentRole()))
+ {
+ MimeBodyPart mimeBodyPart = new MimeBodyPart();
+ mimeBodyPart.setContent(content.getBody(), "text/html; charset=utf-8");
+ multipart.addBodyPart(mimeBodyPart);
+ }
+ else if(EmailContentRole.TEXT.equals(content.getContentRole()))
+ {
+ MimeBodyPart mimeBodyPart = new MimeBodyPart();
+ mimeBodyPart.setContent(content.getBody(), "text/plain; charset=utf-8");
+ multipart.addBodyPart(mimeBodyPart);
+ }
+ }
+
+ emailMessage.setContent(multipart);
+
/////////////
// send it //
/////////////
@@ -132,7 +155,6 @@ public class SendEmailAction
else
{
List
replyToList = Arrays.asList(replyTo);
- replyToList.add(internetAddress);
emailMessage.setReplyTo(replyToList.toArray(new Address[0]));
}
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportExecuteStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportExecuteStep.java
index 766a2a09..d75e74c1 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportExecuteStep.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportExecuteStep.java
@@ -29,6 +29,9 @@ import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
import java.util.Map;
import java.util.UUID;
import com.kingsrook.qqq.backend.core.actions.messaging.SendMessageAction;
@@ -39,6 +42,7 @@ import com.kingsrook.qqq.backend.core.actions.tables.StorageAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.messaging.Content;
import com.kingsrook.qqq.backend.core.model.actions.messaging.MultiParty;
@@ -64,6 +68,7 @@ import com.kingsrook.qqq.backend.core.model.savedreports.SavedReport;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import org.apache.commons.validator.EmailValidator;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@@ -89,6 +94,9 @@ public class RenderSavedReportExecuteStep implements BackendStep
////////////////////////////////
// read inputs, set up params //
////////////////////////////////
+ String sesProviderName = runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.SES_PROVIDER_NAME);
+ String fromEmailAddress = runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.FROM_EMAIL_ADDRESS);
+ String replyToEmailAddress = runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.REPLY_TO_EMAIL_ADDRESS);
String storageTableName = runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.FIELD_NAME_STORAGE_TABLE_NAME);
ReportFormat reportFormat = ReportFormat.fromString(runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.FIELD_NAME_REPORT_FORMAT));
String sendToEmailAddress = runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.FIELD_NAME_EMAIL_ADDRESS);
@@ -96,6 +104,15 @@ public class RenderSavedReportExecuteStep implements BackendStep
String downloadFileBaseName = getDownloadFileBaseName(runBackendStepInput, savedReport);
String storageReference = LocalDate.now() + "/" + LocalTime.now().toString().replaceAll(":", "").replaceFirst("\\..*", "") + "/" + UUID.randomUUID() + "/" + downloadFileBaseName + "." + reportFormat.getExtension();
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // if sending an email (or emails), validate the addresses before doing anything so user gets error and can fix //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ List toEmailAddressList = new ArrayList<>();
+ if(sendToEmailAddress != null)
+ {
+ toEmailAddressList = validateEmailAddresses(sendToEmailAddress);
+ }
+
StorageAction storageAction = new StorageAction();
StorageInput storageInput = new StorageInput(storageTableName).withReference(storageReference);
OutputStream outputStream = storageAction.createOutputStream(storageInput);
@@ -133,7 +150,6 @@ public class RenderSavedReportExecuteStep implements BackendStep
reportInput.setInputValues(values);
ReportOutput reportOutput = new GenerateReportAction().execute(reportInput);
- storageAction.makePublic(storageInput);
///////////////////////////////////
// update record to show success //
@@ -152,20 +168,33 @@ public class RenderSavedReportExecuteStep implements BackendStep
runBackendStepOutput.addValue("storageReference", storageReference);
LOG.info("Completed rendering a report", logPair("savedReportId", savedReport.getId()), logPair("tableName", savedReport.getTableName()), logPair("storageReference", storageReference), logPair("rowCount", reportOutput.getTotalRecordCount()));
- if(sendToEmailAddress != null && CollectionUtils.nullSafeHasContents(QContext.getQInstance().getMessagingProviders()))
+ if(!toEmailAddressList.isEmpty() && CollectionUtils.nullSafeHasContents(QContext.getQInstance().getMessagingProviders()))
{
- String s3Url = "https://bucket-ctlive-reports-dev.s3.us-east-2.amazonaws.com/saved-reports/" + storageReference; // TODO: derp
+ ///////////////////////////////////////////////////////////
+ // since sending email, make s3 file publicly accessible //
+ ///////////////////////////////////////////////////////////
+ storageAction.makePublic(storageInput);
+ ////////////////////////////////////////////////
+ // add multiparty in case multiple recipients //
+ ////////////////////////////////////////////////
+ MultiParty recipients = new MultiParty();
+ for(String toAddress : toEmailAddressList)
+ {
+ recipients.addParty(new Party().withAddress(toAddress).withRole(EmailPartyRole.TO));
+ }
+
+ String downloadURL = storageAction.getDownloadURL(storageInput);
new SendMessageAction().execute(new SendMessageInput()
- .withMessagingProviderName("defaultMessagingProvider") // TODO: derp
- .withTo(new Party().withAddress(sendToEmailAddress).withRole(EmailPartyRole.TO))
+ .withMessagingProviderName(sesProviderName)
+ .withTo(recipients)
.withFrom(new MultiParty()
- .withParty(new Party().withAddress("reports@coldtrack-dev.com").withRole(EmailPartyRole.FROM)) // TODO: derp
- .withParty(new Party().withAddress("noreply@coldtrack-dev.com").withRole(EmailPartyRole.REPLY_TO)) // TODO: derp
+ .withParty(new Party().withAddress(fromEmailAddress).withRole(EmailPartyRole.FROM))
+ .withParty(new Party().withAddress(replyToEmailAddress).withRole(EmailPartyRole.REPLY_TO))
)
.withSubject(downloadFileBaseName)
- .withContent(new Content().withContentRole(EmailContentRole.TEXT).withBody("To download your report, open this URL in your browser: " + s3Url))
- .withContent(new Content().withContentRole(EmailContentRole.HTML).withBody("Link: " + downloadFileName + ""))
+ .withContent(new Content().withContentRole(EmailContentRole.TEXT).withBody("To download your report, open this URL in your browser: " + downloadURL))
+ .withContent(new Content().withContentRole(EmailContentRole.HTML).withBody("Link: " + downloadFileName + ""))
);
}
}
@@ -188,6 +217,42 @@ public class RenderSavedReportExecuteStep implements BackendStep
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private List validateEmailAddresses(String sendToEmailAddress) throws QUserFacingException
+ {
+ ////////////////////////////////////////////////////////////////
+ // split email address string on spaces, comma, and semicolon //
+ ////////////////////////////////////////////////////////////////
+ List toEmailAddressList = Arrays.asList(sendToEmailAddress.split("[\\s,;]+"));
+
+ //////////////////////////////////////////////////////
+ // check each address keeping track of any bad ones //
+ //////////////////////////////////////////////////////
+ List invalidEmails = new ArrayList<>();
+ EmailValidator validator = EmailValidator.getInstance();
+ for(String emailAddress : toEmailAddressList)
+ {
+ if(!validator.isValid(emailAddress))
+ {
+ invalidEmails.add(emailAddress);
+ }
+ }
+
+ ///////////////////////////////////////
+ // if bad one found, throw exception //
+ ///////////////////////////////////////
+ if(!invalidEmails.isEmpty())
+ {
+ throw (new QUserFacingException("The following email addresses were invalid: " + StringUtils.join(",", invalidEmails)));
+ }
+
+ return (toEmailAddressList);
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportMetaDataProducer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportMetaDataProducer.java
index 828f5aa6..6490fb65 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportMetaDataProducer.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/savedreports/RenderSavedReportMetaDataProducer.java
@@ -47,6 +47,9 @@ public class RenderSavedReportMetaDataProducer implements MetaDataProducerInterf
{
public static final String NAME = "renderSavedReport";
+ public static final String SES_PROVIDER_NAME = "sesProviderName";
+ public static final String FROM_EMAIL_ADDRESS = "fromEmailAddress";
+ public static final String REPLY_TO_EMAIL_ADDRESS = "replyToEmailAddress";
public static final String FIELD_NAME_STORAGE_TABLE_NAME = "storageTableName";
public static final String FIELD_NAME_REPORT_FORMAT = "reportFormat";
public static final String FIELD_NAME_EMAIL_ADDRESS = "reportDestinationEmailAddress";
@@ -67,6 +70,9 @@ public class RenderSavedReportMetaDataProducer implements MetaDataProducerInterf
.addStep(new QBackendStepMetaData()
.withName("pre")
.withInputData(new QFunctionInputMetaData()
+ .withField(new QFieldMetaData(SES_PROVIDER_NAME, QFieldType.STRING))
+ .withField(new QFieldMetaData(FROM_EMAIL_ADDRESS, QFieldType.STRING))
+ .withField(new QFieldMetaData(REPLY_TO_EMAIL_ADDRESS, QFieldType.STRING))
.withField(new QFieldMetaData(FIELD_NAME_STORAGE_TABLE_NAME, QFieldType.STRING))
.withRecordListMetaData(new QRecordListMetaData().withTableName(SavedReport.TABLE_NAME)))
.withCode(new QCodeReference(RenderSavedReportPreStep.class)))
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/messaging/email/EmailMessagingProviderTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/messaging/email/EmailMessagingProviderTest.java
index 77077de1..3f008fd7 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/messaging/email/EmailMessagingProviderTest.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/messaging/email/EmailMessagingProviderTest.java
@@ -57,8 +57,7 @@ class EmailMessagingProviderTest extends BaseTest
.withParty(new Party().withAddress("james.maes@kingsrook.com").withLabel("Mames Maes").withRole(EmailPartyRole.CC))
.withParty(new Party().withAddress("tyler.samples@kingsrook.com").withLabel("Tylers Ample").withRole(EmailPartyRole.BCC))
)
- // .withFrom(new Party().withAddress("darin.kelkhoff@gmail.com").withLabel("Darin Kelkhoff"))
- .withFrom(new Party().withAddress("tim.chamberlain@kingsrook.com").withLabel("Tim Chamberlain"))
+ .withFrom(new Party().withAddress("darin.kelkhoff@gmail.com").withLabel("Darin Kelkhoff"))
.withSubject("This is another qqq test message.")
.withContent(new Content().withContentRole(EmailContentRole.TEXT).withBody("This is a text body"))
.withContent(new Content().withContentRole(EmailContentRole.HTML).withBody("This is an HTML body!"))
diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemStorageAction.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemStorageAction.java
index 24d9fa47..e5a0b4f2 100644
--- a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemStorageAction.java
+++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/local/actions/FilesystemStorageAction.java
@@ -51,7 +51,7 @@ public class FilesystemStorageAction extends AbstractFilesystemAction implements
try
{
String fullPath = getFullPath(storageInput);
- File file = new File(fullPath);
+ File file = new File(fullPath);
if(!file.getParentFile().exists())
{
if(!file.getParentFile().mkdirs())
@@ -100,4 +100,15 @@ public class FilesystemStorageAction extends AbstractFilesystemAction implements
}
}
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public String getDownloadURL(StorageInput storageInput) throws QException
+ {
+ return ("file://" + getFullPath(storageInput));
+ }
+
}
diff --git a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3StorageAction.java b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3StorageAction.java
index 40009fea..96c72bde 100644
--- a/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3StorageAction.java
+++ b/qqq-backend-module-filesystem/src/main/java/com/kingsrook/qqq/backend/module/filesystem/s3/actions/S3StorageAction.java
@@ -112,7 +112,30 @@ public class S3StorageAction extends AbstractS3Action implements QStorageInterfa
}
catch(Exception e)
{
- throw (new QException("Exception getting s3 input stream for file", e));
+ throw (new QException("Exception getting s3 input stream for file.", e));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public String getDownloadURL(StorageInput storageInput) throws QException
+ {
+ try
+ {
+ S3BackendMetaData backend = (S3BackendMetaData) storageInput.getBackend();
+ preAction(backend);
+
+ AmazonS3 amazonS3 = getS3Utils().getAmazonS3();
+ String fullPath = getFullPath(storageInput);
+ return (amazonS3.getUrl(backend.getBucketName(), fullPath).toString());
+ }
+ catch(Exception e)
+ {
+ throw (new QException("Exception getting the S3 download URL.", e));
}
}
@@ -135,7 +158,7 @@ public class S3StorageAction extends AbstractS3Action implements QStorageInterfa
}
catch(Exception e)
{
- throw (new QException("Exception making s3 file publicly available", e));
+ throw (new QException("Exception making s3 file publicly available.", e));
}
}