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