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