MimeMessage.java revision 3b965d78774a42358ce6bbdcc43b4c8df130a60e
1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.emailcommon.internet;
18
19import com.android.emailcommon.mail.Address;
20import com.android.emailcommon.mail.Body;
21import com.android.emailcommon.mail.BodyPart;
22import com.android.emailcommon.mail.Message;
23import com.android.emailcommon.mail.MessagingException;
24import com.android.emailcommon.mail.Multipart;
25import com.android.emailcommon.mail.Part;
26
27import org.apache.james.mime4j.BodyDescriptor;
28import org.apache.james.mime4j.ContentHandler;
29import org.apache.james.mime4j.EOLConvertingInputStream;
30import org.apache.james.mime4j.MimeStreamParser;
31import org.apache.james.mime4j.field.DateTimeField;
32import org.apache.james.mime4j.field.Field;
33
34import android.text.TextUtils;
35
36import java.io.BufferedWriter;
37import java.io.IOException;
38import java.io.InputStream;
39import java.io.OutputStream;
40import java.io.OutputStreamWriter;
41import java.text.SimpleDateFormat;
42import java.util.Date;
43import java.util.Locale;
44import java.util.Stack;
45import java.util.regex.Pattern;
46
47/**
48 * An implementation of Message that stores all of its metadata in RFC 822 and
49 * RFC 2045 style headers.
50 *
51 * NOTE:  Automatic generation of a local message-id is becoming unwieldy and should be removed.
52 * It would be better to simply do it explicitly on local creation of new outgoing messages.
53 */
54public class MimeMessage extends Message {
55    private MimeHeader mHeader;
56    private MimeHeader mExtendedHeader;
57
58    // NOTE:  The fields here are transcribed out of headers, and values stored here will supercede
59    // the values found in the headers.  Use caution to prevent any out-of-phase errors.  In
60    // particular, any adds/changes/deletes here must be echoed by changes in the parse() function.
61    private Address[] mFrom;
62    private Address[] mTo;
63    private Address[] mCc;
64    private Address[] mBcc;
65    private Address[] mReplyTo;
66    private Date mSentDate;
67    private Body mBody;
68    protected int mSize;
69    private boolean mInhibitLocalMessageId = false;
70    private boolean mComplete = true;
71
72    // Shared random source for generating local message-id values
73    private static final java.util.Random sRandom = new java.util.Random();
74
75    // In MIME, en_US-like date format should be used. In other words "MMM" should be encoded to
76    // "Jan", not the other localized format like "Ene" (meaning January in locale es).
77    // This conversion is used when generating outgoing MIME messages. Incoming MIME date
78    // headers are parsed by org.apache.james.mime4j.field.DateTimeField which does not have any
79    // localization code.
80    private static final SimpleDateFormat DATE_FORMAT =
81        new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
82
83    // regex that matches content id surrounded by "<>" optionally.
84    private static final Pattern REMOVE_OPTIONAL_BRACKETS = Pattern.compile("^<?([^>]+)>?$");
85    // regex that matches end of line.
86    private static final Pattern END_OF_LINE = Pattern.compile("\r?\n");
87
88    public MimeMessage() {
89        mHeader = null;
90    }
91
92    /**
93     * Generate a local message id.  This is only used when none has been assigned, and is
94     * installed lazily.  Any remote (typically server-assigned) message id takes precedence.
95     * @return a long, locally-generated message-ID value
96     */
97    private static String generateMessageId() {
98        StringBuffer sb = new StringBuffer();
99        sb.append("<");
100        for (int i = 0; i < 24; i++) {
101            // We'll use a 5-bit range (0..31)
102            int value = sRandom.nextInt() & 31;
103            char c = "0123456789abcdefghijklmnopqrstuv".charAt(value);
104            sb.append(c);
105        }
106        sb.append(".");
107        sb.append(Long.toString(System.currentTimeMillis()));
108        sb.append("@email.android.com>");
109        return sb.toString();
110    }
111
112    /**
113     * Parse the given InputStream using Apache Mime4J to build a MimeMessage.
114     *
115     * @param in
116     * @throws IOException
117     * @throws MessagingException
118     */
119    public MimeMessage(InputStream in) throws IOException, MessagingException {
120        parse(in);
121    }
122
123    private MimeStreamParser init() {
124        // Before parsing the input stream, clear all local fields that may be superceded by
125        // the new incoming message.
126        getMimeHeaders().clear();
127        mInhibitLocalMessageId = true;
128        mFrom = null;
129        mTo = null;
130        mCc = null;
131        mBcc = null;
132        mReplyTo = null;
133        mSentDate = null;
134        mBody = null;
135
136        MimeStreamParser parser = new MimeStreamParser();
137        parser.setContentHandler(new MimeMessageBuilder());
138        return parser;
139    }
140
141    protected void parse(InputStream in) throws IOException, MessagingException {
142        MimeStreamParser parser = init();
143        parser.parse(new EOLConvertingInputStream(in));
144        mComplete = !parser.getPrematureEof();
145    }
146
147    public void parse(InputStream in, EOLConvertingInputStream.Callback callback)
148            throws IOException, MessagingException {
149        MimeStreamParser parser = init();
150        parser.parse(new EOLConvertingInputStream(in, getSize(), callback));
151        mComplete = !parser.getPrematureEof();
152    }
153
154    /**
155     * Return the internal mHeader value, with very lazy initialization.
156     * The goal is to save memory by not creating the headers until needed.
157     */
158    private MimeHeader getMimeHeaders() {
159        if (mHeader == null) {
160            mHeader = new MimeHeader();
161        }
162        return mHeader;
163    }
164
165    @Override
166    public Date getReceivedDate() throws MessagingException {
167        return null;
168    }
169
170    @Override
171    public Date getSentDate() throws MessagingException {
172        if (mSentDate == null) {
173            try {
174                DateTimeField field = (DateTimeField)Field.parse("Date: "
175                        + MimeUtility.unfoldAndDecode(getFirstHeader("Date")));
176                mSentDate = field.getDate();
177            } catch (Exception e) {
178
179            }
180        }
181        return mSentDate;
182    }
183
184    @Override
185    public void setSentDate(Date sentDate) throws MessagingException {
186        setHeader("Date", DATE_FORMAT.format(sentDate));
187        this.mSentDate = sentDate;
188    }
189
190    @Override
191    public String getContentType() throws MessagingException {
192        String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
193        if (contentType == null) {
194            return "text/plain";
195        } else {
196            return contentType;
197        }
198    }
199
200    public String getDisposition() throws MessagingException {
201        String contentDisposition = getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
202        if (contentDisposition == null) {
203            return null;
204        } else {
205            return contentDisposition;
206        }
207    }
208
209    public String getContentId() throws MessagingException {
210        String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID);
211        if (contentId == null) {
212            return null;
213        } else {
214            // remove optionally surrounding brackets.
215            return REMOVE_OPTIONAL_BRACKETS.matcher(contentId).replaceAll("$1");
216        }
217    }
218
219    public boolean isComplete() {
220        return mComplete;
221    }
222
223    public String getMimeType() throws MessagingException {
224        return MimeUtility.getHeaderParameter(getContentType(), null);
225    }
226
227    public int getSize() throws MessagingException {
228        return mSize;
229    }
230
231    /**
232     * Returns a list of the given recipient type from this message. If no addresses are
233     * found the method returns an empty array.
234     */
235    @Override
236    public Address[] getRecipients(RecipientType type) throws MessagingException {
237        if (type == RecipientType.TO) {
238            if (mTo == null) {
239                mTo = Address.parse(MimeUtility.unfold(getFirstHeader("To")));
240            }
241            return mTo;
242        } else if (type == RecipientType.CC) {
243            if (mCc == null) {
244                mCc = Address.parse(MimeUtility.unfold(getFirstHeader("CC")));
245            }
246            return mCc;
247        } else if (type == RecipientType.BCC) {
248            if (mBcc == null) {
249                mBcc = Address.parse(MimeUtility.unfold(getFirstHeader("BCC")));
250            }
251            return mBcc;
252        } else {
253            throw new MessagingException("Unrecognized recipient type.");
254        }
255    }
256
257    @Override
258    public void setRecipients(RecipientType type, Address[] addresses) throws MessagingException {
259        final int TO_LENGTH = 4;  // "To: "
260        final int CC_LENGTH = 4;  // "Cc: "
261        final int BCC_LENGTH = 5; // "Bcc: "
262        if (type == RecipientType.TO) {
263            if (addresses == null || addresses.length == 0) {
264                removeHeader("To");
265                this.mTo = null;
266            } else {
267                setHeader("To", MimeUtility.fold(Address.toHeader(addresses), TO_LENGTH));
268                this.mTo = addresses;
269            }
270        } else if (type == RecipientType.CC) {
271            if (addresses == null || addresses.length == 0) {
272                removeHeader("CC");
273                this.mCc = null;
274            } else {
275                setHeader("CC", MimeUtility.fold(Address.toHeader(addresses), CC_LENGTH));
276                this.mCc = addresses;
277            }
278        } else if (type == RecipientType.BCC) {
279            if (addresses == null || addresses.length == 0) {
280                removeHeader("BCC");
281                this.mBcc = null;
282            } else {
283                setHeader("BCC", MimeUtility.fold(Address.toHeader(addresses), BCC_LENGTH));
284                this.mBcc = addresses;
285            }
286        } else {
287            throw new MessagingException("Unrecognized recipient type.");
288        }
289    }
290
291    /**
292     * Returns the unfolded, decoded value of the Subject header.
293     */
294    @Override
295    public String getSubject() throws MessagingException {
296        return MimeUtility.unfoldAndDecode(getFirstHeader("Subject"));
297    }
298
299    @Override
300    public void setSubject(String subject) throws MessagingException {
301        final int HEADER_NAME_LENGTH = 9;     // "Subject: "
302        setHeader("Subject", MimeUtility.foldAndEncode2(subject, HEADER_NAME_LENGTH));
303    }
304
305    @Override
306    public Address[] getFrom() throws MessagingException {
307        if (mFrom == null) {
308            String list = MimeUtility.unfold(getFirstHeader("From"));
309            if (list == null || list.length() == 0) {
310                list = MimeUtility.unfold(getFirstHeader("Sender"));
311            }
312            mFrom = Address.parse(list);
313        }
314        return mFrom;
315    }
316
317    @Override
318    public void setFrom(Address from) throws MessagingException {
319        final int FROM_LENGTH = 6;  // "From: "
320        if (from != null) {
321            setHeader("From", MimeUtility.fold(from.toHeader(), FROM_LENGTH));
322            this.mFrom = new Address[] {
323                    from
324                };
325        } else {
326            this.mFrom = null;
327        }
328    }
329
330    @Override
331    public Address[] getReplyTo() throws MessagingException {
332        if (mReplyTo == null) {
333            mReplyTo = Address.parse(MimeUtility.unfold(getFirstHeader("Reply-to")));
334        }
335        return mReplyTo;
336    }
337
338    @Override
339    public void setReplyTo(Address[] replyTo) throws MessagingException {
340        final int REPLY_TO_LENGTH = 10;  // "Reply-to: "
341        if (replyTo == null || replyTo.length == 0) {
342            removeHeader("Reply-to");
343            mReplyTo = null;
344        } else {
345            setHeader("Reply-to", MimeUtility.fold(Address.toHeader(replyTo), REPLY_TO_LENGTH));
346            mReplyTo = replyTo;
347        }
348    }
349
350    /**
351     * Set the mime "Message-ID" header
352     * @param messageId the new Message-ID value
353     * @throws MessagingException
354     */
355    @Override
356    public void setMessageId(String messageId) throws MessagingException {
357        setHeader("Message-ID", messageId);
358    }
359
360    /**
361     * Get the mime "Message-ID" header.  This value will be preloaded with a locally-generated
362     * random ID, if the value has not previously been set.  Local generation can be inhibited/
363     * overridden by explicitly clearing the headers, removing the message-id header, etc.
364     * @return the Message-ID header string, or null if explicitly has been set to null
365     */
366    @Override
367    public String getMessageId() throws MessagingException {
368        String messageId = getFirstHeader("Message-ID");
369        if (messageId == null && !mInhibitLocalMessageId) {
370            messageId = generateMessageId();
371            setMessageId(messageId);
372        }
373        return messageId;
374    }
375
376    @Override
377    public void saveChanges() throws MessagingException {
378        throw new MessagingException("saveChanges not yet implemented");
379    }
380
381    @Override
382    public Body getBody() throws MessagingException {
383        return mBody;
384    }
385
386    @Override
387    public void setBody(Body body) throws MessagingException {
388        this.mBody = body;
389        if (body instanceof Multipart) {
390            Multipart multipart = ((Multipart)body);
391            multipart.setParent(this);
392            setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType());
393            setHeader("MIME-Version", "1.0");
394        }
395        else if (body instanceof TextBody) {
396            setHeader(MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\n charset=utf-8",
397                    getMimeType()));
398            setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
399        }
400    }
401
402    protected String getFirstHeader(String name) throws MessagingException {
403        return getMimeHeaders().getFirstHeader(name);
404    }
405
406    @Override
407    public void addHeader(String name, String value) throws MessagingException {
408        getMimeHeaders().addHeader(name, value);
409    }
410
411    @Override
412    public void setHeader(String name, String value) throws MessagingException {
413        getMimeHeaders().setHeader(name, value);
414    }
415
416    @Override
417    public String[] getHeader(String name) throws MessagingException {
418        return getMimeHeaders().getHeader(name);
419    }
420
421    @Override
422    public void removeHeader(String name) throws MessagingException {
423        getMimeHeaders().removeHeader(name);
424        if ("Message-ID".equalsIgnoreCase(name)) {
425            mInhibitLocalMessageId = true;
426        }
427    }
428
429    /**
430     * Set extended header
431     *
432     * @param name Extended header name
433     * @param value header value - flattened by removing CR-NL if any
434     * remove header if value is null
435     * @throws MessagingException
436     */
437    public void setExtendedHeader(String name, String value) throws MessagingException {
438        if (value == null) {
439            if (mExtendedHeader != null) {
440                mExtendedHeader.removeHeader(name);
441            }
442            return;
443        }
444        if (mExtendedHeader == null) {
445            mExtendedHeader = new MimeHeader();
446        }
447        mExtendedHeader.setHeader(name, END_OF_LINE.matcher(value).replaceAll(""));
448    }
449
450    /**
451     * Get extended header
452     *
453     * @param name Extended header name
454     * @return header value - null if header does not exist
455     * @throws MessagingException
456     */
457    public String getExtendedHeader(String name) throws MessagingException {
458        if (mExtendedHeader == null) {
459            return null;
460        }
461        return mExtendedHeader.getFirstHeader(name);
462    }
463
464    /**
465     * Set entire extended headers from String
466     *
467     * @param headers Extended header and its value - "CR-NL-separated pairs
468     * if null or empty, remove entire extended headers
469     * @throws MessagingException
470     */
471    public void setExtendedHeaders(String headers) throws MessagingException {
472        if (TextUtils.isEmpty(headers)) {
473            mExtendedHeader = null;
474        } else {
475            mExtendedHeader = new MimeHeader();
476            for (String header : END_OF_LINE.split(headers)) {
477                String[] tokens = header.split(":", 2);
478                if (tokens.length != 2) {
479                    throw new MessagingException("Illegal extended headers: " + headers);
480                }
481                mExtendedHeader.setHeader(tokens[0].trim(), tokens[1].trim());
482            }
483        }
484    }
485
486    /**
487     * Get entire extended headers as String
488     *
489     * @return "CR-NL-separated extended headers - null if extended header does not exist
490     */
491    public String getExtendedHeaders() {
492        if (mExtendedHeader != null) {
493            return mExtendedHeader.writeToString();
494        }
495        return null;
496    }
497
498    /**
499     * Write message header and body to output stream
500     *
501     * @param out Output steam to write message header and body.
502     */
503    public void writeTo(OutputStream out) throws IOException, MessagingException {
504        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
505        // Force creation of local message-id
506        getMessageId();
507        getMimeHeaders().writeTo(out);
508        // mExtendedHeader will not be write out to external output stream,
509        // because it is intended to internal use.
510        writer.write("\r\n");
511        writer.flush();
512        if (mBody != null) {
513            mBody.writeTo(out);
514        }
515    }
516
517    public InputStream getInputStream() throws MessagingException {
518        return null;
519    }
520
521    class MimeMessageBuilder implements ContentHandler {
522        private Stack<Object> stack = new Stack<Object>();
523
524        public MimeMessageBuilder() {
525        }
526
527        private void expect(Class<?> c) {
528            if (!c.isInstance(stack.peek())) {
529                throw new IllegalStateException("Internal stack error: " + "Expected '"
530                        + c.getName() + "' found '" + stack.peek().getClass().getName() + "'");
531            }
532        }
533
534        public void startMessage() {
535            if (stack.isEmpty()) {
536                stack.push(MimeMessage.this);
537            } else {
538                expect(Part.class);
539                try {
540                    MimeMessage m = new MimeMessage();
541                    ((Part)stack.peek()).setBody(m);
542                    stack.push(m);
543                } catch (MessagingException me) {
544                    throw new Error(me);
545                }
546            }
547        }
548
549        public void endMessage() {
550            expect(MimeMessage.class);
551            stack.pop();
552        }
553
554        public void startHeader() {
555            expect(Part.class);
556        }
557
558        public void field(String fieldData) {
559            expect(Part.class);
560            try {
561                String[] tokens = fieldData.split(":", 2);
562                ((Part)stack.peek()).addHeader(tokens[0], tokens[1].trim());
563            } catch (MessagingException me) {
564                throw new Error(me);
565            }
566        }
567
568        public void endHeader() {
569            expect(Part.class);
570        }
571
572        public void startMultipart(BodyDescriptor bd) {
573            expect(Part.class);
574
575            Part e = (Part)stack.peek();
576            try {
577                MimeMultipart multiPart = new MimeMultipart(e.getContentType());
578                e.setBody(multiPart);
579                stack.push(multiPart);
580            } catch (MessagingException me) {
581                throw new Error(me);
582            }
583        }
584
585        public void body(BodyDescriptor bd, InputStream in) throws IOException {
586            expect(Part.class);
587            Body body = MimeUtility.decodeBody(in, bd.getTransferEncoding());
588            try {
589                ((Part)stack.peek()).setBody(body);
590            } catch (MessagingException me) {
591                throw new Error(me);
592            }
593        }
594
595        public void endMultipart() {
596            stack.pop();
597        }
598
599        public void startBodyPart() {
600            expect(MimeMultipart.class);
601
602            try {
603                MimeBodyPart bodyPart = new MimeBodyPart();
604                ((MimeMultipart)stack.peek()).addBodyPart(bodyPart);
605                stack.push(bodyPart);
606            } catch (MessagingException me) {
607                throw new Error(me);
608            }
609        }
610
611        public void endBodyPart() {
612            expect(BodyPart.class);
613            stack.pop();
614        }
615
616        public void epilogue(InputStream is) throws IOException {
617            expect(MimeMultipart.class);
618            StringBuffer sb = new StringBuffer();
619            int b;
620            while ((b = is.read()) != -1) {
621                sb.append((char)b);
622            }
623            // ((Multipart) stack.peek()).setEpilogue(sb.toString());
624        }
625
626        public void preamble(InputStream is) throws IOException {
627            expect(MimeMultipart.class);
628            StringBuffer sb = new StringBuffer();
629            int b;
630            while ((b = is.read()) != -1) {
631                sb.append((char)b);
632            }
633            try {
634                ((MimeMultipart)stack.peek()).setPreamble(sb.toString());
635            } catch (MessagingException me) {
636                throw new Error(me);
637            }
638        }
639
640        public void raw(InputStream is) throws IOException {
641            throw new UnsupportedOperationException("Not supported");
642        }
643    }
644}
645