1/*
2 * Copyright (C) 2015 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 */
16package com.android.phone.common.mail.internet;
17
18import com.android.phone.common.mail.Address;
19import com.android.phone.common.mail.Body;
20import com.android.phone.common.mail.BodyPart;
21import com.android.phone.common.mail.Message;
22import com.android.phone.common.mail.MessagingException;
23import com.android.phone.common.mail.Multipart;
24import com.android.phone.common.mail.Part;
25import com.android.phone.common.mail.utils.LogUtils;
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 supersede
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        final StringBuilder sb = new StringBuilder();
99        sb.append("<");
100        for (int i = 0; i < 24; i++) {
101            // We'll use a 5-bit range (0..31)
102            final int value = sRandom.nextInt() & 31;
103            final 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 InputStream providing message content
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        final MimeStreamParser parser = new MimeStreamParser();
137        parser.setContentHandler(new MimeMessageBuilder());
138        return parser;
139    }
140
141    protected void parse(InputStream in) throws IOException, MessagingException {
142        final 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        final 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                // TODO: We should make it more clear what exceptions can be thrown here,
178                // and whether they reflect a normal or error condition.
179            } catch (Exception e) {
180                LogUtils.v(LogUtils.TAG, "Message missing Date header");
181            }
182        }
183        if (mSentDate == null) {
184            // If we still don't have a date, fall back to "Delivery-date"
185            try {
186                DateTimeField field = (DateTimeField)Field.parse("Date: "
187                        + MimeUtility.unfoldAndDecode(getFirstHeader("Delivery-date")));
188                mSentDate = field.getDate();
189                // TODO: We should make it more clear what exceptions can be thrown here,
190                // and whether they reflect a normal or error condition.
191            } catch (Exception e) {
192                LogUtils.v(LogUtils.TAG, "Message also missing Delivery-Date header");
193            }
194        }
195        return mSentDate;
196    }
197
198    @Override
199    public void setSentDate(Date sentDate) throws MessagingException {
200        setHeader("Date", DATE_FORMAT.format(sentDate));
201        this.mSentDate = sentDate;
202    }
203
204    @Override
205    public String getContentType() throws MessagingException {
206        final String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
207        if (contentType == null) {
208            return "text/plain";
209        } else {
210            return contentType;
211        }
212    }
213
214    @Override
215    public String getDisposition() throws MessagingException {
216        return getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
217    }
218
219    @Override
220    public String getContentId() throws MessagingException {
221        final String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID);
222        if (contentId == null) {
223            return null;
224        } else {
225            // remove optionally surrounding brackets.
226            return REMOVE_OPTIONAL_BRACKETS.matcher(contentId).replaceAll("$1");
227        }
228    }
229
230    public boolean isComplete() {
231        return mComplete;
232    }
233
234    @Override
235    public String getMimeType() throws MessagingException {
236        return MimeUtility.getHeaderParameter(getContentType(), null);
237    }
238
239    @Override
240    public int getSize() throws MessagingException {
241        return mSize;
242    }
243
244    /**
245     * Returns a list of the given recipient type from this message. If no addresses are
246     * found the method returns an empty array.
247     */
248    @Override
249    public Address[] getRecipients(String type) throws MessagingException {
250        if (type == RECIPIENT_TYPE_TO) {
251            if (mTo == null) {
252                mTo = Address.parse(MimeUtility.unfold(getFirstHeader("To")));
253            }
254            return mTo;
255        } else if (type == RECIPIENT_TYPE_CC) {
256            if (mCc == null) {
257                mCc = Address.parse(MimeUtility.unfold(getFirstHeader("CC")));
258            }
259            return mCc;
260        } else if (type == RECIPIENT_TYPE_BCC) {
261            if (mBcc == null) {
262                mBcc = Address.parse(MimeUtility.unfold(getFirstHeader("BCC")));
263            }
264            return mBcc;
265        } else {
266            throw new MessagingException("Unrecognized recipient type.");
267        }
268    }
269
270    @Override
271    public void setRecipients(String type, Address[] addresses) throws MessagingException {
272        final int TO_LENGTH = 4;  // "To: "
273        final int CC_LENGTH = 4;  // "Cc: "
274        final int BCC_LENGTH = 5; // "Bcc: "
275        if (type == RECIPIENT_TYPE_TO) {
276            if (addresses == null || addresses.length == 0) {
277                removeHeader("To");
278                this.mTo = null;
279            } else {
280                setHeader("To", MimeUtility.fold(Address.toHeader(addresses), TO_LENGTH));
281                this.mTo = addresses;
282            }
283        } else if (type == RECIPIENT_TYPE_CC) {
284            if (addresses == null || addresses.length == 0) {
285                removeHeader("CC");
286                this.mCc = null;
287            } else {
288                setHeader("CC", MimeUtility.fold(Address.toHeader(addresses), CC_LENGTH));
289                this.mCc = addresses;
290            }
291        } else if (type == RECIPIENT_TYPE_BCC) {
292            if (addresses == null || addresses.length == 0) {
293                removeHeader("BCC");
294                this.mBcc = null;
295            } else {
296                setHeader("BCC", MimeUtility.fold(Address.toHeader(addresses), BCC_LENGTH));
297                this.mBcc = addresses;
298            }
299        } else {
300            throw new MessagingException("Unrecognized recipient type.");
301        }
302    }
303
304    /**
305     * Returns the unfolded, decoded value of the Subject header.
306     */
307    @Override
308    public String getSubject() throws MessagingException {
309        return MimeUtility.unfoldAndDecode(getFirstHeader("Subject"));
310    }
311
312    @Override
313    public void setSubject(String subject) throws MessagingException {
314        final int HEADER_NAME_LENGTH = 9;     // "Subject: "
315        setHeader("Subject", MimeUtility.foldAndEncode2(subject, HEADER_NAME_LENGTH));
316    }
317
318    @Override
319    public Address[] getFrom() throws MessagingException {
320        if (mFrom == null) {
321            String list = MimeUtility.unfold(getFirstHeader("From"));
322            if (list == null || list.length() == 0) {
323                list = MimeUtility.unfold(getFirstHeader("Sender"));
324            }
325            mFrom = Address.parse(list);
326        }
327        return mFrom;
328    }
329
330    @Override
331    public void setFrom(Address from) throws MessagingException {
332        final int FROM_LENGTH = 6;  // "From: "
333        if (from != null) {
334            setHeader("From", MimeUtility.fold(from.toHeader(), FROM_LENGTH));
335            this.mFrom = new Address[] {
336                    from
337                };
338        } else {
339            this.mFrom = null;
340        }
341    }
342
343    @Override
344    public Address[] getReplyTo() throws MessagingException {
345        if (mReplyTo == null) {
346            mReplyTo = Address.parse(MimeUtility.unfold(getFirstHeader("Reply-to")));
347        }
348        return mReplyTo;
349    }
350
351    @Override
352    public void setReplyTo(Address[] replyTo) throws MessagingException {
353        final int REPLY_TO_LENGTH = 10;  // "Reply-to: "
354        if (replyTo == null || replyTo.length == 0) {
355            removeHeader("Reply-to");
356            mReplyTo = null;
357        } else {
358            setHeader("Reply-to", MimeUtility.fold(Address.toHeader(replyTo), REPLY_TO_LENGTH));
359            mReplyTo = replyTo;
360        }
361    }
362
363    /**
364     * Set the mime "Message-ID" header
365     * @param messageId the new Message-ID value
366     * @throws MessagingException
367     */
368    @Override
369    public void setMessageId(String messageId) throws MessagingException {
370        setHeader("Message-ID", messageId);
371    }
372
373    /**
374     * Get the mime "Message-ID" header.  This value will be preloaded with a locally-generated
375     * random ID, if the value has not previously been set.  Local generation can be inhibited/
376     * overridden by explicitly clearing the headers, removing the message-id header, etc.
377     * @return the Message-ID header string, or null if explicitly has been set to null
378     */
379    @Override
380    public String getMessageId() throws MessagingException {
381        String messageId = getFirstHeader("Message-ID");
382        if (messageId == null && !mInhibitLocalMessageId) {
383            messageId = generateMessageId();
384            setMessageId(messageId);
385        }
386        return messageId;
387    }
388
389    @Override
390    public void saveChanges() throws MessagingException {
391        throw new MessagingException("saveChanges not yet implemented");
392    }
393
394    @Override
395    public Body getBody() throws MessagingException {
396        return mBody;
397    }
398
399    @Override
400    public void setBody(Body body) throws MessagingException {
401        this.mBody = body;
402        if (body instanceof Multipart) {
403            final Multipart multipart = ((Multipart)body);
404            multipart.setParent(this);
405            setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType());
406            setHeader("MIME-Version", "1.0");
407        }
408        else if (body instanceof TextBody) {
409            setHeader(MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\n charset=utf-8",
410                    getMimeType()));
411            setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
412        }
413    }
414
415    protected String getFirstHeader(String name) throws MessagingException {
416        return getMimeHeaders().getFirstHeader(name);
417    }
418
419    @Override
420    public void addHeader(String name, String value) throws MessagingException {
421        getMimeHeaders().addHeader(name, value);
422    }
423
424    @Override
425    public void setHeader(String name, String value) throws MessagingException {
426        getMimeHeaders().setHeader(name, value);
427    }
428
429    @Override
430    public String[] getHeader(String name) throws MessagingException {
431        return getMimeHeaders().getHeader(name);
432    }
433
434    @Override
435    public void removeHeader(String name) throws MessagingException {
436        getMimeHeaders().removeHeader(name);
437        if ("Message-ID".equalsIgnoreCase(name)) {
438            mInhibitLocalMessageId = true;
439        }
440    }
441
442    /**
443     * Set extended header
444     *
445     * @param name Extended header name
446     * @param value header value - flattened by removing CR-NL if any
447     * remove header if value is null
448     * @throws MessagingException
449     */
450    @Override
451    public void setExtendedHeader(String name, String value) throws MessagingException {
452        if (value == null) {
453            if (mExtendedHeader != null) {
454                mExtendedHeader.removeHeader(name);
455            }
456            return;
457        }
458        if (mExtendedHeader == null) {
459            mExtendedHeader = new MimeHeader();
460        }
461        mExtendedHeader.setHeader(name, END_OF_LINE.matcher(value).replaceAll(""));
462    }
463
464    /**
465     * Get extended header
466     *
467     * @param name Extended header name
468     * @return header value - null if header does not exist
469     * @throws MessagingException
470     */
471    @Override
472    public String getExtendedHeader(String name) throws MessagingException {
473        if (mExtendedHeader == null) {
474            return null;
475        }
476        return mExtendedHeader.getFirstHeader(name);
477    }
478
479    /**
480     * Set entire extended headers from String
481     *
482     * @param headers Extended header and its value - "CR-NL-separated pairs
483     * if null or empty, remove entire extended headers
484     * @throws MessagingException
485     */
486    public void setExtendedHeaders(String headers) throws MessagingException {
487        if (TextUtils.isEmpty(headers)) {
488            mExtendedHeader = null;
489        } else {
490            mExtendedHeader = new MimeHeader();
491            for (final String header : END_OF_LINE.split(headers)) {
492                final String[] tokens = header.split(":", 2);
493                if (tokens.length != 2) {
494                    throw new MessagingException("Illegal extended headers: " + headers);
495                }
496                mExtendedHeader.setHeader(tokens[0].trim(), tokens[1].trim());
497            }
498        }
499    }
500
501    /**
502     * Get entire extended headers as String
503     *
504     * @return "CR-NL-separated extended headers - null if extended header does not exist
505     */
506    public String getExtendedHeaders() {
507        if (mExtendedHeader != null) {
508            return mExtendedHeader.writeToString();
509        }
510        return null;
511    }
512
513    /**
514     * Write message header and body to output stream
515     *
516     * @param out Output steam to write message header and body.
517     */
518    @Override
519    public void writeTo(OutputStream out) throws IOException, MessagingException {
520        final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
521        // Force creation of local message-id
522        getMessageId();
523        getMimeHeaders().writeTo(out);
524        // mExtendedHeader will not be write out to external output stream,
525        // because it is intended to internal use.
526        writer.write("\r\n");
527        writer.flush();
528        if (mBody != null) {
529            mBody.writeTo(out);
530        }
531    }
532
533    @Override
534    public InputStream getInputStream() throws MessagingException {
535        return null;
536    }
537
538    class MimeMessageBuilder implements ContentHandler {
539        private final Stack<Object> stack = new Stack<Object>();
540
541        public MimeMessageBuilder() {
542        }
543
544        private void expect(Class<?> c) {
545            if (!c.isInstance(stack.peek())) {
546                throw new IllegalStateException("Internal stack error: " + "Expected '"
547                        + c.getName() + "' found '" + stack.peek().getClass().getName() + "'");
548            }
549        }
550
551        @Override
552        public void startMessage() {
553            if (stack.isEmpty()) {
554                stack.push(MimeMessage.this);
555            } else {
556                expect(Part.class);
557                try {
558                    final MimeMessage m = new MimeMessage();
559                    ((Part)stack.peek()).setBody(m);
560                    stack.push(m);
561                } catch (MessagingException me) {
562                    throw new Error(me);
563                }
564            }
565        }
566
567        @Override
568        public void endMessage() {
569            expect(MimeMessage.class);
570            stack.pop();
571        }
572
573        @Override
574        public void startHeader() {
575            expect(Part.class);
576        }
577
578        @Override
579        public void field(String fieldData) {
580            expect(Part.class);
581            try {
582                final String[] tokens = fieldData.split(":", 2);
583                ((Part)stack.peek()).addHeader(tokens[0], tokens[1].trim());
584            } catch (MessagingException me) {
585                throw new Error(me);
586            }
587        }
588
589        @Override
590        public void endHeader() {
591            expect(Part.class);
592        }
593
594        @Override
595        public void startMultipart(BodyDescriptor bd) {
596            expect(Part.class);
597
598            final Part e = (Part)stack.peek();
599            try {
600                final MimeMultipart multiPart = new MimeMultipart(e.getContentType());
601                e.setBody(multiPart);
602                stack.push(multiPart);
603            } catch (MessagingException me) {
604                throw new Error(me);
605            }
606        }
607
608        @Override
609        public void body(BodyDescriptor bd, InputStream in) throws IOException {
610            expect(Part.class);
611            final Body body = MimeUtility.decodeBody(in, bd.getTransferEncoding());
612            try {
613                ((Part)stack.peek()).setBody(body);
614            } catch (MessagingException me) {
615                throw new Error(me);
616            }
617        }
618
619        @Override
620        public void endMultipart() {
621            stack.pop();
622        }
623
624        @Override
625        public void startBodyPart() {
626            expect(MimeMultipart.class);
627
628            try {
629                final MimeBodyPart bodyPart = new MimeBodyPart();
630                ((MimeMultipart)stack.peek()).addBodyPart(bodyPart);
631                stack.push(bodyPart);
632            } catch (MessagingException me) {
633                throw new Error(me);
634            }
635        }
636
637        @Override
638        public void endBodyPart() {
639            expect(BodyPart.class);
640            stack.pop();
641        }
642
643        @Override
644        public void epilogue(InputStream is) throws IOException {
645            expect(MimeMultipart.class);
646            final StringBuilder sb = new StringBuilder();
647            int b;
648            while ((b = is.read()) != -1) {
649                sb.append((char)b);
650            }
651            // TODO: why is this commented out?
652            // ((Multipart) stack.peek()).setEpilogue(sb.toString());
653        }
654
655        @Override
656        public void preamble(InputStream is) throws IOException {
657            expect(MimeMultipart.class);
658            final StringBuilder sb = new StringBuilder();
659            int b;
660            while ((b = is.read()) != -1) {
661                sb.append((char)b);
662            }
663            try {
664                ((MimeMultipart)stack.peek()).setPreamble(sb.toString());
665            } catch (MessagingException me) {
666                throw new Error(me);
667            }
668        }
669
670        @Override
671        public void raw(InputStream is) throws IOException {
672            throw new UnsupportedOperationException("Not supported");
673        }
674    }
675}
676