AEM 6 SP2 - Email Team on New Project Creation

Goal


New in AEM 6 is Projects http://localhost:4502/projects.html. Projects console can be used to add members, assign work, track progress etc. This post is on emailing team members when a project is created

Thank you ACS Commons for email code snippets

Demo | Package Install | Source Code


Project creation success modal





Email sent to a member



Solution


For this feature to work, CQ mailing service - com.day.cq.mailer.DefaultMailService should be configured with SMTP details. In the following steps we use Gmail for SMTP and a test account experience.aem@gmail.com as both sender and recipient (when project is created)


Create Gmail Account

1) Create a new gmail account for testing purposes, say experience.aem@gmail.com . Use this account as both sender and recipient

2) Make sure you tone down the security of test gmail account a bit, by clicking Turn on of Access for less secure apps (https://www.google.com/settings/security/lesssecureapps)





Set up CQ Mail Service

1) Configure the CQ Mail Service with Gmail SMTP credentials by accessing Felix config manager (http://localhost:4502/system/console/configMgr/com.day.cq.mailer.DefaultMailService) or create a sling:osgiConfig for com.day.cq.mailer.DefaultMailService with these settings





2) If emails are NOT sent by CQ Mailing service, make sure CQ can reach smtp.gmail.com. Check ports etc. In my case the problem was company vpn; if connected to vpn, CQ running on the machine could not reach smtp.gmail.com

3) Configure test users with email addresses





Download Projects Api Jar

As of this writing, the api jar for Projects is not available in Adobe nexus repo (https://repo.adobe.com/nexus). For build purposes, copy jar com.adobe.cq.projects.api-0.0.14.jar from CQ install (search, find in author\crx-quickstart\launchpad\felix\bundle392\data\install) and copy it to local maven repo (windows - C:\Users\<user>\.m2\repository\com\adobe\cq\projects\com.adobe.cq.projects.api\0.0.14)





The Extension

1) Create a servlet apps.experienceaem.projects.SendProjectCreateEmail in CRX folder /apps/touchui-create-project-send-email for send email call when Email Team button is clicked (added in next steps), after project create

package apps.experienceaem.projects;

import com.adobe.cq.projects.api.Project;
import com.adobe.cq.projects.api.ProjectMember;
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.Email;
import org.apache.commons.mail.HtmlEmail;
import org.apache.commons.mail.SimpleEmail;
import org.apache.felix.scr.annotations.*;
import org.apache.felix.scr.annotations.Properties;
import org.apache.jackrabbit.api.security.user.Authorizable;
import org.apache.jackrabbit.api.security.user.Group;
import org.apache.jackrabbit.api.security.user.UserManager;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.commons.json.JSONArray;
import org.apache.sling.jcr.base.util.AccessControlUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Session;
import javax.mail.internet.InternetAddress;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.*;

@Component(metatype = false, label = "Experience AEM Project Create Email Servlet", description = "")
@Service
@Properties({
    @Property(name = "sling.servlet.methods", value = {"GET"}, propertyPrivate = true),
    @Property(name = "sling.servlet.paths", value = "/bin/experience-aem/send-project-create-email", propertyPrivate = true),
    @Property(name = "sling.servlet.extensions", value = "json", propertyPrivate = true)
})
public class SendProjectCreateEmail extends SlingAllMethodsServlet {

    private static final Logger log = LoggerFactory.getLogger(SendProjectCreateEmail.class);

    @Reference
    private ResourceResolverFactory rrFactory;

    @Reference
    private MessageGatewayService messageGatewayService;

    private static String TEMPLATE_PATH = "/apps/touchui-create-project-send-email/mail/template.html";
    private static String SENDER_EMAIL = "experience.aem@gmail.com";
    private static String SENDER_NAME = "Experience AEM";
    private static String SENDER_EMAIL_ADDRESS = "senderEmailAddress";

    public String sendMail(ResourceResolver resolver, Resource projectRes, String recipientEmail,
                         String recipientName){
        if(StringUtils.isEmpty(recipientEmail)){
            throw new RuntimeException("Empty email");
        }

        if(StringUtils.isEmpty(recipientName)){
            recipientName = recipientEmail;
        }

        try{
            Project project = projectRes.adaptTo(Project.class);
            Map<String, String> emailParams = new HashMap<String,String>();

            emailParams.put(SENDER_EMAIL_ADDRESS, SENDER_EMAIL);
            emailParams.put("senderName", SENDER_NAME);
            emailParams.put("projectName", project.getTitle());
            emailParams.put("recipientName", recipientName);
            emailParams.put("body","Project Created - <a href='http://localhost:4502/projects/details.html"
                                            + projectRes.getPath() + "'>" + project.getTitle() + "</a>");
            emailParams.put("projectCreator", projectRes.adaptTo(ValueMap.class).get("jcr:createdBy", ""));

            send(resolver, emailParams, recipientEmail);
        }catch(Exception e){
            log.error("Error sending email to " + recipientEmail, e);
            recipientEmail = "";
        }

        return recipientEmail;
    }

