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