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