    public Map<String, String> getMemberEmails(ResourceResolver resolver, Project project) throws Exception{
        Map<String, String> members = new LinkedHashMap<String, String>();
        String name = null, email = null;

        UserManager um = AccessControlUtil.getUserManager(resolver.adaptTo(Session.class));
        ValueMap profile = null; Iterator<Authorizable> itr = null;
        List<Authorizable> users = new ArrayList<Authorizable>();

        for(ProjectMember member : project.getMembers()) {
            Authorizable user = um.getAuthorizable(member.getId());

            if(user instanceof Group){
                itr = ((Group)user).getMembers();

                while(itr.hasNext()) {
                    users.add(itr.next());
                }
            }else{
                users.add(user);
            }
        }

        for(Authorizable user : users){
            profile = resolver.getResource(user.getPath() + "/profile").adaptTo(ValueMap.class);

            email = profile.get("email", "");

            if(StringUtils.isEmpty(email)){
                continue;
            }

            name = profile.get("familyName", "") + " " + profile.get("givenName", "");

            if(StringUtils.isEmpty(name.trim())){
                name = user.getID();
            }

            members.put(name, email);
        }

        return members;
    }

    private Email send(ResourceResolver resolver, Map<String, String> emailParams,
                       String recipientEmail) throws Exception{

        MailTemplate mailTemplate = MailTemplate.create(TEMPLATE_PATH, resolver.adaptTo(Session.class));

        if (mailTemplate == null) {
            throw new Exception("Template missing - " + TEMPLATE_PATH);
        }

        Email email = mailTemplate.getEmail(StrLookup.mapLookup(emailParams), HtmlEmail.class);

        email.setTo(Collections.singleton(new InternetAddress(recipientEmail)));
        email.setFrom(SENDER_EMAIL);

        MessageGateway<Email> messageGateway = messageGatewayService.getGateway(email.getClass());

        messageGateway.send(email);

        return email;
    }

    public void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
                            throws ServletException, IOException{
        ResourceResolver resolver = request.getResourceResolver();

        String projectPath = request.getParameter("projectPath");

        try{
            if(StringUtils.isEmpty(projectPath)){
                throw new RuntimeException("Empty projectPath");
            }

            Resource res = resolver.getResource(projectPath);

            if(res == null){
                throw new Exception("Project not found - " + projectPath);
            }

            Project project = res.adaptTo(Project.class);

            Map<String, String> members = getMemberEmails(resolver, project);
            String recipientEmail = null;
            JSONArray output = new JSONArray();

            for(Map.Entry<String, String> member : members.entrySet()){
                recipientEmail = sendMail(resolver, res, member.getValue(), member.getKey());

                if(StringUtils.isEmpty(recipientEmail)){
                    continue;
                }

                output.put(recipientEmail);
            }

            response.getWriter().print("{ success : " + output.toString() + " }");
        }catch(Exception e){
            log.error("Error sending email for project create - " + projectPath, e);
            response.getWriter().print("{ error : 'error sending email' }");
        }
    }
}


2) #72 to #78 parameters are needed for email template, created in next step

3) #172 getMemberEmails() call uses Projects api to get a list of members, members in groups, addresses for sending emails

4) Create a template html /apps/touchui-create-project-send-email/mail/template.html for email body with following code. The ${} placeholders are replaced with parameter values

From: ${senderName} <${senderEmailAddress}>
Subject: ${projectName} project created

Hello ${recipientName}

${body}

From
${projectCreator}

5) Create clientlib (cq:ClientLibraryFolder) /apps/touchui-create-project-send-email/clientlib with categories cq.projects.admin.createprojectwizard

6) Create clientlib js file /apps/touchui-create-project-send-email/clientlib/send-email.js, add the following code

(function(window, $) {
    var MODAL = "modal",
        OPEN_PROJECT_TEXT = "Open project",
        EMAIL_SERVLET = "/bin/experience-aem/send-project-create-email?projectPath=";

    var modalPlugin = $.fn[MODAL],
        ui = $(window).adaptTo("foundation-ui");

    function emailTeam(path){
        if(path.indexOf("/content") < 0){
            return;
        }

        var projectPath = path.substring(path.indexOf("/content"));

        $.ajax( EMAIL_SERVLET + projectPath).done(handler);

        function handler(data){
            if(data.success){
                document.location = path;
                return;
            }

            ui.alert("Error", "Error emailing team", "error");
        }
    }

    //there could be many ways to intercept project creation ajax, i just thought the following is cleaner
    function modalOverride(optionsIn){
        modalPlugin.call(this, optionsIn);

        var $element = $(this);

        if($element.length == 0){
            return;
        }

        var $openProject = $element.find(".coral-Button--primary");

        if($openProject.html() != OPEN_PROJECT_TEXT){
            return;
        }

        var path = $openProject.attr("href");

        $openProject.attr("href", "").html("Email Team").click( function(){
            emailTeam(path);
        } ) ;
    }

    $.fn[MODAL] = modalOverride;
})(window, Granite.$);

7) AEM's create project submit function was added inside a closure in /libs/cq/gui/components/projects/admin/createprojectwizard/clientlibs/js/createprojectwizard.js and not available for extension.  #51 intercepts the result of create project call, by extending modal plugin and making necessary changes to button, for sending email

3 comments:

  1. Are you finding for Adobe Customer Service Number? If yes then check out the Adobe Usa,Canada helpline number @+1877-339-8403 and get most reliable Adobe technical support services.

    ReplyDelete
  2. very nice article works fine for me

    ReplyDelete
  3. how can we make it workable in AEM 6.4 Please help if anybody has any information .

    ReplyDelete