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