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