Send Emails with attachment in AEM

Send emails with attachment in AEM using the Day CQ Mail Service

Goal

Create an OSGi component that sends emails.

Use Case

In this example the email is sent after a scheduled job. More specifically, in the original scenario, it was an import process. Another common use case could be within a servlet triggered by an user click.

Procedure

Let’s create the class where the email sending is triggered. As I said, in this case, a scheduler:

package com.adobe.training.core.schedulers;

import com.adobe.training.core.service.EmailService;
import com.adobe.training.core.utils.Lab2020CommonMethods;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.Map;

@Designate(ocd = ImportEntity1Scheduler.Config.class)
@Component(service = Runnable.class)
public class ImportEntity1Scheduler implements Runnable {

    @Reference
    private ResourceResolverFactory resourceResolverFactory;

    @Reference
    private EmailService emailService;

    private boolean enabled;

    @ObjectClassDefinition(name = "Lab2020 - Import Entiy1 Job")
    public static @interface Config {

        @AttributeDefinition(
                name = "Enabled",
                description = "Schedule task is enabled",
                type = AttributeType.BOOLEAN
        )
        boolean isEnabled() default false;

        @AttributeDefinition(
                name = "Concurrent",
                description = "Schedule task concurrently",
                type = AttributeType.BOOLEAN
        )
        boolean scheduler_concurrent() default true;

        @AttributeDefinition(
                name = "Expression",
                description = "Cron-job expression. Default: runs every midnight and every 8 hours [6 AM, 2 PM, 10 PM]. (0 0 0/6 * * ?)",
                type = AttributeType.STRING
        )
        String scheduler_expression() default "0 0 6/8 * * ?";
    }

    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public void run() {
        /*
         Some logic
         */
        ResourceResolver resourceResolver = Lab2020CommonMethods.getResourceResolver(resourceResolverFactory);
        String csvReport = "/content/dam/Lab2020/reports/entity1/report-timestamp.csv";
        Map<String, String> params = new HashMap<>();
        params.put("importEntity", "Entity1");
        if (emailService.isEmailServiceEnabled()) {
            emailService.sendEmail(params, csvReport, resourceResolver);
        }
    }

    @Activate
    protected void activate(final Config config) {
        enabled = config.isEnabled();
    }

}

What is important here to keep in mind is the parameters map. You need to know that you can have as many dynamic values as you want in your email template and they are passed and replaced through this map (you’ll see it later) .

In this case the variable “csvReport” is the result of some logic that after creating/updating your pages/nodes it creates a CSV file in the DAM. In other cases, you could also have a specific file available in your DAM to attach for instance, or another generated file. As always, it depends on your use case.

It’s been used a resource resolver associated with a system user. If you don’t know how to create it, check this article.

The email service OSGi component instead is as follows (remember to create the interaface!):

package com.adobe.training.core.service.impl;

import com.adobe.training.core.service.EmailService;
import com.adobe.training.core.service.config.EmailServiceConfiguration;
import com.day.cq.commons.mail.MailTemplate;
import com.day.cq.mailer.MessageGateway;
import com.day.cq.mailer.MessageGatewayService;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.text.StrLookup;
import org.apache.commons.mail.EmailException;
import org.apache.commons.mail.HtmlEmail;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.metatype.annotations.Designate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Binary;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.mail.MessagingException;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.util.ByteArrayDataSource;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import static com.day.crx.JcrConstants.JCR_DATA;

/**
 * Created by r.teruzzi on 22/01/2018.
 */
@Component(
        immediate = true,
        service = EmailService.class
)
@Designate(ocd = EmailServiceConfiguration.class)
public class EmailServiceImpl implements EmailService {

    @Reference
    private MessageGatewayService messageGatewayService;

    private static final Logger LOGGER = LoggerFactory.getLogger(EmailServiceImpl.class);
    private String[] listOfAddresses = null;
    private String emailTemplatePathWithAttachment;
    private boolean isEnabled;

    @Activate
    @Modified
    public void activate(EmailServiceConfiguration configuration) {
        listOfAddresses = configuration.getListOfAddresses();
        emailTemplatePathWithAttachment = configuration.getEmailTemplatePathWithAttachment();
        isEnabled = configuration.isEnabled();
    }

