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