CE-1068: changes from review: allow multiple email address entry, fixed download urls, fixed localhost to use inbucket/filesystem

This commit is contained in:
Tim Chamberlain
2024-04-24 17:11:48 -05:00
parent 570d1a80b5
commit 9281d07e96
8 changed files with 165 additions and 18 deletions

View File

@ -57,4 +57,13 @@ public interface QStorageInterface
////////// //////////
} }
/*******************************************************************************
**
*******************************************************************************/
default String getDownloadURL(StorageInput storageInput) throws QException
{
return (null);
}
} }

View File

@ -105,4 +105,16 @@ public class StorageAction
QStorageInterface storageInterface = qBackendModuleInterface.getStorageInterface(); QStorageInterface storageInterface = qBackendModuleInterface.getStorageInterface();
storageInterface.makePublic(storageInput); storageInterface.makePublic(storageInput);
} }
/*******************************************************************************
**
*******************************************************************************/
public String getDownloadURL(StorageInput storageInput) throws QException
{
QBackendModuleInterface qBackendModuleInterface = preAction(storageInput);
QStorageInterface storageInterface = qBackendModuleInterface.getStorageInterface();
return (storageInterface.getDownloadURL(storageInput));
}
} }

View File

@ -28,20 +28,25 @@ import java.util.List;
import java.util.Properties; import java.util.Properties;
import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException; 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.MultiParty;
import com.kingsrook.qqq.backend.core.model.actions.messaging.Party; 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.PartyRole;
import com.kingsrook.qqq.backend.core.model.actions.messaging.SendMessageInput; 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.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.model.actions.messaging.email.EmailPartyRole;
import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils;
import jakarta.mail.Address; import jakarta.mail.Address;
import jakarta.mail.Message; import jakarta.mail.Message;
import jakarta.mail.Multipart;
import jakarta.mail.Session; import jakarta.mail.Session;
import jakarta.mail.Transport; import jakarta.mail.Transport;
import jakarta.mail.internet.AddressException; import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress; import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeBodyPart;
import jakarta.mail.internet.MimeMessage; import jakarta.mail.internet.MimeMessage;
import jakarta.mail.internet.MimeMultipart;
/******************************************************************************* /*******************************************************************************
@ -60,10 +65,10 @@ public class SendEmailAction
///////////////////////////////////////// /////////////////////////////////////////
// set up properties to make a session // // 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.host", messagingProvider.getSmtpServer());
properties.setProperty("mail.smtp.port", messagingProvider.getSmtpPort()); properties.setProperty("mail.smtp.port", messagingProvider.getSmtpPort());
Session session = Session.getDefaultInstance(properties); Session session = Session.getInstance(properties);
try try
{ {
@ -72,7 +77,6 @@ public class SendEmailAction
//////////////////////////////////////////// ////////////////////////////////////////////
MimeMessage emailMessage = new MimeMessage(session); MimeMessage emailMessage = new MimeMessage(session);
emailMessage.setSubject(sendMessageInput.getSubject()); emailMessage.setSubject(sendMessageInput.getSubject());
emailMessage.setText(sendMessageInput.getContentList().get(0).getBody());
Party to = sendMessageInput.getTo(); Party to = sendMessageInput.getTo();
if(to instanceof MultiParty toMultiParty) if(to instanceof MultiParty toMultiParty)
@ -100,6 +104,25 @@ public class SendEmailAction
addSender(emailMessage, from); 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 // // send it //
///////////// /////////////
@ -132,7 +155,6 @@ public class SendEmailAction
else else
{ {
List<Address> replyToList = Arrays.asList(replyTo); List<Address> replyToList = Arrays.asList(replyTo);
replyToList.add(internetAddress);
emailMessage.setReplyTo(replyToList.toArray(new Address[0])); emailMessage.setReplyTo(replyToList.toArray(new Address[0]));
} }
} }

View File

@ -29,6 +29,9 @@ import java.time.LocalDate;
import java.time.LocalTime; import java.time.LocalTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import com.kingsrook.qqq.backend.core.actions.messaging.SendMessageAction; 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.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException; 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.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.messaging.Content; 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.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.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ExceptionUtils; import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.commons.validator.EmailValidator;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -89,6 +94,9 @@ public class RenderSavedReportExecuteStep implements BackendStep
//////////////////////////////// ////////////////////////////////
// read inputs, set up params // // 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); String storageTableName = runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.FIELD_NAME_STORAGE_TABLE_NAME);
ReportFormat reportFormat = ReportFormat.fromString(runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.FIELD_NAME_REPORT_FORMAT)); ReportFormat reportFormat = ReportFormat.fromString(runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.FIELD_NAME_REPORT_FORMAT));
String sendToEmailAddress = runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.FIELD_NAME_EMAIL_ADDRESS); String sendToEmailAddress = runBackendStepInput.getValueString(RenderSavedReportMetaDataProducer.FIELD_NAME_EMAIL_ADDRESS);
@ -96,6 +104,15 @@ public class RenderSavedReportExecuteStep implements BackendStep
String downloadFileBaseName = getDownloadFileBaseName(runBackendStepInput, savedReport); String downloadFileBaseName = getDownloadFileBaseName(runBackendStepInput, savedReport);
String storageReference = LocalDate.now() + "/" + LocalTime.now().toString().replaceAll(":", "").replaceFirst("\\..*", "") + "/" + UUID.randomUUID() + "/" + downloadFileBaseName + "." + reportFormat.getExtension(); 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<String> toEmailAddressList = new ArrayList<>();
if(sendToEmailAddress != null)
{
toEmailAddressList = validateEmailAddresses(sendToEmailAddress);
}
StorageAction storageAction = new StorageAction(); StorageAction storageAction = new StorageAction();
StorageInput storageInput = new StorageInput(storageTableName).withReference(storageReference); StorageInput storageInput = new StorageInput(storageTableName).withReference(storageReference);
OutputStream outputStream = storageAction.createOutputStream(storageInput); OutputStream outputStream = storageAction.createOutputStream(storageInput);
@ -133,7 +150,6 @@ public class RenderSavedReportExecuteStep implements BackendStep
reportInput.setInputValues(values); reportInput.setInputValues(values);
ReportOutput reportOutput = new GenerateReportAction().execute(reportInput); ReportOutput reportOutput = new GenerateReportAction().execute(reportInput);
storageAction.makePublic(storageInput);
/////////////////////////////////// ///////////////////////////////////
// update record to show success // // update record to show success //
@ -152,20 +168,33 @@ public class RenderSavedReportExecuteStep implements BackendStep
runBackendStepOutput.addValue("storageReference", storageReference); 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())); 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() new SendMessageAction().execute(new SendMessageInput()
.withMessagingProviderName("defaultMessagingProvider") // TODO: derp .withMessagingProviderName(sesProviderName)
.withTo(new Party().withAddress(sendToEmailAddress).withRole(EmailPartyRole.TO)) .withTo(recipients)
.withFrom(new MultiParty() .withFrom(new MultiParty()
.withParty(new Party().withAddress("reports@coldtrack-dev.com").withRole(EmailPartyRole.FROM)) // TODO: derp .withParty(new Party().withAddress(fromEmailAddress).withRole(EmailPartyRole.FROM))
.withParty(new Party().withAddress("noreply@coldtrack-dev.com").withRole(EmailPartyRole.REPLY_TO)) // TODO: derp .withParty(new Party().withAddress(replyToEmailAddress).withRole(EmailPartyRole.REPLY_TO))
) )
.withSubject(downloadFileBaseName) .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.TEXT).withBody("To download your report, open this URL in your browser: " + downloadURL))
.withContent(new Content().withContentRole(EmailContentRole.HTML).withBody("Link: <a target=\"_blank\" href=\"" + s3Url + "\">" + downloadFileName + "</a>")) .withContent(new Content().withContentRole(EmailContentRole.HTML).withBody("Link: <a target=\"_blank\" href=\"" + downloadURL + "\" download>" + downloadFileName + "</a>"))
); );
} }
} }
@ -188,6 +217,42 @@ public class RenderSavedReportExecuteStep implements BackendStep
/*******************************************************************************
**
*******************************************************************************/
private List<String> validateEmailAddresses(String sendToEmailAddress) throws QUserFacingException
{
////////////////////////////////////////////////////////////////
// split email address string on spaces, comma, and semicolon //
////////////////////////////////////////////////////////////////
List<String> toEmailAddressList = Arrays.asList(sendToEmailAddress.split("[\\s,;]+"));
//////////////////////////////////////////////////////
// check each address keeping track of any bad ones //
//////////////////////////////////////////////////////
List<String> 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);
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/