    @Override
    public void sendEmail(Map<String, String> params, String csvReport, ResourceResolver resourceResolver) {
        if (listOfAddresses == null || listOfAddresses.length <= 0) {
            if (listOfAddresses == null) {
                LOGGER.error("List of addresses is null");
            } else {
                LOGGER.warn("List of recipients is empty");
            }
        } else {
            List<InternetAddress> addresses = new ArrayList<>();
            for (String recipient : listOfAddresses) {
                try {
                    addresses.add(new InternetAddress(recipient));
                } catch (AddressException e) {
                    LOGGER.warn("Invalid email address {} passed to sendEmail(). Skipping.", recipient, e);
                }
            }
            InternetAddress[] iAddressRecipients = addresses.toArray(new InternetAddress[addresses.size()]);
            try {
                sendEmail(iAddressRecipients, params, csvReport, resourceResolver);
                //Should never happen
            } catch (Exception e) {
                LOGGER.error("Failed to send email", e);
            }
        }
    }

    private void sendEmail(InternetAddress[] iAddressRecipients, Map<String, String> params, String csvReport, ResourceResolver resourceResolver) {

        if (iAddressRecipients != null && resourceResolver != null) {
            final MessageGateway<HtmlEmail> messageGateway = messageGatewayService.getGateway(HtmlEmail.class);
            for (final InternetAddress address : iAddressRecipients) {
                try {
                    MailTemplate mailTemplate = MailTemplate.create(emailTemplatePathWithAttachment, resourceResolver.adaptTo(Session.class));
                    HtmlEmail email = mailTemplate.getEmail(StrLookup.mapLookup(params), HtmlEmail.class);
                    Resource csvResource = resourceResolver.getResource(csvReport);
                    if (csvResource != null) {
                        Resource csvResourceOriginal = resourceResolver.getResource(csvReport + "/jcr:content/renditions/original");
                        ByteArrayDataSource imageDS = getByteArrayDataSource(csvResourceOriginal);
                        if (imageDS != null) {
                            email.attach(imageDS, csvResource.getName(), StringUtils.EMPTY);
                        }
                    }
                    email.setTo(Collections.singleton(address));
                    messageGateway.send(email);
                } catch (IOException | EmailException | MessagingException e) {
                    LOGGER.error("Error in sending email to [ {} ]", address, e);
                } catch (RepositoryException e) {
                    LOGGER.error("Failed to get Stream of {}", csvReport, e);
                }
            }
        }
    }

    private ByteArrayDataSource getByteArrayDataSource(Resource csvResourceOriginal) throws RepositoryException, IOException {
        if (csvResourceOriginal != null) {
            Node csvNode = csvResourceOriginal.adaptTo(Node.class);
            if (csvNode != null) {
                Node contentNode = csvNode.getNode("jcr:content");
                if (contentNode.hasProperty(JCR_DATA)) {
                    Binary imageBinary = contentNode.getProperty(JCR_DATA).getBinary();
                    InputStream imageStream = imageBinary.getStream();
                    return new ByteArrayDataSource(imageStream, "text/csv");
                }
            }
        }
        return null;
    }

    @Override
    public boolean isEmailServiceEnabled() {
        return isEnabled;
    }
}

And its configuration interface:

@ObjectClassDefinition(name = "Lab2020 - Email Configuration")
public @interface EmailServiceConfiguration {

    @AttributeDefinition(name = "List of addresses to send the report email")
    String[] getListOfAddresses();

    @AttributeDefinition(name = "Report Email Template Path for Email with attachment")
    String getEmailTemplatePathWithAttachment();

    @AttributeDefinition(name = "Enable email sending", type = AttributeType.BOOLEAN)
    boolean isEnabled() default false;
}
# Configuration created by Apache Sling JCR Installer
getEmailTemplatePathWithAttachment="/conf/Lab2020/settings/email/report-with-attachment.txt"
getListOfAddresses=["local-part1@domain","local-part2@domain"]
isEnabled="true"

In this example, for the “getListOfAddresses” value, we used our emails since it was an internal process.

Going back to the email service, what we are doing is:

  • Prepare the InternetAddress array containing the emails configured (Lines 74 – 82)
  • Use HtmlEmail message type (Line 95)
  • Create the email template with the configured path (Line 98)
  • Create the HTML email replacing the dynamic values (Line 99)
  • Get the CSV resource and attach it (Lines 100 – 105 and 119-127)
  • Set the address and send the email (Lines 108 – 109)

Our email was really simple:

Subject: ${importEntity} Import Report - Lab2020

Dear customer,
The import process is finished. Please find attached the report.

-------------------------------------------------------
This is an automatic generated message. Please do not reply.

And of course you can add HTML code as well.

To test it locally, I’d suggest to download a FakeSMPT server, you can easily find it on Google 😊 Then configure the CQ Mail Service with “localhost” as server and the port you use.

That’s all! This works on the latest AEM versions.

Cheers! 🍻

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: