001/* 002 * Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the Eclipse Public License v1.0 006 * which accompanies this distribution, and is available at 007 * http://www.eclipse.org/legal/epl-v10.html 008 * 009 * Contributors: 010 * bstefanescu 011 */ 012package org.nuxeo.ecm.automation.core.operations.notification; 013 014import java.io.IOException; 015import java.io.InputStream; 016import java.net.URL; 017import java.util.ArrayList; 018import java.util.List; 019import java.util.Map; 020 021import javax.mail.MessagingException; 022import javax.mail.Session; 023 024import org.apache.commons.lang.StringEscapeUtils; 025import org.apache.commons.logging.Log; 026import org.apache.commons.logging.LogFactory; 027import org.nuxeo.common.utils.FileUtils; 028import org.nuxeo.ecm.automation.OperationContext; 029import org.nuxeo.ecm.automation.OperationException; 030import org.nuxeo.ecm.automation.core.Constants; 031import org.nuxeo.ecm.automation.core.annotations.Context; 032import org.nuxeo.ecm.automation.core.annotations.Operation; 033import org.nuxeo.ecm.automation.core.annotations.OperationMethod; 034import org.nuxeo.ecm.automation.core.annotations.Param; 035import org.nuxeo.ecm.automation.core.collectors.DocumentModelCollector; 036import org.nuxeo.ecm.automation.core.mail.Composer; 037import org.nuxeo.ecm.automation.core.mail.Mailer; 038import org.nuxeo.ecm.automation.core.mail.Mailer.Message; 039import org.nuxeo.ecm.automation.core.mail.Mailer.Message.AS; 040import org.nuxeo.ecm.automation.core.scripting.Scripting; 041import org.nuxeo.ecm.automation.core.util.StringList; 042import org.nuxeo.ecm.core.api.Blob; 043import org.nuxeo.ecm.core.api.DocumentModel; 044import org.nuxeo.ecm.core.api.NuxeoException; 045import org.nuxeo.ecm.core.api.PropertyException; 046import org.nuxeo.ecm.core.api.model.Property; 047import org.nuxeo.ecm.core.api.model.impl.ListProperty; 048import org.nuxeo.ecm.core.api.model.impl.MapProperty; 049import org.nuxeo.ecm.core.api.model.impl.primitives.BlobProperty; 050import org.nuxeo.ecm.platform.ec.notification.service.NotificationServiceHelper; 051import org.nuxeo.ecm.platform.rendering.api.RenderingException; 052import org.nuxeo.ecm.platform.usermanager.UserManager; 053import org.nuxeo.runtime.api.Framework; 054 055import freemarker.template.TemplateException; 056 057/** 058 * Save the session - TODO remove this? 059 * 060 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a> 061 */ 062@Operation(id = SendMail.ID, category = Constants.CAT_NOTIFICATION, label = "Send E-Mail", description = "Send an email using the input document to the specified recipients. You can use the HTML parameter to specify whether you message is in HTML format or in plain text. Also you can attach any blob on the current document to the message by using the comma separated list of xpath expressions 'files'. If you xpath points to a blob list all blobs in the list will be attached. Return back the input document(s). If rollbackOnError is true, the whole chain will be rollbacked if an error occurs while trying to send the email (for instance if no SMTP server is configured), else a simple warning will be logged and the chain will continue.", aliases = { "Notification.SendMail" }) 063public class SendMail { 064 065 protected static final Log log = LogFactory.getLog(SendMail.class); 066 067 public static final Composer COMPOSER = new Composer(); 068 069 public static final String ID = "Document.Mail"; 070 071 @Context 072 protected OperationContext ctx; 073 074 @Context 075 protected UserManager umgr; 076 077 @Param(name = "from") 078 protected String from; 079 080 @Param(name = "to") 081 protected StringList to; 082 083 // Useful for tests. 084 protected Session mailSession; 085 086 /** 087 * @since 5.9.1 088 */ 089 @Param(name = "cc", required = false) 090 protected StringList cc; 091 092 /** 093 * @since 5.9.1 094 */ 095 @Param(name = "bcc", required = false) 096 protected StringList bcc; 097 098 /** 099 * @since 5.9.1 100 */ 101 @Param(name = "replyto", required = false) 102 protected StringList replyto; 103 104 @Param(name = "subject") 105 protected String subject; 106 107 @Param(name = "message", widget = Constants.W_MAIL_TEMPLATE) 108 protected String message; 109 110 @Param(name = "HTML", required = false, values = { "false" }) 111 protected boolean asHtml = false; 112 113 @Param(name = "files", required = false) 114 protected StringList blobXpath; 115 116 @Param(name = "rollbackOnError", required = false, values = { "true" }) 117 protected boolean rollbackOnError = true; 118 119 /** 120 * @since 5.9.1 121 */ 122 @Param(name = "Strict User Resolution", required = false) 123 protected boolean isStrict = true; 124 125 @Param(name = "viewId", required = false, values = { "view_documents" }) 126 protected String viewId = "view_documents"; 127 128 @OperationMethod(collector = DocumentModelCollector.class) 129 public DocumentModel run(DocumentModel doc) throws TemplateException, RenderingException, OperationException, 130 MessagingException, IOException { 131 send(doc); 132 return doc; 133 } 134 135 protected String getContent() throws OperationException, IOException { 136 message = message.trim(); 137 if (message.startsWith("template:")) { 138 String name = message.substring("template:".length()).trim(); 139 URL url = MailTemplateHelper.getTemplate(name); 140 if (url == null) { 141 throw new OperationException("No such mail template: " + name); 142 } 143 InputStream in = url.openStream(); 144 return FileUtils.read(in); 145 } else { 146 return StringEscapeUtils.unescapeHtml(message); 147 } 148 } 149 150 protected void send(DocumentModel doc) throws TemplateException, RenderingException, OperationException, 151 MessagingException, IOException { 152 // TODO should sent one by one to each recipient? and have the template 153 // rendered for each recipient? Use: "mailto" var name? 154 try { 155 Map<String, Object> map = Scripting.initBindings(ctx); 156 // do not use document wrapper which is working only in mvel. 157 map.put("Document", doc); 158 map.put("docUrl", MailTemplateHelper.getDocumentUrl(doc, viewId)); 159 map.put("subject", subject); 160 map.put("to", to); 161 map.put("toResolved", MailBox.fetchPersonsFromList(to, isStrict)); 162 map.put("from", from); 163 map.put("fromResolved", MailBox.fetchPersonsFromString(from, isStrict)); 164 map.put("from", cc); 165 map.put("fromResolved", MailBox.fetchPersonsFromList(cc, isStrict)); 166 map.put("from", bcc); 167 map.put("fromResolved", MailBox.fetchPersonsFromList(bcc, isStrict)); 168 map.put("from", replyto); 169 map.put("fromResolved", MailBox.fetchPersonsFromList(replyto, isStrict)); 170 map.put("viewId", viewId); 171 map.put("baseUrl", NotificationServiceHelper.getNotificationService().getServerUrlPrefix()); 172 map.put("Runtime", Framework.getRuntime()); 173 Mailer.Message msg = createMessage(doc, getContent(), map); 174 msg.setSubject(subject, "UTF-8"); 175 176 addMailBoxInfo(msg); 177 178 msg.send(); 179 } catch (NuxeoException | TemplateException | RenderingException | OperationException | MessagingException 180 | IOException e) { 181 if (rollbackOnError) { 182 throw e; 183 } else { 184 log.warn( 185 String.format( 186 "An error occured while trying to execute the %s operation, see complete stack trace below. Continuing chain since 'rollbackOnError' was set to false.", 187 ID), e); 188 } 189 } 190 } 191 192 /** 193 * @since 5.9.1 194 */ 195 private void addMailBoxInfo(Mailer.Message msg) throws MessagingException { 196 List<MailBox> persons = MailBox.fetchPersonsFromString(from, isStrict); 197 addMailBoxInfoInMessageHeader(msg, AS.FROM, persons); 198 199 persons = MailBox.fetchPersonsFromList(to, isStrict); 200 addMailBoxInfoInMessageHeader(msg, AS.TO, persons); 201 202 persons = MailBox.fetchPersonsFromList(cc, isStrict); 203 addMailBoxInfoInMessageHeader(msg, AS.CC, persons); 204 205 persons = MailBox.fetchPersonsFromList(bcc, isStrict); 206 addMailBoxInfoInMessageHeader(msg, AS.BCC, persons); 207 208 if (replyto != null && !replyto.isEmpty()) { 209 msg.setReplyTo(null); 210 persons = MailBox.fetchPersonsFromList(replyto, isStrict); 211 addMailBoxInfoInMessageHeader(msg, AS.REPLYTO, persons); 212 } 213 } 214 215 /** 216 * @since 5.9.1 217 */ 218 private void addMailBoxInfoInMessageHeader(Message msg, AS as, List<MailBox> persons) throws MessagingException { 219 for (MailBox person : persons) { 220 msg.addInfoInMessageHeader(person.toString(), as); 221 } 222 } 223 224 protected Mailer.Message createMessage(DocumentModel doc, String message, Map<String, Object> map) 225 throws MessagingException, TemplateException, RenderingException, IOException { 226 if (blobXpath == null) { 227 if (asHtml) { 228 return COMPOSER.newHtmlMessage(message, map); 229 } else { 230 return COMPOSER.newTextMessage(message, map); 231 } 232 } else { 233 ArrayList<Blob> blobs = new ArrayList<Blob>(); 234 for (String xpath : blobXpath) { 235 try { 236 Property p = doc.getProperty(xpath); 237 if (p instanceof BlobProperty) { 238 getBlob(p.getValue(), blobs); 239 } else if (p instanceof ListProperty) { 240 for (Property pp : p) { 241 getBlob(pp.getValue(), blobs); 242 } 243 } else if (p instanceof MapProperty) { 244 for (Property sp : ((MapProperty) p).values()) { 245 getBlob(sp.getValue(), blobs); 246 } 247 } else { 248 Object o = p.getValue(); 249 if (o instanceof Blob) { 250 blobs.add((Blob) o); 251 } 252 } 253 } catch (PropertyException pe) { 254 log.error("Error while fetching blobs: " + pe.getMessage()); 255 log.debug(pe, pe); 256 continue; 257 } 258 } 259 return COMPOSER.newMixedMessage(message, map, asHtml ? "html" : "plain", blobs); 260 } 261 } 262 263 /** 264 * @since 5.7 265 * @param o: the object to introspect to find a blob 266 * @param blobs: the Blob list where the blobs are put during property introspection 267 */ 268 @SuppressWarnings("unchecked") 269 private void getBlob(Object o, List<Blob> blobs) { 270 if (o instanceof List) { 271 for (Object item : (List<Object>) o) { 272 getBlob(item, blobs); 273 } 274 } else if (o instanceof Map) { 275 for (Object item : ((Map<String, Object>) o).values()) { 276 getBlob(item, blobs); 277 } 278 } else if (o instanceof Blob) { 279 blobs.add((Blob) o); 280 } 281 282 } 283}