View File

@ -47,6 +47,9 @@ public class RenderSavedReportMetaDataProducer implements MetaDataProducerInterf
{ {
public static final String NAME = "renderSavedReport"; 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_STORAGE_TABLE_NAME = "storageTableName";
public static final String FIELD_NAME_REPORT_FORMAT = "reportFormat"; public static final String FIELD_NAME_REPORT_FORMAT = "reportFormat";
public static final String FIELD_NAME_EMAIL_ADDRESS = "reportDestinationEmailAddress"; public static final String FIELD_NAME_EMAIL_ADDRESS = "reportDestinationEmailAddress";
@ -67,6 +70,9 @@ public class RenderSavedReportMetaDataProducer implements MetaDataProducerInterf
.addStep(new QBackendStepMetaData() .addStep(new QBackendStepMetaData()
.withName("pre") .withName("pre")
.withInputData(new QFunctionInputMetaData() .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)) .withField(new QFieldMetaData(FIELD_NAME_STORAGE_TABLE_NAME, QFieldType.STRING))
.withRecordListMetaData(new QRecordListMetaData().withTableName(SavedReport.TABLE_NAME))) .withRecordListMetaData(new QRecordListMetaData().withTableName(SavedReport.TABLE_NAME)))
.withCode(new QCodeReference(RenderSavedReportPreStep.class))) .withCode(new QCodeReference(RenderSavedReportPreStep.class)))

