001/* 002 * (C) Copyright 2006-2018 Nuxeo (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 static java.nio.charset.StandardCharsets.UTF_8; 022 023import java.io.IOException; 024import java.io.InputStream; 025import java.net.URL; 026import java.util.ArrayList; 027import java.util.Date; 028import java.util.List; 029import java.util.Map; 030 031import javax.mail.MessagingException; 032import javax.mail.Session; 033import javax.ws.rs.core.UriBuilder; 034 035import org.apache.commons.io.IOUtils; 036import org.apache.commons.logging.Log; 037import org.apache.commons.logging.LogFactory; 038import org.apache.commons.text.StringEscapeUtils; 039import org.nuxeo.ecm.automation.OperationContext; 040import org.nuxeo.ecm.automation.OperationException; 041import org.nuxeo.ecm.automation.core.Constants; 042import org.nuxeo.ecm.automation.core.annotations.Context; 043import org.nuxeo.ecm.automation.core.annotations.Operation; 044import org.nuxeo.ecm.automation.core.annotations.OperationMethod; 045import org.nuxeo.ecm.automation.core.annotations.Param; 046import org.nuxeo.ecm.automation.core.collectors.DocumentModelCollector; 047import org.nuxeo.ecm.automation.core.mail.Composer; 048import org.nuxeo.ecm.automation.core.mail.Mailer; 049import org.nuxeo.ecm.automation.core.mail.Mailer.Message; 050import org.nuxeo.ecm.automation.core.mail.Mailer.Message.AS; 051import org.nuxeo.ecm.automation.core.scripting.Scripting; 052import org.nuxeo.ecm.automation.core.util.StringList; 053import org.nuxeo.ecm.core.api.Blob; 054import org.nuxeo.ecm.core.api.DocumentModel; 055import org.nuxeo.ecm.core.api.NuxeoException; 056import org.nuxeo.ecm.core.api.PropertyException; 057import org.nuxeo.ecm.core.api.model.Property; 058import org.nuxeo.ecm.core.api.model.impl.ListProperty; 059import org.nuxeo.ecm.core.api.model.impl.MapProperty; 060import org.nuxeo.ecm.core.api.model.impl.primitives.BlobProperty; 061import org.nuxeo.ecm.platform.ec.notification.service.NotificationServiceHelper; 062import org.nuxeo.ecm.platform.rendering.api.RenderingException; 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 @Param(name = "from") 086 protected String from; 087 088 @Param(name = "to", required = false) 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) 138 throws TemplateException, RenderingException, OperationException, 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 try (InputStream in = url.openStream()) { 152 return IOUtils.toString(in, UTF_8); 153 } 154 } else { 155 return StringEscapeUtils.unescapeHtml4(message); 156 } 157 } 158 159 protected void send(DocumentModel doc) 160 throws TemplateException, RenderingException, OperationException, MessagingException, IOException { 161 // TODO should sent one by one to each recipient? and have the template 162 // rendered for each recipient? Use: "mailto" var name? 163 try { 164 Map<String, Object> map = Scripting.initBindings(ctx); 165 // do not use document wrapper which is working only in mvel. 166 map.put("Document", doc); 167 map.put("docUrl", 168 createDocUrlWithToken(MailTemplateHelper.getDocumentUrl(doc, viewId), (String) map.get("token"))); 169 map.put("subject", subject); 170 map.put("to", to); 171 map.put("toResolved", MailBox.fetchPersonsFromList(to, isStrict)); 172 map.put("from", from); 173 map.put("fromResolved", MailBox.fetchPersonsFromString(from, isStrict)); 174 map.put("from", cc); 175 map.put("fromResolved", MailBox.fetchPersonsFromList(cc, isStrict)); 176 map.put("from", bcc); 177 map.put("fromResolved", MailBox.fetchPersonsFromList(bcc, isStrict)); 178 map.put("from", replyto); 179 map.put("fromResolved", MailBox.fetchPersonsFromList(replyto, isStrict)); 180 map.put("viewId", viewId); 181 map.put("baseUrl", NotificationServiceHelper.getNotificationService().getServerUrlPrefix()); 182 map.put("Runtime", Framework.getRuntime()); 183 Mailer.Message msg = createMessage(doc, getContent(), map); 184 msg.setSubject(subject, "UTF-8"); 185 msg.setSentDate(new Date()); 186 187 addMailBoxInfo(msg); 188 189 msg.send(); 190 } catch (NuxeoException | TemplateException | RenderingException | OperationException | MessagingException 191 | IOException e) { 192 if (rollbackOnError) { 193 throw e; 194 } else { 195 log.warn(String.format( 196 "An error occured while trying to execute the %s operation, see complete stack trace below. Continuing chain since 'rollbackOnError' was set to false.", 197 ID), e); 198 } 199 } 200 } 201 202 // Only visible for testing purposes 203 protected String createDocUrlWithToken(String documentUrl, String token) { 204 return token != null ? UriBuilder.fromUri(documentUrl).queryParam("token", token).build().toString() 205 : documentUrl; 206 } 207 208 /** 209 * @since 5.9.1 210 */ 211 private void addMailBoxInfo(Mailer.Message msg) throws MessagingException { 212 List<MailBox> persons = MailBox.fetchPersonsFromString(from, isStrict); 213 addMailBoxInfoInMessageHeader(msg, AS.FROM, persons); 214 215 persons = MailBox.fetchPersonsFromList(to, isStrict); 216 addMailBoxInfoInMessageHeader(msg, AS.TO, persons); 217 218 persons = MailBox.fetchPersonsFromList(cc, isStrict); 219 addMailBoxInfoInMessageHeader(msg, AS.CC, persons); 220 221 persons = MailBox.fetchPersonsFromList(bcc, isStrict); 222 addMailBoxInfoInMessageHeader(msg, AS.BCC, persons); 223 224 if (replyto != null && !replyto.isEmpty()) { 225 msg.setReplyTo(null); 226 persons = MailBox.fetchPersonsFromList(replyto, isStrict); 227 addMailBoxInfoInMessageHeader(msg, AS.REPLYTO, persons); 228 } 229 } 230 231 /** 232 * @since 5.9.1 233 */ 234 private void addMailBoxInfoInMessageHeader(Message msg, AS as, List<MailBox> persons) throws MessagingException { 235 for (MailBox person : persons) { 236 msg.addInfoInMessageHeader(person.toString(), as); 237 } 238 } 239 240 protected Mailer.Message createMessage(DocumentModel doc, String message, Map<String, Object> map) 241 throws MessagingException, TemplateException, RenderingException, IOException { 242 if (blobXpath == null) { 243 if (asHtml) { 244 return COMPOSER.newHtmlMessage(message, map); 245 } else { 246 return COMPOSER.newTextMessage(message, map); 247 } 248 } else { 249 List<Blob> blobs = new ArrayList<>(); 250 for (String xpath : blobXpath) { 251 try { 252 Property p = doc.getProperty(xpath); 253 if (p instanceof BlobProperty) { 254 getBlob(p.getValue(), blobs); 255 } else if (p instanceof ListProperty) { 256 for (Property pp : p) { 257 getBlob(pp.getValue(), blobs); 258 } 259 } else if (p instanceof MapProperty) { 260 for (Property sp : ((MapProperty) p).values()) { 261 getBlob(sp.getValue(), blobs); 262 } 263 } else { 264 Object o = p.getValue(); 265 if (o instanceof Blob) { 266 blobs.add((Blob) o); 267 } 268 } 269 } catch (PropertyException pe) { 270 log.error("Error while fetching blobs: " + pe.getMessage()); 271 log.debug(pe, pe); 272 } 273 } 274 return COMPOSER.newMixedMessage(message, map, asHtml ? "html" : "plain", blobs); 275 } 276 } 277 278 /** 279 * @since 5.7 280 * @param o: the object to introspect to find a blob 281 * @param blobs: the Blob list where the blobs are put during property introspection 282 */ 283 @SuppressWarnings("unchecked") 284 private void getBlob(Object o, List<Blob> blobs) { 285 if (o instanceof List) { 286 for (Object item : (List<Object>) o) { 287 getBlob(item, blobs); 288 } 289 } else if (o instanceof Map) { 290 for (Object item : ((Map<String, Object>) o).values()) { 291 getBlob(item, blobs); 292 } 293 } else if (o instanceof Blob) { 294 blobs.add((Blob) o); 295 } 296 297 } 298}