001/*
002 * (C) Copyright 2006-2008 Nuxeo SAS (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Alexandre Russel
016 *
017 * $Id$
018 */
019
020package org.nuxeo.ecm.platform.mail.action;
021
022import java.io.IOException;
023import java.io.InputStream;
024import java.io.UnsupportedEncodingException;
025import java.util.ArrayList;
026import java.util.HashMap;
027import java.util.List;
028import java.util.Map;
029
030import javax.mail.Address;
031import javax.mail.Message;
032import javax.mail.MessagingException;
033import javax.mail.Part;
034import javax.mail.internet.MimeMessage;
035import javax.mail.internet.MimeMultipart;
036import javax.mail.internet.MimeUtility;
037
038import org.apache.commons.logging.Log;
039import org.apache.commons.logging.LogFactory;
040import org.nuxeo.ecm.core.api.Blob;
041import org.nuxeo.ecm.core.api.Blobs;
042
043/**
044 * Transforms the message using the transformer and puts it in the context under transformed.
045 *
046 * @author Alexandre Russel
047 */
048public class TransformMessageAction implements MessageAction {
049
050    private static final Log log = LogFactory.getLog(TransformMessageAction.class);
051
052    protected final Map<String, Map<String, Object>> schemas = new HashMap<String, Map<String, Object>>();
053
054    protected final Map<String, Object> mailSchema = new HashMap<String, Object>();
055
056    protected final Map<String, Object> dcSchema = new HashMap<String, Object>();
057
058    protected final Map<String, Object> filesSchema = new HashMap<String, Object>();
059
060    protected final List<Map<String, Object>> files = new ArrayList<Map<String, Object>>();
061
062    protected StringBuilder text = new StringBuilder();
063
064    private final HashMap<String, List<Part>> messageBodyParts = new HashMap<String, List<Part>>();
065
066    public TransformMessageAction() {
067        messageBodyParts.put("text", new ArrayList<Part>());
068        messageBodyParts.put("html", new ArrayList<Part>());
069        schemas.put("mail", mailSchema);
070        schemas.put("dublincore", dcSchema);
071        filesSchema.put("files", files);
072        schemas.put("files", filesSchema);
073    }
074
075    public boolean execute(ExecutionContext context) throws MessagingException {
076        Message message = context.getMessage();
077        if (log.isDebugEnabled()) {
078            log.debug("Transforming message" + message.getSubject());
079        }
080        if (message.getFrom() != null && message.getFrom().length != 0) {
081            List<String> contributors = new ArrayList<String>();
082            for (Address ad : message.getFrom()) {
083                contributors.add(safelyDecodeText(ad.toString()));
084            }
085            dcSchema.put("contributors", contributors);
086            dcSchema.put("creator", contributors.get(0));
087            dcSchema.put("created", message.getReceivedDate());
088        }
089        if (message.getAllRecipients() != null && message.getAllRecipients().length != 0) {
090            List<String> recipients = new ArrayList<String>();
091            for (Address address : message.getAllRecipients()) {
092                recipients.add(safelyDecodeText(address.toString()));
093            }
094            mailSchema.put("recipients", recipients);
095        }
096        if (message instanceof MimeMessage) {
097            try {
098                processMimeMessage((MimeMessage) message);
099            } catch (IOException e) {
100                throw new MessagingException(e.getMessage(), e);
101            }
102        }
103        mailSchema.put("text", text.toString());
104        dcSchema.put("title", message.getSubject());
105        context.put("transformed", schemas);
106        return true;
107    }
108
109    private void processMimeMessage(MimeMessage message) throws MessagingException, IOException {
110        Object object = message.getContent();
111        if (object instanceof String) {
112            addToTextMessage(message.getContent().toString(), true);
113        } else if (object instanceof MimeMultipart) {
114            processMultipartMessage((MimeMultipart) object);
115            processSavedTextMessageBody();
116        }
117    }
118
119    private void processMultipartMessage(MimeMultipart parts) throws MessagingException, IOException {
120        log.debug("processing multipart message.");
121        for (int i = 0; i < parts.getCount(); i++) {
122            Part part = parts.getBodyPart(i);
123            if (part.getDataHandler().getContent() instanceof MimeMultipart) {
124                log.debug("** found embedded multipart message");
125                processMultipartMessage((MimeMultipart) part.getDataHandler().getContent());
126                continue;
127            }
128            log.debug("processing single part message: " + part.getClass());
129            processSingleMessagePart(part);
130        }
131    }
132
133    private void processSingleMessagePart(Part part) throws MessagingException, IOException {
134        String partContentType = part.getContentType();
135        String partFileName = getFileName(part);
136
137        if (partFileName != null) {
138            log.debug("Add named attachment: " + partFileName);
139            setFile(partFileName, part.getInputStream());
140            return;
141        }
142
143        if (!contentTypeIsReadableText(partContentType)) {
144            log.debug("Add unnamed binary attachment.");
145            setFile(null, part.getInputStream());
146            return;
147        }
148
149        if (contentTypeIsPlainText(partContentType)) {
150            log.debug("found plain text unnamed attachment [save for later processing]");
151            messageBodyParts.get("text").add(part);
152            return;
153        }
154
155        log.debug("found html unnamed attachment [save for later processing]");
156        messageBodyParts.get("html").add(part);
157    }
158
159    private void processSavedTextMessageBody() throws MessagingException, IOException {
160        if (messageBodyParts.get("text").isEmpty()) {
161            log.debug("entering case 2: no plain text found -> html is the body of the message.");
162            addPartsToTextMessage(messageBodyParts.get("html"));
163        } else {
164            log.debug("entering case 1: text is saved as message body and html as attachment.");
165            addPartsToTextMessage(messageBodyParts.get("text"));
166            addPartsAsAttachements(messageBodyParts.get("html"));
167        }
168    }
169
170    private void addPartsToTextMessage(List<Part> someMessageParts) throws MessagingException, IOException {
171        for (Part part : someMessageParts) {
172            addToTextMessage(part.getContent().toString(), contentTypeIsPlainText(part.getContentType()));
173        }
174    }
175
176    private void addPartsAsAttachements(List<Part> someMessageParts) throws MessagingException, IOException {
177        for (Part part : someMessageParts) {
178            setFile(getFileName(part), part.getInputStream());
179        }
180    }
181
182    private static boolean contentTypeIsReadableText(String contentType) {
183        boolean isText = contentTypeIsPlainText(contentType);
184        boolean isHTML = contentTypeIsHtml(contentType);
185        return isText || isHTML;
186    }
187
188    private static boolean contentTypeIsHtml(String contentType) {
189        contentType = contentType.trim().toLowerCase();
190        return contentType.startsWith("text/html");
191    }
192
193    private static boolean contentTypeIsPlainText(String contentType) {
194        contentType = contentType.trim().toLowerCase();
195        return contentType.startsWith("text/plain");
196    }
197
198    /**
199     * "javax.mail.internet.MimeBodyPart" is decoding the file name (with special characters) if it has the
200     * "mail.mime.decodefilename" sysstem property set but the "com.sun.mail.imap.IMAPBodyPart" subclass of MimeBodyPart
201     * is overriding getFileName() and never deal with encoded file names. the filename is decoded with the utility
202     * function: MimeUtility.decodeText(filename); so we force here a filename decode. MimeUtility.decodeText is doing
203     * nothing if the text is not encoded
204     */
205    private static String getFileName(Part mailPart) throws MessagingException {
206        String sysPropertyVal = System.getProperty("mail.mime.decodefilename");
207        boolean decodeFileName = sysPropertyVal != null && !sysPropertyVal.equalsIgnoreCase("false");
208
209        String encodedFilename = mailPart.getFileName();
210
211        if (!decodeFileName || encodedFilename == null) {
212            return encodedFilename;
213        }
214
215        try {
216            return MimeUtility.decodeText(encodedFilename);
217        } catch (UnsupportedEncodingException ex) {
218            throw new MessagingException("Can't decode attachment filename.", ex);
219        }
220    }
221
222    private static String safelyDecodeText(String textToDecode) {
223        try {
224            return MimeUtility.decodeText(textToDecode);
225        } catch (UnsupportedEncodingException ex) {
226            log.error("Can't decode text. Use undecoded!", ex);
227            return textToDecode;
228        }
229    }
230
231    private void setFile(String fileName, InputStream inputStream) throws IOException {
232        log.debug("* adding attachment: " + fileName);
233        Map<String, Object> map = new HashMap<String, Object>();
234        Blob fileBlob = Blobs.createBlob(inputStream);
235        map.put("file", fileBlob);
236        map.put("filename", fileName);
237        files.add(map);
238    }
239
240    private void addToTextMessage(String message, boolean isPlainText) {
241        log.debug("* adding text to message body: " + message);
242        // if(isPlainText){
243        // message = "<pre>" + message + "</pre>";
244        // }
245        text.append(message);
246    }
247
248    public void reset(ExecutionContext context) {
249        mailSchema.clear();
250        dcSchema.clear();
251        files.clear();
252        text = new StringBuilder();
253
254        messageBodyParts.get("text").clear();
255        messageBodyParts.get("html").clear();
256    }
257
258}