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