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  */
16 package com.android.phone.common.mail.internet;
17 
18 import com.android.phone.common.mail.Address;
19 import com.android.phone.common.mail.Body;
20 import com.android.phone.common.mail.BodyPart;
21 import com.android.phone.common.mail.Message;
22 import com.android.phone.common.mail.MessagingException;
23 import com.android.phone.common.mail.Multipart;
24 import com.android.phone.common.mail.Part;
25 import com.android.phone.common.mail.utils.LogUtils;
26 
27 import org.apache.james.mime4j.BodyDescriptor;
28 import org.apache.james.mime4j.ContentHandler;
29 import org.apache.james.mime4j.EOLConvertingInputStream;
30 import org.apache.james.mime4j.MimeStreamParser;
31 import org.apache.james.mime4j.field.DateTimeField;
32 import org.apache.james.mime4j.field.Field;
33 
34 import android.text.TextUtils;
35 
36 import java.io.BufferedWriter;
37 import java.io.IOException;
38 import java.io.InputStream;
39 import java.io.OutputStream;
40 import java.io.OutputStreamWriter;
41 import java.text.SimpleDateFormat;
42 import java.util.Date;
43 import java.util.Locale;
44 import java.util.Stack;
45 import 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  */
54 public 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 
MimeMessage()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      */
generateMessageId()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      */
MimeMessage(InputStream in)119     public MimeMessage(InputStream in) throws IOException, MessagingException {
120         parse(in);
121     }
122 
init()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 
parse(InputStream in)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 
parse(InputStream in, EOLConvertingInputStream.Callback callback)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      */
getMimeHeaders()158     private MimeHeader getMimeHeaders() {
159         if (mHeader == null) {
160             mHeader = new MimeHeader();
161         }
162         return mHeader;
163     }
164 
165     @Override
getReceivedDate()166     public Date getReceivedDate() throws MessagingException {
167         return null;
168     }
169 
170     @Override
getSentDate()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
setSentDate(Date sentDate)199     public void setSentDate(Date sentDate) throws MessagingException {
200         setHeader("Date", DATE_FORMAT.format(sentDate));
201         this.mSentDate = sentDate;
202     }
203 
204     @Override
getContentType()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
getDisposition()215     public String getDisposition() throws MessagingException {
216         return getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
217     }
218 
219     @Override
getContentId()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 
isComplete()230     public boolean isComplete() {
231         return mComplete;
232     }
233 
234     @Override
getMimeType()235     public String getMimeType() throws MessagingException {
236         return MimeUtility.getHeaderParameter(getContentType(), null);
237     }
238 
239     @Override
getSize()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
getRecipients(String type)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
setRecipients(String type, Address[] addresses)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
getSubject()308     public String getSubject() throws MessagingException {
309         return MimeUtility.unfoldAndDecode(getFirstHeader("Subject"));
310     }
311 
312     @Override
setSubject(String subject)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
getFrom()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
setFrom(Address from)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
getReplyTo()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
setReplyTo(Address[] replyTo)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
setMessageId(String messageId)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
getMessageId()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
saveChanges()390     public void saveChanges() throws MessagingException {
391         throw new MessagingException("saveChanges not yet implemented");
392     }
393 
394     @Override
getBody()395     public Body getBody() throws MessagingException {
396         return mBody;
397     }
398 
399     @Override
setBody(Body body)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 
getFirstHeader(String name)415     protected String getFirstHeader(String name) throws MessagingException {
416         return getMimeHeaders().getFirstHeader(name);
417     }
418 
419     @Override
addHeader(String name, String value)420     public void addHeader(String name, String value) throws MessagingException {
421         getMimeHeaders().addHeader(name, value);
422     }
423 
424     @Override
setHeader(String name, String value)425     public void setHeader(String name, String value) throws MessagingException {
426         getMimeHeaders().setHeader(name, value);
427     }
428 
429     @Override
getHeader(String name)430     public String[] getHeader(String name) throws MessagingException {
431         return getMimeHeaders().getHeader(name);
432     }
433 
434     @Override
removeHeader(String name)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
setExtendedHeader(String name, String value)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
getExtendedHeader(String name)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      */
setExtendedHeaders(String headers)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      */
getExtendedHeaders()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
writeTo(OutputStream out)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
getInputStream()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 
MimeMessageBuilder()541         public MimeMessageBuilder() {
542         }
543 
expect(Class<?> c)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
startMessage()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
endMessage()568         public void endMessage() {
569             expect(MimeMessage.class);
570             stack.pop();
571         }
572 
573         @Override
startHeader()574         public void startHeader() {
575             expect(Part.class);
576         }
577 
578         @Override
field(String fieldData)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
endHeader()590         public void endHeader() {
591             expect(Part.class);
592         }
593 
594         @Override
startMultipart(BodyDescriptor bd)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
body(BodyDescriptor bd, InputStream in)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
endMultipart()620         public void endMultipart() {
621             stack.pop();
622         }
623 
624         @Override
startBodyPart()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
endBodyPart()638         public void endBodyPart() {
639             expect(BodyPart.class);
640             stack.pop();
641         }
642 
643         @Override
epilogue(InputStream is)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
preamble(InputStream is)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
raw(InputStream is)671         public void raw(InputStream is) throws IOException {
672             throw new UnsupportedOperationException("Not supported");
673         }
674     }
675 }
676