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}