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) or create a sling:osgiConfig for com.day.cq.mailer.DefaultMailService with these settings
2) If emails are not being 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 connect to smtp.gmail.com
3) Configure some test accounts with email addresses
Download Project 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 making the send email call when Send Email 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 sender and recipients emails
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 clienlib 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. To capture the result of call, #51 intercepts the result of create project call, by extending the modal and making necessary changes to button, for sending email