1/*
2* Copyright (C) 2013 Samsung System LSI
3* Licensed under the Apache License, Version 2.0 (the "License");
4* you may not use this file except in compliance with the License.
5* You may obtain a copy of the License at
6*
7*      http://www.apache.org/licenses/LICENSE-2.0
8*
9* Unless required by applicable law or agreed to in writing, software
10* distributed under the License is distributed on an "AS IS" BASIS,
11* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12* See the License for the specific language governing permissions and
13* limitations under the License.
14*/
15package com.android.bluetooth.map;
16
17import java.io.UnsupportedEncodingException;
18import java.nio.charset.Charset;
19import java.nio.charset.IllegalCharsetNameException;
20import java.text.SimpleDateFormat;
21import java.util.ArrayList;
22import java.util.Arrays;
23import java.util.Date;
24import java.util.Locale;
25import java.util.UUID;
26
27import android.text.util.Rfc822Token;
28import android.text.util.Rfc822Tokenizer;
29import android.util.Base64;
30import android.util.Log;
31
32public class BluetoothMapbMessageMime extends BluetoothMapbMessage {
33
34    public static class MimePart {
35        public long mId = INVALID_VALUE;   /* The _id from the content provider, can be used to
36                                            * sort the parts if needed */
37        public String mContentType = null; /* The mime type, e.g. text/plain */
38        public String mContentId = null;
39        public String mContentLocation = null;
40        public String mContentDisposition = null;
41        public String mPartName = null;    /* e.g. text_1.txt*/
42        public String mCharsetName = null; /* This seems to be a number e.g. 106 for UTF-8
43                                              CharacterSets holds a method for the mapping. */
44        public String mFileName = null;     /* Do not seem to be used */
45        public byte[] mData = null;        /* The raw un-encoded data e.g. the raw
46                                            * jpeg data or the text.getBytes("utf-8") */
47
48
49        String getDataAsString() {
50            String result = null;
51            String charset = mCharsetName;
52            // Figure out if we support the charset, else fall back to UTF-8, as this is what
53            // the MAP specification suggest to use, and is compatible with US-ASCII.
54            if(charset == null){
55                charset = "UTF-8";
56            } else {
57                charset = charset.toUpperCase();
58                try {
59                    if(Charset.isSupported(charset) == false) {
60                        charset = "UTF-8";
61                    }
62                } catch (IllegalCharsetNameException e) {
63                    Log.w(TAG, "Received unknown charset: " + charset + " - using UTF-8.");
64                    charset = "UTF-8";
65                }
66            }
67            try{
68                result = new String(mData, charset);
69            } catch (UnsupportedEncodingException e) {
70                /* This cannot happen unless Charset.isSupported() is out of sync with String */
71                try{
72                    result = new String(mData, "UTF-8");
73                } catch (UnsupportedEncodingException e2) {/* This cannot happen */}
74            }
75            return result;
76        }
77
78        public void encode(StringBuilder sb, String boundaryTag, boolean last)
79                                                       throws UnsupportedEncodingException {
80            sb.append("--").append(boundaryTag).append("\r\n");
81            if(mContentType != null)
82                sb.append("Content-Type: ").append(mContentType);
83            if(mCharsetName != null)
84                sb.append("; ").append("charset=\"").append(mCharsetName).append("\"");
85            sb.append("\r\n");
86            if(mContentLocation != null)
87                sb.append("Content-Location: ").append(mContentLocation).append("\r\n");
88            if(mContentId != null)
89                sb.append("Content-ID: ").append(mContentId).append("\r\n");
90            if(mContentDisposition != null)
91                sb.append("Content-Disposition: ").append(mContentDisposition).append("\r\n");
92            if(mData != null) {
93                /* TODO: If errata 4176 is adopted in the current form (it is not in either 1.1 or 1.2),
94                the below use of UTF-8 is not allowed, Base64 should be used for text. */
95
96                if(mContentType != null &&
97                        (mContentType.toUpperCase().contains("TEXT") ||
98                         mContentType.toUpperCase().contains("SMIL") )) {
99                    String text = new String(mData,"UTF-8");
100                    if(text.getBytes().length == text.getBytes("UTF-8").length){
101                        /* Add the header split empty line */
102                        sb.append("Content-Transfer-Encoding: 8BIT\r\n\r\n");
103                    }else {
104                        /* Add the header split empty line */
105                        sb.append("Content-Transfer-Encoding: Quoted-Printable\r\n\r\n");
106                        text = BluetoothMapUtils.encodeQuotedPrintable(mData);
107                    }
108                    sb.append(text).append("\r\n");
109                }
110                else {
111                    /* Add the header split empty line */
112                    sb.append("Content-Transfer-Encoding: Base64\r\n\r\n");
113                    sb.append(Base64.encodeToString(mData, Base64.DEFAULT)).append("\r\n");
114                }
115            }
116            if(last) {
117                sb.append("--").append(boundaryTag).append("--").append("\r\n");
118            }
119        }
120
121        public void encodePlainText(StringBuilder sb) throws UnsupportedEncodingException {
122            if(mContentType != null && mContentType.toUpperCase().contains("TEXT")) {
123                String text = new String(mData, "UTF-8");
124                if(text.getBytes().length != text.getBytes("UTF-8").length){
125                        text = BluetoothMapUtils.encodeQuotedPrintable(mData);
126                }
127                sb.append(text).append("\r\n");
128            } else if(mContentType != null && mContentType.toUpperCase().contains("/SMIL")) {
129                /* Skip the smil.xml, as no-one knows what it is. */
130            } else {
131                /* Not a text part, just print the filename or part name if they exist. */
132                if(mPartName != null)
133                    sb.append("<").append(mPartName).append(">\r\n");
134                else
135                    sb.append("<").append("attachment").append(">\r\n");
136            }
137        }
138    }
139
140    private long date = INVALID_VALUE;
141    private String subject = null;
142    private ArrayList<Rfc822Token> from = null;   // Shall not be empty
143    private ArrayList<Rfc822Token> sender = null;   // Shall not be empty
144    private ArrayList<Rfc822Token> to = null;     // Shall not be empty
145    private ArrayList<Rfc822Token> cc = null;     // Can be empty
146    private ArrayList<Rfc822Token> bcc = null;    // Can be empty
147    private ArrayList<Rfc822Token> replyTo = null;// Can be empty
148    private String messageId = null;
149    private ArrayList<MimePart> parts = null;
150    private String contentType = null;
151    private String boundary = null;
152    private boolean textOnly = false;
153    private boolean includeAttachments;
154    private boolean hasHeaders = false;
155    private String encoding = null;
156
157    private String getBoundary() {
158        if(boundary == null)
159            // Include "=_" as these cannot occur in quoted printable text
160            boundary = "--=_" + UUID.randomUUID();
161        return boundary;
162    }
163
164    /**
165     * @return the parts
166     */
167    public ArrayList<MimePart> getMimeParts() {
168        return parts;
169    }
170
171    public String getMessageAsText() {
172        StringBuilder sb = new StringBuilder();
173        if(subject != null && !subject.isEmpty()) {
174            sb.append("<Sub:").append(subject).append("> ");
175        }
176        if(parts != null) {
177            for(MimePart part : parts) {
178                if(part.mContentType.toUpperCase().contains("TEXT")) {
179                    sb.append(new String(part.mData));
180                }
181            }
182        }
183        return sb.toString();
184    }
185    public MimePart addMimePart() {
186        if(parts == null)
187            parts = new ArrayList<BluetoothMapbMessageMime.MimePart>();
188        MimePart newPart = new MimePart();
189        parts.add(newPart);
190        return newPart;
191    }
192    public String getDateString() {
193        SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
194        Date dateObj = new Date(date);
195        return format.format(dateObj); // Format according to RFC 2822 page 14
196    }
197    public long getDate() {
198        return date;
199    }
200    public void setDate(long date) {
201        this.date = date;
202    }
203    public String getSubject() {
204        return subject;
205    }
206    public void setSubject(String subject) {
207        this.subject = subject;
208    }
209    public ArrayList<Rfc822Token> getFrom() {
210        return from;
211    }
212    public void setFrom(ArrayList<Rfc822Token> from) {
213        this.from = from;
214    }
215    public void addFrom(String name, String address) {
216        if(this.from == null)
217            this.from = new ArrayList<Rfc822Token>(1);
218        this.from.add(new Rfc822Token(name, address, null));
219    }
220    public ArrayList<Rfc822Token> getSender() {
221        return sender;
222    }
223    public void setSender(ArrayList<Rfc822Token> sender) {
224        this.sender = sender;
225    }
226    public void addSender(String name, String address) {
227        if(this.sender == null)
228            this.sender = new ArrayList<Rfc822Token>(1);
229        this.sender.add(new Rfc822Token(name,address,null));
230    }
231    public ArrayList<Rfc822Token> getTo() {
232        return to;
233    }
234    public void setTo(ArrayList<Rfc822Token> to) {
235        this.to = to;
236    }
237    public void addTo(String name, String address) {
238        if(this.to == null)
239            this.to = new ArrayList<Rfc822Token>(1);
240        this.to.add(new Rfc822Token(name, address, null));
241    }
242    public ArrayList<Rfc822Token> getCc() {
243        return cc;
244    }
245    public void setCc(ArrayList<Rfc822Token> cc) {
246        this.cc = cc;
247    }
248    public void addCc(String name, String address) {
249        if(this.cc == null)
250            this.cc = new ArrayList<Rfc822Token>(1);
251        this.cc.add(new Rfc822Token(name, address, null));
252    }
253    public ArrayList<Rfc822Token> getBcc() {
254        return bcc;
255    }
256    public void setBcc(ArrayList<Rfc822Token> bcc) {
257        this.bcc = bcc;
258    }
259    public void addBcc(String name, String address) {
260        if(this.bcc == null)
261            this.bcc = new ArrayList<Rfc822Token>(1);
262        this.bcc.add(new Rfc822Token(name, address, null));
263    }
264    public ArrayList<Rfc822Token> getReplyTo() {
265        return replyTo;
266    }
267    public void setReplyTo(ArrayList<Rfc822Token> replyTo) {
268        this.replyTo = replyTo;
269    }
270    public void addReplyTo(String name, String address) {
271        if(this.replyTo == null)
272            this.replyTo = new ArrayList<Rfc822Token>(1);
273        this.replyTo.add(new Rfc822Token(name, address, null));
274    }
275    public void setMessageId(String messageId) {
276        this.messageId = messageId;
277    }
278    public String getMessageId() {
279        return messageId;
280    }
281    public void setContentType(String contentType) {
282        this.contentType = contentType;
283    }
284    public String getContentType() {
285        return contentType;
286    }
287    public void setTextOnly(boolean textOnly) {
288        this.textOnly = textOnly;
289    }
290    public boolean getTextOnly() {
291        return textOnly;
292    }
293    public void setIncludeAttachments(boolean includeAttachments) {
294        this.includeAttachments = includeAttachments;
295    }
296    public boolean getIncludeAttachments() {
297        return includeAttachments;
298    }
299    public void updateCharset() {
300        if(parts != null) {
301            mCharset = null;
302            for(MimePart part : parts) {
303                if(part.mContentType != null &&
304                   part.mContentType.toUpperCase().contains("TEXT")) {
305                    mCharset = "UTF-8";
306                    if(V) Log.v(TAG,"Charset set to UTF-8");
307                    break;
308                }
309            }
310        }
311    }
312    public int getSize() {
313        int message_size = 0;
314        if(parts != null) {
315            for(MimePart part : parts) {
316                message_size += part.mData.length;
317            }
318        }
319        return message_size;
320    }
321
322    /**
323     * Encode an address header, and perform folding if needed.
324     * @param sb The stringBuilder to write to
325     * @param headerName The RFC 2822 header name
326     * @param addresses the reformatted address substrings to encode.
327     */
328    public void encodeHeaderAddresses(StringBuilder sb, String headerName,
329            ArrayList<Rfc822Token> addresses) {
330        /* TODO: Do we need to encode the addresses if they contain illegal characters?
331         * This depends of the outcome of errata 4176. The current spec. states to use UTF-8
332         * where possible, but the RFCs states to use US-ASCII for the headers - hence encoding
333         * would be needed to support non US-ASCII characters. But the MAP spec states not to
334         * use any encoding... */
335        int partLength, lineLength = 0;
336        lineLength += headerName.getBytes().length;
337        sb.append(headerName);
338        for(Rfc822Token address : addresses) {
339            partLength = address.toString().getBytes().length+1;
340            // Add folding if needed
341            if(lineLength + partLength >= 998) // max line length in RFC2822
342            {
343                sb.append("\r\n "); // Append a FWS (folding whitespace)
344                lineLength = 0;
345            }
346            sb.append(address.toString()).append(";");
347            lineLength += partLength;
348        }
349        sb.append("\r\n");
350    }
351
352    public void encodeHeaders(StringBuilder sb) throws UnsupportedEncodingException
353    {
354        /* TODO: From RFC-4356 - about the RFC-(2)822 headers:
355         *    "Current Internet Message format requires that only 7-bit US-ASCII
356         *     characters be present in headers.  Non-7-bit characters in an address
357         *     domain must be encoded with [IDN].  If there are any non-7-bit
358         *     characters in the local part of an address, the message MUST be
359         *     rejected.  Non-7-bit characters elsewhere in a header MUST be encoded
360         *     according to [Hdr-Enc]."
361         *    We need to add the address encoding in encodeHeaderAddresses, but it is not
362         *    straight forward, as it is unclear how to do this.  */
363        if (date != INVALID_VALUE)
364            sb.append("Date: ").append(getDateString()).append("\r\n");
365        /* According to RFC-2822 headers must use US-ASCII, where the MAP specification states
366         * UTF-8 should be used for the entire <bmessage-body-content>. We let the MAP specification
367         * take precedence above the RFC-2822.
368         */
369        /* If we are to use US-ASCII anyway, here is the code for it for base64.
370          if (subject != null){
371            // Use base64 encoding for the subject, as it may contain non US-ASCII characters or
372            // other illegal (RFC822 header), and android do not seem to have encoders/decoders
373            // for quoted-printables
374            sb.append("Subject:").append("=?utf-8?B?");
375            sb.append(Base64.encodeToString(subject.getBytes("utf-8"), Base64.DEFAULT));
376            sb.append("?=\r\n");
377        }*/
378        if (subject != null)
379            sb.append("Subject: ").append(subject).append("\r\n");
380        if(from == null)
381            sb.append("From: \r\n");
382        if(from != null)
383            encodeHeaderAddresses(sb, "From: ", from); // This includes folding if needed.
384        if(sender != null)
385            encodeHeaderAddresses(sb, "Sender: ", sender); // This includes folding if needed.
386        /* For MMS one recipient(to, cc or bcc) must exists, if none: 'To:  undisclosed-
387         * recipients:;' could be used.
388         */
389        if(to == null && cc == null && bcc == null)
390            sb.append("To:  undisclosed-recipients:;\r\n");
391        if(to != null)
392            encodeHeaderAddresses(sb, "To: ", to); // This includes folding if needed.
393        if(cc != null)
394            encodeHeaderAddresses(sb, "Cc: ", cc); // This includes folding if needed.
395        if(bcc != null)
396            encodeHeaderAddresses(sb, "Bcc: ", bcc); // This includes folding if needed.
397        if(replyTo != null)
398            encodeHeaderAddresses(sb, "Reply-To: ", replyTo); // This includes folding if needed.
399        if(includeAttachments == true)
400        {
401            if(messageId != null)
402                sb.append("Message-Id: ").append(messageId).append("\r\n");
403            if(contentType != null)
404                sb.append("Content-Type: ").append(
405                        contentType).append("; boundary=").append(getBoundary()).append("\r\n");
406        }
407     // If no headers exists, we still need two CRLF, hence keep it out of the if above.
408        sb.append("\r\n");
409    }
410
411    /* Notes on MMS
412     * ------------
413     * According to rfc4356 all headers of a MMS converted to an E-mail must use
414     * 7-bit encoding. According the the MAP specification only 8-bit encoding is
415     * allowed - hence the bMessage-body should contain no SMTP headers. (Which makes
416     * sense, since the info is already present in the bMessage properties.)
417     * The result is that no information from RFC4356 is needed, since it does not
418     * describe any mapping between MMS content and E-mail content.
419     * Suggestion:
420     * Clearly state in the MAP specification that
421     * only the actual message content should be included in the <bmessage-body-content>.
422     * Correct the Example to not include the E-mail headers, and in stead show how to
423     * include a picture or another binary attachment.
424     *
425     * If the headers should be included, clearly state which, as the example clearly shows
426     * that some of the headers should be excluded.
427     * Additionally it is not clear how to handle attachments. There is a parameter in the
428     * get message to include attachments, but since only 8-bit encoding is allowed,
429     * (hence neither base64 nor binary) there is no mechanism to embed the attachment in
430     * the <bmessage-body-content>.
431     *
432     * UPDATE: Errata 4176 allows the needed encoding typed inside the <bmessage-body-content>
433     * including Base64 and Quoted Printables - hence it is possible to encode non-us-ascii
434     * messages - e.g. pictures and utf-8 strings with non-us-ascii content.
435     * It have not yet been adopted, but since the comments clearly suggest that it is allowed
436     * to use encoding schemes for non-text parts, it is still not clear what to do about non
437     * US-ASCII text in the headers.
438     * */
439
440    /**
441     * Encode the bMessage as a Mime message(MMS/IM)
442     * @return
443     * @throws UnsupportedEncodingException
444     */
445    public byte[] encodeMime() throws UnsupportedEncodingException
446    {
447        ArrayList<byte[]> bodyFragments = new ArrayList<byte[]>();
448        StringBuilder sb = new StringBuilder();
449        int count = 0;
450        String mimeBody;
451
452        encoding = "8BIT"; // The encoding used
453
454        encodeHeaders(sb);
455        if(parts != null) {
456            if(getIncludeAttachments() == false) {
457                for(MimePart part : parts) {
458                    /* We call encode on all parts, to include a tag,
459                     * where an attachment is missing. */
460                    part.encodePlainText(sb);
461                }
462            } else {
463                for(MimePart part : parts) {
464                    count++;
465                    part.encode(sb, getBoundary(), (count == parts.size()));
466                }
467            }
468        }
469
470        mimeBody = sb.toString();
471
472        if(mimeBody != null) {
473           // Replace any occurrences of END:MSG with \END:MSG
474            String tmpBody = mimeBody.replaceAll("END:MSG", "/END\\:MSG");
475            bodyFragments.add(tmpBody.getBytes("UTF-8"));
476        } else {
477            bodyFragments.add(new byte[0]);
478        }
479
480        return encodeGeneric(bodyFragments);
481    }
482
483
484    /**
485     * Try to parse the hdrPart string as e-mail headers.
486     * @param hdrPart The string to parse.
487     * @return Null if the entire string were e-mail headers. The part of the string in which
488     * no headers were found.
489     */
490    private String parseMimeHeaders(String hdrPart) {
491        String[] headers = hdrPart.split("\r\n");
492        if(D) Log.d(TAG,"Header count=" + headers.length);
493        String header;
494        hasHeaders = false;
495
496        for(int i = 0, c = headers.length; i < c; i++) {
497            header = headers[i];
498            if(D) Log.d(TAG,"Header[" + i + "]: " + header);
499            /* We need to figure out if any headers are present, in cases where devices do
500             * not follow the e-mail RFCs.
501             * Skip empty lines, and then parse headers until a non-header line is found,
502             * at which point we treat the remaining as plain text.
503             */
504            if(header.trim() == "")
505                continue;
506            String[] headerParts = header.split(":",2);
507            if(headerParts.length != 2) {
508                // We treat the remaining content as plain text.
509                StringBuilder remaining = new StringBuilder();
510                for(; i < c; i++)
511                    remaining.append(headers[i]);
512
513                return remaining.toString();
514            }
515
516            String headerType = headerParts[0].toUpperCase();
517            String headerValue = headerParts[1].trim();
518
519            // Address headers
520            /* If this is empty, the MSE needs to fill it in before sending the message.
521             * This happens when sending the MMS.
522             */
523            if(headerType.contains("FROM")) {
524                headerValue = BluetoothMapUtils.stripEncoding(headerValue);
525                Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue);
526                from = new ArrayList<Rfc822Token>(Arrays.asList(tokens));
527            } else if(headerType.contains("TO")) {
528                headerValue = BluetoothMapUtils.stripEncoding(headerValue);
529                Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue);
530                to = new ArrayList<Rfc822Token>(Arrays.asList(tokens));
531            } else if(headerType.contains("CC")) {
532                headerValue = BluetoothMapUtils.stripEncoding(headerValue);
533                Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue);
534                cc = new ArrayList<Rfc822Token>(Arrays.asList(tokens));
535            } else if(headerType.contains("BCC")) {
536                headerValue = BluetoothMapUtils.stripEncoding(headerValue);
537                Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue);
538                bcc = new ArrayList<Rfc822Token>(Arrays.asList(tokens));
539            } else if(headerType.contains("REPLY-TO")) {
540                headerValue = BluetoothMapUtils.stripEncoding(headerValue);
541                Rfc822Token tokens[] = Rfc822Tokenizer.tokenize(headerValue);
542                replyTo = new ArrayList<Rfc822Token>(Arrays.asList(tokens));
543            } else if(headerType.contains("SUBJECT")) { // Other headers
544                subject = BluetoothMapUtils.stripEncoding(headerValue);
545            } else if(headerType.contains("MESSAGE-ID")) {
546                messageId = headerValue;
547            } else if(headerType.contains("DATE")) {
548                /* The date is not needed, as the time stamp will be set in the DB
549                 * when the message is send. */
550            } else if(headerType.contains("MIME-VERSION")) {
551                /* The mime version is not needed */
552            } else if(headerType.contains("CONTENT-TYPE")) {
553                String[] contentTypeParts = headerValue.split(";");
554                contentType = contentTypeParts[0];
555                // Extract the boundary if it exists
556                for(int j=1, n=contentTypeParts.length; j<n; j++)
557                {
558                    if(contentTypeParts[j].contains("boundary")) {
559                        boundary = contentTypeParts[j].split("boundary[\\s]*=", 2)[1].trim();
560                        // removing quotes from boundary string
561                        if ((boundary.charAt(0) == '\"')
562                                && (boundary.charAt(boundary.length()-1) == '\"'))
563                            boundary = boundary.substring(1, boundary.length()-1);
564                        if(D) Log.d(TAG,"Boundary tag=" + boundary);
565                    } else if(contentTypeParts[j].contains("charset")) {
566                        mCharset = contentTypeParts[j].split("charset[\\s]*=", 2)[1].trim();
567                    }
568                }
569            } else if(headerType.contains("CONTENT-TRANSFER-ENCODING")) {
570                encoding = headerValue;
571            } else {
572                if(D) Log.w(TAG,"Skipping unknown header: " + headerType + " (" + header + ")");
573            }
574        }
575        return null;
576    }
577
578    private void parseMimePart(String partStr) {
579        String[] parts = partStr.split("\r\n\r\n", 2); // Split the header from the body
580        MimePart newPart = addMimePart();
581        String partEncoding = encoding; /* Use the overall encoding as default */
582        String body;
583
584        String[] headers = parts[0].split("\r\n");
585        if(D) Log.d(TAG, "parseMimePart: headers count=" + headers.length);
586
587        if(parts.length != 2) {
588            body = partStr;
589        } else {
590            for(String header : headers) {
591                // Skip empty lines(the \r\n after the boundary tag) and endBoundary tags
592                if((header.length() == 0)
593                        || (header.trim().isEmpty())
594                        || header.trim().equals("--"))
595                    continue;
596
597                String[] headerParts = header.split(":",2);
598                if(headerParts.length != 2) {
599                    if(D) Log.w(TAG, "part-Header not formatted correctly: ");
600                    continue;
601                }
602                if(D) Log.d(TAG, "parseMimePart: header=" + header);
603                String headerType = headerParts[0].toUpperCase();
604                String headerValue = headerParts[1].trim();
605                if(headerType.contains("CONTENT-TYPE")) {
606                    String[] contentTypeParts = headerValue.split(";");
607                    newPart.mContentType = contentTypeParts[0];
608                    // Extract the boundary if it exists
609                    for(int j=1, n=contentTypeParts.length; j<n; j++)
610                    {
611                        String value = contentTypeParts[j].toLowerCase();
612                        if(value.contains("charset")) {
613                            newPart.mCharsetName = value.split("charset[\\s]*=", 2)[1].trim();
614                        }
615                    }
616                }
617                else if(headerType.contains("CONTENT-LOCATION")) {
618                    // This is used if the smil refers to a file name in its src
619                    newPart.mContentLocation = headerValue;
620                    newPart.mPartName = headerValue;
621                }
622                else if(headerType.contains("CONTENT-TRANSFER-ENCODING")) {
623                    partEncoding = headerValue;
624                }
625                else if(headerType.contains("CONTENT-ID")) {
626                    // This is used if the smil refers to a cid:<xxx> in it's src
627                    newPart.mContentId = headerValue;
628                }
629                else if(headerType.contains("CONTENT-DISPOSITION")) {
630                    // This is used if the smil refers to a cid:<xxx> in it's src
631                    newPart.mContentDisposition = headerValue;
632                }
633                else {
634                    if(D) Log.w(TAG,"Skipping unknown part-header: " + headerType
635                                                                     + " (" + header + ")");
636                }
637            }
638            body = parts[1];
639            if(body.length() > 2) {
640                if(body.charAt(body.length()-2) == '\r'
641                        && body.charAt(body.length()-2) == '\n') {
642                    body = body.substring(0, body.length()-2);
643                }
644            }
645        }
646        // Now for the body
647        newPart.mData = decodeBody(body, partEncoding, newPart.mCharsetName);
648    }
649
650    private void parseMimeBody(String body) {
651        MimePart newPart = addMimePart();
652        newPart.mCharsetName = mCharset;
653        newPart.mData = decodeBody(body, encoding, mCharset);
654    }
655
656    private byte[] decodeBody(String body, String encoding, String charset) {
657        if(encoding != null && encoding.toUpperCase().contains("BASE64")) {
658            return Base64.decode(body, Base64.DEFAULT);
659        } else if(encoding != null && encoding.toUpperCase().contains("QUOTED-PRINTABLE")) {
660            return BluetoothMapUtils.quotedPrintableToUtf8(body, charset);
661        }else{
662            // TODO: handle other encoding types? - here we simply store the string data as bytes
663            try {
664
665                return body.getBytes("UTF-8");
666            } catch (UnsupportedEncodingException e) {
667                // This will never happen, as UTF-8 is mandatory on Android platforms
668            }
669        }
670        return null;
671    }
672
673    private void parseMime(String message) {
674        /* Overall strategy for decoding:
675         * 1) split on first empty line to extract the header
676         * 2) unfold and parse headers
677         * 3) split on boundary to split into parts (or use the remaining as a part,
678         *    if part is not found)
679         * 4) parse each part
680         * */
681        String[] messageParts;
682        String[] mimeParts;
683        String remaining = null;
684        String messageBody = null;
685        message = message.replaceAll("\\r\\n[ \\\t]+", ""); // Unfold
686        messageParts = message.split("\r\n\r\n", 2); // Split the header from the body
687        if(messageParts.length != 2) {
688            // Handle entire message as plain text
689            messageBody = message;
690        }
691        else
692        {
693            remaining = parseMimeHeaders(messageParts[0]);
694            // If we have some text not being a header, add it to the message body.
695            if(remaining != null) {
696                messageBody = remaining + messageParts[1];
697                if(D) Log.d(TAG, "parseMime remaining=" + remaining );
698            } else {
699                messageBody = messageParts[1];
700            }
701        }
702
703        if(boundary == null)
704        {
705            // If the boundary is not set, handle as non-multi-part
706            parseMimeBody(messageBody);
707            setTextOnly(true);
708            if(contentType == null)
709                contentType = "text/plain";
710            parts.get(0).mContentType = contentType;
711        }
712        else
713        {
714            mimeParts = messageBody.split("--" + boundary);
715            if(D) Log.d(TAG, "mimePart count=" + mimeParts.length);
716            // Part 0 is the message to clients not capable of decoding MIME
717            for(int i = 1; i < mimeParts.length - 1; i++) {
718                String part = mimeParts[i];
719                if (part != null && (part.length() > 0))
720                    parseMimePart(part);
721            }
722        }
723    }
724
725    /* Notes on SMIL decoding (from http://tools.ietf.org/html/rfc2557):
726     * src="filename.jpg" refers to a part with Content-Location: filename.jpg
727     * src="cid:1234@hest.net" refers to a part with Content-ID:<1234@hest.net>*/
728    @Override
729    public void parseMsgPart(String msgPart) {
730        parseMime(msgPart);
731
732    }
733
734    @Override
735    public void parseMsgInit() {
736        // Not used for e-mail
737
738    }
739
740    @Override
741    public byte[] encode() throws UnsupportedEncodingException {
742        return encodeMime();
743    }
744
745}
746