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