View File

@ -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("james.maes@kingsrook.com").withLabel("Mames Maes").withRole(EmailPartyRole.CC))
.withParty(new Party().withAddress("tyler.samples@kingsrook.com").withLabel("Tylers Ample").withRole(EmailPartyRole.BCC)) .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("darin.kelkhoff@gmail.com").withLabel("Darin Kelkhoff"))
.withFrom(new Party().withAddress("tim.chamberlain@kingsrook.com").withLabel("Tim Chamberlain"))
.withSubject("This is another qqq test message.") .withSubject("This is another qqq test message.")
.withContent(new Content().withContentRole(EmailContentRole.TEXT).withBody("This is a text body")) .withContent(new Content().withContentRole(EmailContentRole.TEXT).withBody("This is a text body"))
.withContent(new Content().withContentRole(EmailContentRole.HTML).withBody("This <u>is</u> an <b>HTML</b> body!")) .withContent(new Content().withContentRole(EmailContentRole.HTML).withBody("This <u>is</u> an <b>HTML</b> body!"))

View File

@ -51,7 +51,7 @@ public class FilesystemStorageAction extends AbstractFilesystemAction implements
try try
{ {
String fullPath = getFullPath(storageInput); String fullPath = getFullPath(storageInput);
File file = new File(fullPath); File file = new File(fullPath);
if(!file.getParentFile().exists()) if(!file.getParentFile().exists())
{ {
if(!file.getParentFile().mkdirs()) 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));
}
} }

View File

@ -112,7 +112,30 @@ public class S3StorageAction extends AbstractS3Action implements QStorageInterfa
} }
catch(Exception e) 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) catch(Exception e)
{ {
throw (new QException("Exception making s3 file publicly available", e)); throw (new QException("Exception making s3 file publicly available.", e));
} }
} }