I have a piece of code that I hard coded in order to fix a silent issue that occurred while inserting attachments into my email.
Basically when running my tests the email object comes back with the attachments attached and I can assert through said tests that everything is attached, with the correct names and information.
The initial version where I hard coded stuff can be seen below:
private final InputStream firstAttachment = WelcomeEmail.class
.getResourceAsStream("/META-INF/resources/attachments/welcome/example1.pdf");
private final InputStream secondAttachment =
WelcomeEmail.class.getResourceAsStream("/META-INF/resources/attachments/welcome/example2.pdf");
private final InputStream thirdAttachment = WelcomeEmail.class
.getResourceAsStream("/META-INF/resources/attachments/welcome/example3.pdf");
private final InputStream fourthAttachment = WelcomeEmail.class.getResourceAsStream(
"/META-INF/resources/attachments/welcome/example4.pdf");
/**
* Builds an instance of an email based on a specific template.
*
* @param paymentIntent Payment Intent used to extract information for the email generation
* @param context The Context used to populate the email template
* @return Instance of the Email template populated.
*/
@Override
public MailTemplateInstance build(PaymentIntentAggregate paymentIntent,
CustomerEmailContext context) {
if (paymentIntent == null) {
log.warn("Couldn't send customer Welcome Email: Missing Payment Intent");
return null;
}
String customerEmail = context.getCustomerEmail();
Mail mail = new Mail().setTo(List.of(customerEmail))
.setSubject(SIGN_ENCRYPT_SUBJECT_PREFIX + getSubject());
try {
mail.addAttachment("Name1.pdf", firstAttachment.readAllBytes(), ".pdf");
mail.addAttachment("Name2.pdf", secondAttachment.readAllBytes(), ".pdf");
mail.addAttachment("Name3.pdf", thirdAttachment.readAllBytes(), ".pdf");
mail.addAttachment("Name4.pdf", fourthAttachment.readAllBytes(), ".pdf");
} catch (IOException e) {
throw new RuntimeException(e);
}
return mailTemplate.of(mail).data("mailId", generateEmailId());
}
This version is working and the email is being sent with all the attachments in it.
To improve this code I developed the following refactored version:
private static final String ATTACHMENTS_DIRECTORY = "/META-INF/resources/attachments/welcome/";
/**
* Builds an instance of an email based on a specific template.
*
* @param paymentIntent Payment Intent used to extract information for the email generation
* @param context The Context used to populate the email template
* @return Instance of the Email template populated.
*/
@Override
public MailTemplateInstance build(PaymentIntentAggregate paymentIntent,
CustomerEmailContext context) {
if (paymentIntent == null) {
log.warn("Couldn't send customer Welcome Email: Missing Payment Intent");
return null;
}
String customerEmail = context.getCustomerEmail();
Mail mail = new Mail().setTo(List.of(customerEmail)).setSubject(getSubject())
.setAttachments(AttachmentService.getAttachments(ATTACHMENTS_DIRECTORY));
return mailTemplate.of(mail).data("mailId", generateEmailId());
}
Where the AttachmentService which, as the name implies, basically takes care of retrieving attachments, looks like this:
@Slf4j
@ApplicationScoped
public class AttachmentService {
/**
* Gets all the attachments from the given resource name (directory)
*
* @param resourceName The resource name which must always be a directory
*
* @return All the attachments retrieved from the given resource
*/
public static List<Attachment> getAttachments(String resourceName) {
return getAttachmentNames(resourceName).stream()
.map(attachmentName -> getAttachment(resourceName + attachmentName, attachmentName))
.toList();
}
private static Attachment getAttachment(String attachmentFQN, String attachmentName) {
try (InputStream attachment = AttachmentService.class.getResourceAsStream(attachmentFQN)) {
return new Attachment(attachmentName, attachment.readAllBytes(), ContentType.PDF);
} catch (IOException ioException) {
log.error("An issue occurred while attempting to read attachment {}", attachmentFQN,
ioException);
throw new RuntimeException(ioException);
}
}
/**
* Retrieves all attachment names from the given resource (directory).
*
* @param resourceName The directory path
*
* @return List of all attachment names in the given directory
*/
private static List<String> getAttachmentNames(String resourceName) {
try (BufferedReader directoryContentReader = new BufferedReader(
new InputStreamReader(AttachmentService.class.getResourceAsStream(resourceName)))) {
return directoryContentReader.lines().toList();
} catch (IOException ioException) {
log.error("An issue occurred while attempting to read attachment names from path {}",
resourceName, ioException);
throw new RuntimeException(ioException);
}
}
}
This refactored version does not break any of the existing tests I made and asserts that, when running the maven lifecycle or running the tests directly through the IDE, that the attachments are inserted into the email object.
The problem is, when running this code from the JAR the attachments are not inserted into the email and the email is sent as is without them. In other words, when deploying the code, the email is sent without the attachments.
There is no error being logged and therefor it is impossible to determine if and where the issue is happening.
I have tried changing the path to the directory where the attachments are being stored and that immediately throws an error which tells me it is not an issue with the current directory path.
One suspicion I have is that I might be somehow mishandling the InputStreams when using them to read the content of the attachments but at the same time no exception is being thrown and I use try with resources to specifically close the streams when I finish using them.
Any help would be greatly appreciated, kind regards in advance!
Some of the documentation I consulted while developing this code:
Basically for anyone that comes across this post the issue was rooted in the getAttachmentNames method.
/**
* Retrieves all attachment names from the given resource (directory).
*
* @param resourceName The directory path
*
* @return List of all attachment names in the given directory
*/
private static List<String> getAttachmentNames(String resourceName) {
try (BufferedReader directoryContentReader = new BufferedReader(
new InputStreamReader(AttachmentService.class.getResourceAsStream(resourceName)))) {
return directoryContentReader.lines().toList();
} catch (IOException ioException) {
log.error("An issue occurred while attempting to read attachment names from path {}",
resourceName, ioException);
throw new RuntimeException(ioException);
}
}
Running this code in your IDE will lead you to believe that executing
BufferedReader directoryContentReader = new BufferedReader(
new InputStreamReader(AttachmentService.class.getResourceAsStream(resourceName)))
on a directory will give you the same functionality that Files.list()
or DirectoryStream
provides but in fact this is not part of the specification of the getResourceAsStream()
method. On the other hand, it will not give you an error, which was what was happening here.
So again, to re-iterate, DO NOT attempt to use getResourceAsStream()
on a directory and expect it to return the contents of the given directory, it will not work when running from a native JAR.
This answer on another SO post was what helped me figure this out and solve the issue.