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}