source: trunk/LogicMail/src/org/logicprobe/LogicMail/util/MailMessageParser.java @ 948

Revision 948, 18.6 KB checked in by octorian, 6 months ago (diff)

Added null check to reduce errors

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1/*-
2 * Copyright (c) 2008, Derek Konigsberg
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 *
9 * 1. Redistributions of source code must retain the above copyright
10 *    notice, this list of conditions and the following disclaimer.
11 * 2. Redistributions in binary form must reproduce the above copyright
12 *    notice, this list of conditions and the following disclaimer in the
13 *    documentation and/or other materials provided with the distribution.
14 * 3. Neither the name of the project nor the names of its
15 *    contributors may be used to endorse or promote products derived
16 *    from this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
21 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
22 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
23 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
24 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
27 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
29 * OF THE POSSIBILITY OF SUCH DAMAGE.
30 */
31package org.logicprobe.LogicMail.util;
32
33import net.rim.device.api.io.IOUtilities;
34import net.rim.device.api.io.LineReader;
35import net.rim.device.api.io.SharedInputStream;
36import net.rim.device.api.mime.MIMEInputStream;
37import net.rim.device.api.mime.MIMEParsingException;
38import net.rim.device.api.system.EventLogger;
39import net.rim.device.api.util.Arrays;
40import net.rim.device.api.util.DataBuffer;
41
42import org.logicprobe.LogicMail.AppInfo;
43import org.logicprobe.LogicMail.message.MimeMessageContent;
44import org.logicprobe.LogicMail.message.MimeMessageContentFactory;
45import org.logicprobe.LogicMail.message.MessageEnvelope;
46import org.logicprobe.LogicMail.message.MimeMessagePart;
47import org.logicprobe.LogicMail.message.MimeMessagePartFactory;
48import org.logicprobe.LogicMail.message.MultiPart;
49import org.logicprobe.LogicMail.message.TextPart;
50import org.logicprobe.LogicMail.message.UnsupportedContentException;
51
52import java.io.ByteArrayInputStream;
53import java.io.IOException;
54import java.io.InputStream;
55
56import java.util.Calendar;
57import java.util.Hashtable;
58import java.util.Vector;
59
60
61/**
62 * This class contains static parser functions used for
63 * parsing raw message source text.
64 */
65public class MailMessageParser {
66    private static String strCRLF = "\r\n";
67    private static final byte[] CRLF = new byte[] { (byte)'\r', (byte)'\n' };
68    private static final byte[] CONTENT_TYPE_KEY = "Content-Type:".getBytes();
69    private static final byte[] BOUNDARY_EQ = "boundary=".getBytes();
70
71    private MailMessageParser() {
72    }
73
74    /**
75     * Parses the message envelope from the message headers.
76     *
77     * @param rawHeaders The raw header text, separated into lines.
78     * @return The message envelope.
79     */
80    public static MessageEnvelope parseMessageEnvelope(String[] rawHeaders) {
81        Hashtable headers = StringParser.parseMailHeaders(rawHeaders);
82        MessageEnvelope env = new MessageEnvelope();
83
84        // Populate the common header field bits of the envelope
85        env.subject = StringParser.parseEncodedHeader((String) headers.get(
86                    "subject"));
87
88        if (env.subject == null) {
89            env.subject = "";
90        }
91
92        env.from = parseAddressList((String) headers.get("from"));
93        env.sender = parseAddressList((String) headers.get("sender"));
94        env.to = parseAddressList((String) headers.get("to"));
95        env.cc = parseAddressList((String) headers.get("cc"));
96        env.bcc = parseAddressList((String) headers.get("bcc"));
97
98        try {
99            env.date = StringParser.parseDateString((String) headers.get("date"));
100        } catch (Exception e) {
101            env.date = Calendar.getInstance().getTime();
102        }
103
104        env.replyTo = parseAddressList((String) headers.get("reply-to"));
105        env.messageId = (String) headers.get("message-id");
106        env.inReplyTo = (String) headers.get("in-reply-to");
107
108        return env;
109    }
110
111    /**
112     * Generates the message headers corresponding to the provided envelope.
113     *
114     * @param envelope The message envelope.
115     * @param includeUserAgent True to include the User-Agent line.
116     * @return The headers, one per line, with CRLF line separators.
117     */
118    public static String generateMessageHeaders(MessageEnvelope envelope,
119        boolean includeUserAgent) {
120        StringBuffer buffer = new StringBuffer();
121
122        // Create the message headers
123        buffer.append(StringParser.createEncodedRecipientHeader("From:", envelope.from));
124        buffer.append(strCRLF);
125
126        buffer.append(StringParser.createEncodedRecipientHeader("To:", envelope.to));
127        buffer.append(strCRLF);
128
129        if ((envelope.cc != null) && (envelope.cc.length > 0)) {
130            buffer.append(StringParser.createEncodedRecipientHeader("Cc:", envelope.cc));
131            buffer.append(strCRLF);
132        }
133
134        if ((envelope.replyTo != null) && (envelope.replyTo.length > 0)) {
135            buffer.append(StringParser.createEncodedRecipientHeader("Reply-To:", envelope.replyTo));
136            buffer.append(strCRLF);
137        }
138
139        buffer.append("Date: ");
140        buffer.append(StringParser.createDateString(envelope.date));
141        buffer.append(strCRLF);
142
143        if (includeUserAgent) {
144            buffer.append("User-Agent: ");
145            buffer.append(AppInfo.getName());
146            buffer.append('/');
147            buffer.append(AppInfo.getVersion());
148            buffer.append(strCRLF);
149        }
150
151        buffer.append(StringParser.createEncodedHeader("Subject:", envelope.subject));
152        buffer.append(strCRLF);
153
154        if (envelope.inReplyTo != null) {
155            buffer.append("In-Reply-To: ");
156            buffer.append(envelope.inReplyTo);
157            buffer.append(strCRLF);
158        }
159
160        return buffer.toString();
161    }
162
163    /**
164     * Separates a list of addresses contained within a message header.
165     * This is slightly more complicated than a string tokenizer, as it
166     * has to deal with quoting and escaping.
167     *
168     * @param text The header line containing the addresses.
169     * @return The separated addresses.
170     */
171    private static String[] parseAddressList(String text) {
172        String[] addresses = StringParser.parseCsvString(text);
173
174        for (int i = 0; i < addresses.length; i++) {
175            addresses[i] = StringParser.parseEncodedHeader(addresses[i]);
176
177            if ((addresses[i].length() > 0) && (addresses[i].charAt(0) == '"')) {
178                int p = addresses[i].indexOf('<');
179
180                while ((p > 0) && (addresses[i].charAt(p) != '"'))
181                    p--;
182
183                if ((p > 0) && ((p + 1) < addresses[i].length())) {
184                    addresses[i] = addresses[i].substring(1, p) +
185                        addresses[i].substring(p + 1);
186                }
187            }
188        }
189
190        return addresses;
191    }
192   
193    /**
194     * Convert string-format raw message source to an InputStream that is
195     * compatible with {@link #parseRawMessage(Hashtable, InputStream)}.
196     *
197     * @param messageSource the message source
198     * @return the input stream to be passed to the parser code
199     */
200    public static InputStream convertMessageResultToStream(String messageSource) {
201        ByteArrayInputStream inputStream = new ByteArrayInputStream(messageSource.getBytes());
202       
203        Vector lines = new Vector();
204        try {
205            LineReader reader = new LineReader(inputStream);
206            byte[] line;
207            while((line = reader.readLine()) != null) {
208                lines.addElement(line);
209            }
210        } catch (IOException e) {
211            EventLogger.logEvent(AppInfo.GUID,
212                    ("Error converting message to stream: " + e.getMessage()).getBytes(),
213                    EventLogger.WARNING);
214        }
215       
216        byte[][] resultLines = new byte[lines.size()][];
217        lines.copyInto(resultLines);
218        return convertMessageResultToStream(resultLines);
219    }
220   
221    /**
222     * Convert the server result to an InputStream wrapping a byte[] buffer
223     * that includes the CRLF markers that were stripped out by the socket
224     * reading code, and has any other necessary pre-processing applied.
225     *
226     * @param resultLines the lines of message data returned by the server
227     * @return the input stream to be passed to the parser code
228     */
229    public static InputStream convertMessageResultToStream(byte[][] resultLines) {
230        DataBuffer buf = new DataBuffer();
231       
232        boolean inHeaders = true;
233        boolean inInitialHeaders = true;
234        boolean firstHeaderLine = true;
235        byte[] boundary = null;
236        for(int i=0; i<resultLines.length; i++) {
237            if(inHeaders) {
238                // Special logic to unfold message headers and replace HTAB
239                // indentations in folded headers with spaces.
240                // This is a workaround for a bug in MIMEInputStream that
241                // causes it to fail to parse certain messages with folded
242                // headers.  (The bug appears to be fixed in OS 6.0, but the
243                // workaround is always invoked because it should not have
244                // any harmful side-effects.)
245                if(resultLines[i].length == 0) {
246                    inHeaders = false;
247                    if(!firstHeaderLine) {
248                        removeTrailingColon(buf);
249                        buf.write(CRLF);
250                    }
251                    buf.write(CRLF);
252                   
253                    if(inInitialHeaders) {
254                        boundary = getContentBoundary(buf.getArray(), buf.getArrayStart(), buf.getLength());
255                        inInitialHeaders = false;
256                    }
257                }
258                else if(resultLines[i][0] == (byte)'\t' || resultLines[i][0] == (byte)' ') {
259                    for(int j=1; j<resultLines[i].length; j++) {
260                        if(resultLines[i][j] != (byte)'\t' && resultLines[i][j] != (byte)' ') {
261                            buf.write((byte)' ');
262                            buf.write(resultLines[i], j, resultLines[i].length - j);
263                            break;
264                        }
265                    }
266                }
267                else {
268                    if(!firstHeaderLine) {
269                        removeTrailingColon(buf);
270                        buf.write(CRLF);
271                    }
272                    buf.write(resultLines[i]);
273                }
274                if(firstHeaderLine) { firstHeaderLine = false; }
275            }
276            else {
277                buf.write(resultLines[i]);
278                buf.write(CRLF);
279                if(Arrays.equals(resultLines[i], boundary)) {
280                    inHeaders = true;
281                    firstHeaderLine = true;
282                }
283            }
284        }
285       
286        ByteArrayInputStream inputStream = new ByteArrayInputStream(
287                buf.getArray(), buf.getArrayStart(), buf.getLength());
288       
289        return inputStream;
290    }
291   
292    private static void removeTrailingColon(DataBuffer buf) {
293        int len = buf.getLength();
294        if(len > 0 && buf.getArray()[buf.getArrayStart() + len - 1] == (byte)';') {
295            buf.setLength(len - 1);
296        }
297    }
298
299    private static byte[] getContentBoundary(byte[] buf, int offset, int length) {
300        int p = StringArrays.indexOf(buf, CONTENT_TYPE_KEY, offset, length, true);
301        if(p == -1) { return null; } else { p += CONTENT_TYPE_KEY.length; }
302       
303        int q = StringArrays.indexOf(buf, CRLF, p, length, false);
304        if(q == -1) { return null; }
305       
306        int r = StringArrays.indexOf(buf, BOUNDARY_EQ, p, q, true);
307        if(r == -1) { return null; } else { r += BOUNDARY_EQ.length; }
308       
309        int s = StringArrays.indexOf(buf, (byte)' ', r);
310        if(s == -1 || s > q) { s = q; }
311       
312        if(buf[r] == (byte)'\"') { r++; }
313        if(buf[s - 1] == (byte)';') { s--; }
314        if((s - r) <= 0) { return null; }
315        if(buf[s - 1] == (byte)'\"') { s--; }
316        if((s - r) <= 0) { return null; }
317       
318        byte[] result = new byte[(s - r) + 2];
319        result[0] = (byte)'-';
320        result[1] = (byte)'-';
321        System.arraycopy(buf, r, result, 2, s - r);
322       
323        return result;
324    }
325
326    /**
327     * Parses the raw message body.
328     * There will be a single entry in the content map that does not match the
329     * type information described below.  This entry will have a key of
330     * <code>Boolean.TRUE</code>, and an <code>Integer</code> value representing
331     * the result of {@link MIMEInputStream#isPartComplete()} for the message.
332     *
333     * @param contentMap Map to populate with MessagePart-to-MessageContent data.
334     * @param inputStream The stream to read the raw message from
335     * @return The root message part.
336     * @throws IOException Signals that an I/O exception has occurred.
337     */
338    public static MimeMessagePart parseRawMessage(Hashtable contentMap, InputStream inputStream)
339        throws IOException {
340        MIMEInputStream mimeInputStream = null;
341
342        try {
343            mimeInputStream = new MIMEInputStream(inputStream);
344        } catch (MIMEParsingException e) {
345            EventLogger.logEvent(AppInfo.GUID,
346                    ("Unable to parse MIME encoded message: " + e.getMessage()).getBytes(),
347                    EventLogger.WARNING);
348            return null;
349        } catch (ArrayIndexOutOfBoundsException e) {
350            EventLogger.logEvent(AppInfo.GUID,
351                    ("Unable to parse MIME encoded message: " + e.getMessage()).getBytes(),
352                    EventLogger.WARNING);
353            return null;
354        }
355
356        contentMap.put(Boolean.TRUE, new Integer(mimeInputStream.isPartComplete()));
357       
358        MimeMessagePart rootPart = getMessagePart(contentMap, mimeInputStream);
359
360        return rootPart;
361    }
362
363    /**
364     * Recursively walk the provided MIMEInputStream, building a message
365     * tree in the process.
366     *
367     * @param contentMap Map to populate with MessagePart-to-MessageContent data.
368     * @param mimeInputStream MIMEInputStream of the downloaded message data
369     * @return Root MessagePart element for this portion of the message tree
370     */
371    private static MimeMessagePart getMessagePart(Hashtable contentMap, MIMEInputStream mimeInputStream)
372        throws IOException {
373        // Parse out the MIME type and relevant header fields
374        String mimeType = mimeInputStream.getContentType();
375        String type = mimeType.substring(0, mimeType.indexOf('/'));
376        String subtype = mimeType.substring(mimeType.indexOf('/') + 1);
377        String encoding = mimeInputStream.getHeader("Content-Transfer-Encoding");
378        String charset = mimeInputStream.getContentTypeParameter("charset");
379        String name = StringParser.parseEncodedHeader(mimeInputStream.getContentTypeParameter("name"), false);
380        String disposition = mimeInputStream.getHeader("Content-Disposition");
381        String contentId = mimeInputStream.getHeader("Content-ID");
382
383        // Default parameters used when headers are missing
384        if (encoding == null) {
385            encoding = "7bit";
386        }
387
388        // Clean up the disposition field
389        if(disposition != null) {
390                int p = disposition.indexOf(';');
391                if(p != -1) {
392                        disposition = disposition.substring(0, p);
393                }
394                disposition = disposition.toLowerCase();
395        }
396       
397        // Handle the multi-part case
398        if (mimeInputStream.isMultiPart() &&
399                type.equalsIgnoreCase("multipart")) {
400            MimeMessagePart part = MimeMessagePartFactory.createMimeMessagePart(
401                        type, subtype, null, null, null, null, null, -1);
402            MIMEInputStream[] mimeSubparts = mimeInputStream.getParts();
403
404            for (int i = 0; i < mimeSubparts.length; i++) {
405                MimeMessagePart subPart = getMessagePart(contentMap, mimeSubparts[i]);
406
407                if (subPart != null) {
408                    ((MultiPart) part).addPart(subPart);
409                }
410            }
411
412            return part;
413        }
414        // Handle the single-part case
415        else {
416            // Decode the data if the part is complete or indeterminate (as is
417            // common if this is the message's only part), or is a text part
418            // where partial decoding still yields usable output.
419            byte[] buffer;
420            if(mimeInputStream.isPartComplete() != 0
421                    || type.equalsIgnoreCase(TextPart.TYPE)) {
422                buffer = readRawData(mimeInputStream);
423            }
424            else {
425                buffer = null;
426            }
427           
428            MimeMessagePart part = MimeMessagePartFactory.createMimeMessagePart(
429                    type, subtype, name, encoding, charset, disposition, contentId,
430                    (buffer != null) ? buffer.length : 0);
431
432            if(buffer != null && buffer.length > 0) {
433                try {
434                    MimeMessageContent content = MimeMessageContentFactory.createContentEncoded(part, buffer);
435                    content.setPartComplete(mimeInputStream.isPartComplete());
436                    contentMap.put(part, content);
437                } catch (UnsupportedContentException e) {
438                    EventLogger.logEvent(AppInfo.GUID,
439                            ("UnsupportedContentException: " + e.getMessage()).getBytes(),
440                            EventLogger.WARNING);
441                }
442            }
443            return part;
444        }
445    }
446   
447    private static byte[] readRawData(MIMEInputStream mimeInputStream) throws IOException {
448        byte[] buffer;
449        SharedInputStream sis = mimeInputStream.getRawMIMEInputStream();
450        if(sis == null) { return new byte[0]; }
451        buffer = IOUtilities.streamToBytes(sis);
452
453        int offset = 0;
454        while (((offset + 3) < buffer.length) &&
455                !((buffer[offset] == '\r') &&
456                        (buffer[offset + 1] == '\n') &&
457                        (buffer[offset + 2] == '\r') &&
458                        (buffer[offset + 3] == '\n'))) {
459            offset++;
460        }
461        offset += 4;
462
463        try {
464            return Arrays.copy(buffer, offset, buffer.length - offset);
465        } catch (IndexOutOfBoundsException e) {
466            return new byte[0];
467        }
468    }   
469}
Note: See TracBrowser for help on using the repository browser.