001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.mail.util;
018
019import javax.activation.DataHandler;
020import javax.activation.DataSource;
021import javax.mail.Message;
022import javax.mail.MessagingException;
023import javax.mail.Multipart;
024import javax.mail.Part;
025import javax.mail.internet.InternetAddress;
026import javax.mail.internet.MimeBodyPart;
027import javax.mail.internet.MimeMessage;
028import javax.mail.internet.MimePart;
029import javax.mail.internet.MimeUtility;
030import javax.mail.util.ByteArrayDataSource;
031import java.io.BufferedInputStream;
032import java.io.BufferedOutputStream;
033import java.io.ByteArrayOutputStream;
034import java.io.IOException;
035import java.io.InputStream;
036import java.io.UnsupportedEncodingException;
037import java.util.ArrayList;
038import java.util.Arrays;
039import java.util.List;
040
041/**
042 * Parses a MimeMessage and stores the individual parts such a plain text,
043 * HTML text and attachments.
044 *
045 * @since 1.3
046 * @version $Id: MimeMessageParser.java 1531118 2013-10-10 21:33:22Z tn $
047 */
048public class MimeMessageParser
049{
050    /** The MimeMessage to convert */
051    private final MimeMessage mimeMessage;
052
053    /** Plain mail content from MimeMessage */
054    private String plainContent;
055
056    /** Html mail content from MimeMessage */
057    private String htmlContent;
058
059    /** List of attachments of MimeMessage */
060    private final List<DataSource> attachmentList;
061
062    /** Is this a Multipart email */
063    private boolean isMultiPart;
064
065    /**
066     * Constructs an instance with the MimeMessage to be extracted.
067     *
068     * @param message the message to parse
069     */
070    public MimeMessageParser(final MimeMessage message)
071    {
072        attachmentList = new ArrayList<DataSource>();
073        this.mimeMessage = message;
074        this.isMultiPart = false;
075    }
076
077    /**
078     * Does the actual extraction.
079     *
080     * @return this instance
081     * @throws Exception parsing the mime message failed
082     */
083    public MimeMessageParser parse() throws Exception
084    {
085        this.parse(null, mimeMessage);
086        return this;
087    }
088
089    /**
090     * @return the 'to' recipients of the message
091     * @throws Exception determining the recipients failed
092     */
093    public List<javax.mail.Address> getTo() throws Exception
094    {
095        javax.mail.Address[] recipients = this.mimeMessage.getRecipients(Message.RecipientType.TO);
096        return recipients != null ? Arrays.asList(recipients) : new ArrayList<javax.mail.Address>();
097    }
098
099    /**
100     * @return the 'cc' recipients of the message
101     * @throws Exception determining the recipients failed
102     */
103    public List<javax.mail.Address> getCc() throws Exception
104    {
105        javax.mail.Address[] recipients = this.mimeMessage.getRecipients(Message.RecipientType.CC);
106        return recipients != null ? Arrays.asList(recipients) : new ArrayList<javax.mail.Address>();
107    }
108
109    /**
110     * @return the 'bcc' recipients of the message
111     * @throws Exception determining the recipients failed
112     */
113    public List<javax.mail.Address> getBcc() throws Exception
114    {
115        javax.mail.Address[] recipients = this.mimeMessage.getRecipients(Message.RecipientType.BCC);
116        return recipients != null ? Arrays.asList(recipients) : new ArrayList<javax.mail.Address>();
117    }
118
119    /**
120     * @return the 'from' field of the message
121     * @throws Exception parsing the mime message failed
122     */
123    public String getFrom() throws Exception
124    {
125        javax.mail.Address[] addresses = this.mimeMessage.getFrom();
126        if ((addresses == null) || (addresses.length == 0))
127        {
128            return null;
129        }
130        else
131        {
132            return ((InternetAddress) addresses[0]).getAddress();
133        }
134    }
135
136    /**
137     * @return the 'replyTo' address of the email
138     * @throws Exception parsing the mime message failed
139     */
140    public String getReplyTo() throws Exception
141    {
142        javax.mail.Address[] addresses = this.mimeMessage.getReplyTo();
143        if ((addresses == null) || (addresses.length == 0))
144        {
145            return null;
146        }
147        else
148        {
149            return ((InternetAddress) addresses[0]).getAddress();
150        }
151    }
152
153    /**
154     * @return the mail subject
155     * @throws Exception parsing the mime message failed
156     */
157    public String getSubject() throws Exception
158    {
159        return this.mimeMessage.getSubject();
160    }
161
162    /**
163     * Extracts the content of a MimeMessage recursively.
164     *
165     * @param parent the parent multi-part
166     * @param part   the current MimePart
167     * @throws MessagingException parsing the MimeMessage failed
168     * @throws IOException        parsing the MimeMessage failed
169     */
170    protected void parse(Multipart parent, MimePart part)
171        throws MessagingException, IOException
172    {
173        if (part.isMimeType("text/plain") && (plainContent == null)
174                && (!MimePart.ATTACHMENT.equalsIgnoreCase(part.getDisposition())))
175        {
176            plainContent = (String) part.getContent();
177        }
178        else
179        {
180            if (part.isMimeType("text/html") && (htmlContent == null)
181                    && (!MimePart.ATTACHMENT.equalsIgnoreCase(part.getDisposition())))
182            {
183                htmlContent = (String) part.getContent();
184            }
185            else
186            {
187                if (part.isMimeType("multipart/*"))
188                {
189                    this.isMultiPart = true;
190                    Multipart mp = (Multipart) part.getContent();
191                    int count = mp.getCount();
192
193                    // iterate over all MimeBodyPart
194
195                    for (int i = 0; i < count; i++)
196                    {
197                        parse(mp, (MimeBodyPart) mp.getBodyPart(i));
198                    }
199                }
200                else
201                {
202                    this.attachmentList.add(createDataSource(parent, part));
203                }
204            }
205        }
206    }
207
208    /**
209     * Parses the MimePart to create a DataSource.
210     *
211     * @param parent the parent multi-part
212     * @param part   the current part to be processed
213     * @return the DataSource
214     * @throws MessagingException creating the DataSource failed
215     * @throws IOException        creating the DataSource failed
216     */
217    protected DataSource createDataSource(Multipart parent, MimePart part)
218        throws MessagingException, IOException
219    {
220        DataHandler dataHandler = part.getDataHandler();
221        DataSource dataSource = dataHandler.getDataSource();
222        String contentType = getBaseMimeType(dataSource.getContentType());
223        byte[] content = this.getContent(dataSource.getInputStream());
224        ByteArrayDataSource result = new ByteArrayDataSource(content, contentType);
225        String dataSourceName = getDataSourceName(part, dataSource);
226
227        result.setName(dataSourceName);
228        return result;
229    }
230
231    /** @return Returns the mimeMessage. */
232    public MimeMessage getMimeMessage()
233    {
234        return mimeMessage;
235    }
236
237    /** @return Returns the isMultiPart. */
238    public boolean isMultipart()
239    {
240        return isMultiPart;
241    }
242
243    /** @return Returns the plainContent if any */
244    public String getPlainContent()
245    {
246        return plainContent;
247    }
248
249    /** @return Returns the attachmentList. */
250    public List<DataSource> getAttachmentList()
251    {
252        return attachmentList;
253    }
254
255    /** @return Returns the htmlContent if any */
256    public String getHtmlContent()
257    {
258        return htmlContent;
259    }
260
261    /** @return true if a plain content is available */
262    public boolean hasPlainContent()
263    {
264        return this.plainContent != null;
265    }
266
267    /** @return true if HTML content is available */
268    public boolean hasHtmlContent()
269    {
270        return this.htmlContent != null;
271    }
272
273    /** @return true if attachments are available */
274    public boolean hasAttachments()
275    {
276        return this.attachmentList.size() > 0;
277    }
278
279    /**
280     * Find an attachment using its name.
281     *
282     * @param name the name of the attachment
283     * @return the corresponding datasource or null if nothing was found
284     */
285    public DataSource findAttachmentByName(String name)
286    {
287        DataSource dataSource;
288
289        for (int i = 0; i < getAttachmentList().size(); i++)
290        {
291            dataSource = getAttachmentList().get(i);
292            if (name.equalsIgnoreCase(dataSource.getName()))
293            {
294                return dataSource;
295            }
296        }
297
298        return null;
299    }
300
301    /**
302     * Determines the name of the data source if it is not already set.
303     *
304     * @param part the mail part
305     * @param dataSource the data source
306     * @return the name of the data source or {@code null} if no name can be determined
307     * @throws MessagingException accessing the part failed
308     * @throws UnsupportedEncodingException decoding the text failed
309     */
310    protected String getDataSourceName(Part part, DataSource dataSource)
311        throws MessagingException, UnsupportedEncodingException
312    {
313        String result = dataSource.getName();
314
315        if (result == null || result.length() == 0)
316        {
317            result = part.getFileName();
318        }
319
320        if (result != null && result.length() > 0)
321        {
322            result = MimeUtility.decodeText(result);
323        }
324        else
325        {
326            result = null;
327        }
328
329        return result;
330    }
331
332    /**
333     * Read the content of the input stream.
334     *
335     * @param is the input stream to process
336     * @return the content of the input stream
337     * @throws IOException reading the input stream failed
338     */
339    private byte[] getContent(InputStream is)
340        throws IOException
341    {
342        int ch;
343        byte[] result;
344
345        ByteArrayOutputStream os = new ByteArrayOutputStream();
346        BufferedInputStream isReader = new BufferedInputStream(is);
347        BufferedOutputStream osWriter = new BufferedOutputStream(os);
348
349        while ((ch = isReader.read()) != -1)
350        {
351            osWriter.write(ch);
352        }
353
354        osWriter.flush();
355        result = os.toByteArray();
356        osWriter.close();
357
358        return result;
359    }
360
361    /**
362     * Parses the mimeType.
363     *
364     * @param fullMimeType the mime type from the mail api
365     * @return the real mime type
366     */
367    private String getBaseMimeType(String fullMimeType)
368    {
369        int pos = fullMimeType.indexOf(';');
370        if (pos >= 0)
371        {
372            return fullMimeType.substring(0, pos);
373        }
374        else
375        {
376            return fullMimeType;
377        }
378    }
379}