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