Home | History | Annotate | Download | only in exchange
      1 /*
      2  * Copyright (C) 2008-2009 Marc Blank
      3  * Licensed to The Android Open Source Project.
      4  *
      5  * Licensed under the Apache License, Version 2.0 (the "License");
      6  * you may not use this file except in compliance with the License.
      7  * You may obtain a copy of the License at
      8  *
      9  *      http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  * Unless required by applicable law or agreed to in writing, software
     12  * distributed under the License is distributed on an "AS IS" BASIS,
     13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  * See the License for the specific language governing permissions and
     15  * limitations under the License.
     16  */
     17 
     18 package com.android.exchange;
     19 
     20 import android.content.ContentUris;
     21 import android.content.ContentValues;
     22 import android.content.Context;
     23 import android.database.Cursor;
     24 import android.net.TrafficStats;
     25 import android.net.Uri;
     26 import android.os.RemoteException;
     27 import android.text.TextUtils;
     28 
     29 import com.android.emailcommon.TrafficFlags;
     30 import com.android.emailcommon.internet.Rfc822Output;
     31 import com.android.emailcommon.mail.MessagingException;
     32 import com.android.emailcommon.provider.Account;
     33 import com.android.emailcommon.provider.EmailContent.Body;
     34 import com.android.emailcommon.provider.EmailContent.BodyColumns;
     35 import com.android.emailcommon.provider.EmailContent.MailboxColumns;
     36 import com.android.emailcommon.provider.EmailContent.Message;
     37 import com.android.emailcommon.provider.EmailContent.MessageColumns;
     38 import com.android.emailcommon.provider.EmailContent.SyncColumns;
     39 import com.android.emailcommon.provider.Mailbox;
     40 import com.android.emailcommon.service.EmailServiceStatus;
     41 import com.android.emailcommon.utility.Utility;
     42 import com.android.exchange.CommandStatusException.CommandStatus;
     43 import com.android.exchange.adapter.Parser;
     44 import com.android.exchange.adapter.Parser.EmptyStreamException;
     45 import com.android.exchange.adapter.Serializer;
     46 import com.android.exchange.adapter.Tags;
     47 
     48 import org.apache.http.HttpEntity;
     49 import org.apache.http.HttpStatus;
     50 import org.apache.http.entity.InputStreamEntity;
     51 
     52 import java.io.ByteArrayOutputStream;
     53 import java.io.File;
     54 import java.io.FileInputStream;
     55 import java.io.FileOutputStream;
     56 import java.io.IOException;
     57 import java.io.InputStream;
     58 import java.io.OutputStream;
     59 
     60 public class EasOutboxService extends EasSyncService {
     61 
     62     public static final int SEND_FAILED = 1;
     63     public static final String MAILBOX_KEY_AND_NOT_SEND_FAILED =
     64         MessageColumns.MAILBOX_KEY + "=? and (" + SyncColumns.SERVER_ID + " is null or " +
     65         SyncColumns.SERVER_ID + "!=" + SEND_FAILED + ')';
     66     public static final String[] BODY_SOURCE_PROJECTION =
     67         new String[] {BodyColumns.SOURCE_MESSAGE_KEY};
     68     public static final String WHERE_MESSAGE_KEY = Body.MESSAGE_KEY + "=?";
     69 
     70     // This is a normal email (i.e. not one of the other types)
     71     public static final int MODE_NORMAL = 0;
     72     // This is a smart reply email
     73     public static final int MODE_SMART_REPLY = 1;
     74     // This is a smart forward email
     75     public static final int MODE_SMART_FORWARD = 2;
     76 
     77     // This needs to be long enough to send the longest reasonable message, without being so long
     78     // as to effectively "hang" sending of mail.  The standard 30 second timeout isn't long enough
     79     // for pictures and the like.  For now, we'll use 15 minutes, in the knowledge that any socket
     80     // failure would probably generate an Exception before timing out anyway
     81     public static final int SEND_MAIL_TIMEOUT = 15*MINUTES;
     82 
     83     protected EasOutboxService(Context _context, Mailbox _mailbox) {
     84         super(_context, _mailbox);
     85     }
     86 
     87     /**
     88      * Our own HttpEntity subclass that is able to insert opaque data (in this case the MIME
     89      * representation of the message body as stored in a temporary file) into the serializer stream
     90      */
     91     private static class SendMailEntity extends InputStreamEntity {
     92         private final Context mContext;
     93         private final FileInputStream mFileStream;
     94         private final long mFileLength;
     95         private final int mSendTag;
     96         private final Message mMessage;
     97 
     98         private static final int[] MODE_TAGS =  new int[] {Tags.COMPOSE_SEND_MAIL,
     99             Tags.COMPOSE_SMART_REPLY, Tags.COMPOSE_SMART_FORWARD};
    100 
    101         public SendMailEntity(Context context, FileInputStream instream, long length, int tag,
    102                 Message message) {
    103             super(instream, length);
    104             mContext = context;
    105             mFileStream = instream;
    106             mFileLength = length;
    107             mSendTag = tag;
    108             mMessage = message;
    109         }
    110 
    111         /**
    112          * We always return -1 because we don't know the actual length of the POST data (this
    113          * causes HttpClient to send the data in "chunked" mode)
    114          */
    115         @Override
    116         public long getContentLength() {
    117             ByteArrayOutputStream baos = new ByteArrayOutputStream();
    118             try {
    119                 // Calculate the overhead for the WBXML data
    120                 writeTo(baos, false);
    121                 // Return the actual size that will be sent
    122                 return baos.size() + mFileLength;
    123             } catch (IOException e) {
    124                 // Just return -1 (unknown)
    125             } finally {
    126                 try {
    127                     baos.close();
    128                 } catch (IOException e) {
    129                     // Ignore
    130                 }
    131             }
    132             return -1;
    133         }
    134 
    135         @Override
    136         public void writeTo(OutputStream outstream) throws IOException {
    137             writeTo(outstream, true);
    138         }
    139 
    140         /**
    141          * Write the message to the output stream
    142          * @param outstream the output stream to write
    143          * @param withData whether or not the actual data is to be written; true when sending
    144          *   mail; false when calculating size only
    145          * @throws IOException
    146          */
    147         public void writeTo(OutputStream outstream, boolean withData) throws IOException {
    148             // Not sure if this is possible; the check is taken from the superclass
    149             if (outstream == null) {
    150                 throw new IllegalArgumentException("Output stream may not be null");
    151             }
    152 
    153             // We'll serialize directly into the output stream
    154             Serializer s = new Serializer(outstream);
    155             // Send the appropriate initial tag
    156             s.start(mSendTag);
    157             // The Message-Id for this message (note that we cannot use the messageId stored in
    158             // the message, as EAS 14 limits the length to 40 chars and we use 70+)
    159             s.data(Tags.COMPOSE_CLIENT_ID, "SendMail-" + System.nanoTime());
    160             // We always save sent mail
    161             s.tag(Tags.COMPOSE_SAVE_IN_SENT_ITEMS);
    162 
    163             // If we're using smart reply/forward, we need info about the original message
    164             if (mSendTag != Tags.COMPOSE_SEND_MAIL) {
    165                 OriginalMessageInfo info = getOriginalMessageInfo(mContext, mMessage.mId);
    166                 if (info != null) {
    167                     s.start(Tags.COMPOSE_SOURCE);
    168                     // For search results, use the long id (stored in mProtocolSearchInfo); else,
    169                     // use folder id/item id combo
    170                     if (mMessage.mProtocolSearchInfo != null) {
    171                         s.data(Tags.COMPOSE_LONG_ID, mMessage.mProtocolSearchInfo);
    172                     } else {
    173                         s.data(Tags.COMPOSE_ITEM_ID, info.mItemId);
    174                         s.data(Tags.COMPOSE_FOLDER_ID, info.mCollectionId);
    175                     }
    176                     s.end();  // Tags.COMPOSE_SOURCE
    177                 }
    178             }
    179 
    180             // Start the MIME tag; this is followed by "opaque" data (byte array)
    181             s.start(Tags.COMPOSE_MIME);
    182             // Send opaque data from the file stream
    183             if (withData) {
    184                 s.opaque(mFileStream, (int)mFileLength);
    185             } else {
    186                 s.opaqueWithoutData((int)mFileLength);
    187             }
    188             // And we're done
    189             s.end().end().done();
    190         }
    191     }
    192 
    193     private static class SendMailParser extends Parser {
    194         private final int mStartTag;
    195         private int mStatus;
    196 
    197         public SendMailParser(InputStream in, int startTag) throws IOException {
    198             super(in);
    199             mStartTag = startTag;
    200         }
    201 
    202         public int getStatus() {
    203             return mStatus;
    204         }
    205 
    206         /**
    207          * The only useful info in the SendMail response is the status; we capture and save it
    208          */
    209         @Override
    210         public boolean parse() throws IOException {
    211             if (nextTag(START_DOCUMENT) != mStartTag) {
    212                 throw new IOException();
    213             }
    214             while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
    215                 if (tag == Tags.COMPOSE_STATUS) {
    216                     mStatus = getValueInt();
    217                 } else {
    218                     skipTag();
    219                 }
    220             }
    221             return true;
    222         }
    223     }
    224 
    225     /**
    226      * For OriginalMessageInfo, we use the terminology of EAS for the serverId and mailboxId of the
    227      * original message
    228      */
    229     protected static class OriginalMessageInfo {
    230         final String mItemId;
    231         final String mCollectionId;
    232         final String mLongId;
    233 
    234         OriginalMessageInfo(String itemId, String collectionId, String longId) {
    235             mItemId = itemId;
    236             mCollectionId = collectionId;
    237             mLongId = longId;
    238         }
    239     }
    240 
    241     private void sendCallback(long msgId, String subject, int status) {
    242         try {
    243             ExchangeService.callback().sendMessageStatus(msgId, subject, status, 0);
    244         } catch (RemoteException e) {
    245             // It's all good
    246         }
    247     }
    248 
    249     /*package*/ String generateSmartSendCmd(boolean reply, OriginalMessageInfo info) {
    250         StringBuilder sb = new StringBuilder();
    251         sb.append(reply ? "SmartReply" : "SmartForward");
    252         if (!TextUtils.isEmpty(info.mLongId)) {
    253             sb.append("&LongId=");
    254             sb.append(Uri.encode(info.mLongId, ":"));
    255         } else {
    256             sb.append("&ItemId=");
    257             sb.append(Uri.encode(info.mItemId, ":"));
    258             sb.append("&CollectionId=");
    259             sb.append(Uri.encode(info.mCollectionId, ":"));
    260         }
    261         return sb.toString();
    262     }
    263 
    264     /**
    265      * Get information about the original message that is referenced by the message to be sent; this
    266      * information will exist for replies and forwards
    267      *
    268      * @param context the caller's context
    269      * @param msgId the id of the message we're sending
    270      * @return a data structure with the serverId and mailboxId of the original message, or null if
    271      * either or both of those pieces of information can't be found
    272      */
    273     private static OriginalMessageInfo getOriginalMessageInfo(Context context, long msgId) {
    274         // Note: itemId and collectionId are the terms used by EAS to refer to the serverId and
    275         // mailboxId of a Message
    276         String itemId = null;
    277         String collectionId = null;
    278         String longId = null;
    279 
    280         // First, we need to get the id of the reply/forward message
    281         String[] cols = Utility.getRowColumns(context, Body.CONTENT_URI,
    282                 BODY_SOURCE_PROJECTION, WHERE_MESSAGE_KEY,
    283                 new String[] {Long.toString(msgId)});
    284         if (cols != null) {
    285             long refId = Long.parseLong(cols[0]);
    286             // Then, we need the serverId and mailboxKey of the message
    287             cols = Utility.getRowColumns(context, Message.CONTENT_URI, refId,
    288                     SyncColumns.SERVER_ID, MessageColumns.MAILBOX_KEY,
    289                     MessageColumns.PROTOCOL_SEARCH_INFO);
    290             if (cols != null) {
    291                 itemId = cols[0];
    292                 long boxId = Long.parseLong(cols[1]);
    293                 // Then, we need the serverId of the mailbox
    294                 cols = Utility.getRowColumns(context, Mailbox.CONTENT_URI, boxId,
    295                         MailboxColumns.SERVER_ID);
    296                 if (cols != null) {
    297                     collectionId = cols[0];
    298                 }
    299             }
    300         }
    301         // We need either a longId or both itemId (serverId) and collectionId (mailboxId) to process
    302         // a smart reply or a smart forward
    303         if (longId != null || (itemId != null && collectionId != null)){
    304             return new OriginalMessageInfo(itemId, collectionId, longId);
    305         }
    306         return null;
    307     }
    308 
    309     private void sendFailed(long msgId, int result) {
    310         ContentValues cv = new ContentValues();
    311         cv.put(SyncColumns.SERVER_ID, SEND_FAILED);
    312         Message.update(mContext, Message.CONTENT_URI, msgId, cv);
    313         sendCallback(msgId, null, result);
    314     }
    315 
    316     /**
    317      * Send a single message via EAS
    318      * Note that we mark messages SEND_FAILED when there is a permanent failure, rather than an
    319      * IOException, which is handled by ExchangeService with retries, backoffs, etc.
    320      *
    321      * @param cacheDir the cache directory for this context
    322      * @param msgId the _id of the message to send
    323      * @throws IOException
    324      */
    325     int sendMessage(File cacheDir, long msgId) throws IOException, MessagingException {
    326         // We always return SUCCESS unless the sending error is account-specific (security or
    327         // authentication) rather than message-specific; returning anything else will terminate
    328         // the Outbox sync! Message-specific errors are marked in the messages themselves.
    329         int result = EmailServiceStatus.SUCCESS;
    330         // Say we're starting to send this message
    331         sendCallback(msgId, null, EmailServiceStatus.IN_PROGRESS);
    332         // Create a temporary file (this will hold the outgoing message in RFC822 (MIME) format)
    333         File tmpFile = File.createTempFile("eas_", "tmp", cacheDir);
    334         try {
    335             // Get the message and fail quickly if not found
    336             Message msg = Message.restoreMessageWithId(mContext, msgId);
    337             if (msg == null) return EmailServiceStatus.MESSAGE_NOT_FOUND;
    338 
    339             // See what kind of outgoing messge this is
    340             int flags = msg.mFlags;
    341             boolean reply = (flags & Message.FLAG_TYPE_REPLY) != 0;
    342             boolean forward = (flags & Message.FLAG_TYPE_FORWARD) != 0;
    343             boolean includeQuotedText = (flags & Message.FLAG_NOT_INCLUDE_QUOTED_TEXT) == 0;
    344 
    345             // The reference message and mailbox are called item and collection in EAS
    346             OriginalMessageInfo referenceInfo = null;
    347             // Respect the sense of the include quoted text flag
    348             if (includeQuotedText && (reply || forward)) {
    349                 referenceInfo = getOriginalMessageInfo(mContext, msgId);
    350             }
    351             // Generally, we use SmartReply/SmartForward if we've got a good reference
    352             boolean smartSend = referenceInfo != null;
    353             // But we won't use SmartForward if the account isn't set up for it (currently, we only
    354             // use SmartForward for EAS 12.0 or later to avoid creating eml files that are
    355             // potentially difficult for the recipient to handle)
    356             if (forward && ((mAccount.mFlags & Account.FLAGS_SUPPORTS_SMART_FORWARD) == 0)) {
    357                 smartSend = false;
    358             }
    359 
    360             // Write the message to the temporary file
    361             FileOutputStream fileOutputStream = new FileOutputStream(tmpFile);
    362             Rfc822Output.writeTo(mContext, msgId, fileOutputStream, smartSend, true);
    363             fileOutputStream.close();
    364 
    365             // Sending via EAS14 is a whole 'nother kettle of fish
    366             boolean isEas14 = (Double.parseDouble(mAccount.mProtocolVersion) >=
    367                 Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE);
    368 
    369             while (true) {
    370                 // Get an input stream to our temporary file and create an entity with it
    371                 FileInputStream fileStream = new FileInputStream(tmpFile);
    372                 long fileLength = tmpFile.length();
    373 
    374                 // The type of entity depends on whether we're using EAS 14
    375                 HttpEntity inputEntity;
    376                 // For EAS 14, we need to save the wbxml tag we're using
    377                 int modeTag = 0;
    378                 if (isEas14) {
    379                     int mode =
    380                         !smartSend ? MODE_NORMAL : reply ? MODE_SMART_REPLY : MODE_SMART_FORWARD;
    381                     modeTag = SendMailEntity.MODE_TAGS[mode];
    382                     inputEntity =
    383                         new SendMailEntity(mContext, fileStream, fileLength, modeTag, msg);
    384                 } else {
    385                     inputEntity = new InputStreamEntity(fileStream, fileLength);
    386                 }
    387                 // Create the appropriate command and POST it to the server
    388                 String cmd = "SendMail";
    389                 if (smartSend) {
    390                     // In EAS 14, we don't send itemId and collectionId in the command
    391                     if (isEas14) {
    392                         cmd = reply ? "SmartReply" : "SmartForward";
    393                     } else {
    394                         cmd = generateSmartSendCmd(reply, referenceInfo);
    395                     }
    396                 }
    397 
    398                 // If we're not EAS 14, add our save-in-sent setting here
    399                 if (!isEas14) {
    400                     cmd += "&SaveInSent=T";
    401                 }
    402                 userLog("Send cmd: " + cmd);
    403 
    404                 // Finally, post SendMail to the server
    405                 EasResponse resp = sendHttpClientPost(cmd, inputEntity, SEND_MAIL_TIMEOUT);
    406                 try {
    407                     fileStream.close();
    408                     int code = resp.getStatus();
    409                     if (code == HttpStatus.SC_OK) {
    410                         // HTTP OK before EAS 14 is a thumbs up; in EAS 14, we've got to parse
    411                         // the reply
    412                         if (isEas14) {
    413                             try {
    414                                 // Try to parse the result
    415                                 SendMailParser p =
    416                                     new SendMailParser(resp.getInputStream(), modeTag);
    417                                 // If we get here, the SendMail failed; go figure
    418                                 p.parse();
    419                                 // The parser holds the status
    420                                 int status = p.getStatus();
    421                                 userLog("SendMail error, status: " + status);
    422                                 if (CommandStatus.isNeedsProvisioning(status)) {
    423                                     result = EmailServiceStatus.SECURITY_FAILURE;
    424                                 } else if (status == CommandStatus.ITEM_NOT_FOUND && smartSend) {
    425                                     // This is the retry case for EAS 14; we'll send without "smart"
    426                                     // commands next time
    427                                     resp.close();
    428                                     smartSend = false;
    429                                     continue;
    430                                 }
    431                                 sendFailed(msgId, result);
    432                                 return result;
    433                             } catch (EmptyStreamException e) {
    434                                 // This is actually fine; an empty stream means SendMail succeeded
    435                             }
    436                         }
    437 
    438                         // If we're here, the SendMail command succeeded
    439                         userLog("Deleting message...");
    440                         // Delete the message from the Outbox and send callback
    441                         mContentResolver.delete(
    442                                 ContentUris.withAppendedId(Message.CONTENT_URI, msgId), null, null);
    443                         sendCallback(-1, msg.mSubject, EmailServiceStatus.SUCCESS);
    444                         break;
    445                     } else if (code == EasSyncService.INTERNAL_SERVER_ERROR_CODE && smartSend) {
    446                         // This is the retry case for EAS 12.1 and below; we'll send without "smart"
    447                         // commands next time
    448                         resp.close();
    449                         smartSend = false;
    450                     } else {
    451                         userLog("Message sending failed, code: " + code);
    452                         if (EasResponse.isAuthError(code)) {
    453                             result = EmailServiceStatus.LOGIN_FAILED;
    454                         } else if (EasResponse.isProvisionError(code)) {
    455                             result = EmailServiceStatus.SECURITY_FAILURE;
    456                         }
    457                         sendFailed(msgId, result);
    458                         break;
    459                     }
    460                 } finally {
    461                     resp.close();
    462                 }
    463             }
    464         } catch (IOException e) {
    465             // We catch this just to send the callback
    466             sendCallback(msgId, null, EmailServiceStatus.CONNECTION_ERROR);
    467             throw e;
    468         } finally {
    469             // Clean up the temporary file
    470             if (tmpFile.exists()) {
    471                 tmpFile.delete();
    472             }
    473         }
    474         return result;
    475     }
    476 
    477     @Override
    478     public void run() {
    479         setupService();
    480         // Use SMTP flags for sending mail
    481         TrafficStats.setThreadStatsTag(TrafficFlags.getSmtpFlags(mContext, mAccount));
    482         File cacheDir = mContext.getCacheDir();
    483         try {
    484             mDeviceId = ExchangeService.getDeviceId(mContext);
    485             // Get a cursor to Outbox messages
    486             Cursor c = mContext.getContentResolver().query(Message.CONTENT_URI,
    487                     Message.ID_COLUMN_PROJECTION, MAILBOX_KEY_AND_NOT_SEND_FAILED,
    488                     new String[] {Long.toString(mMailbox.mId)}, null);
    489             try {
    490                 // Loop through the messages, sending each one
    491                 while (c.moveToNext()) {
    492                     long msgId = c.getLong(Message.ID_COLUMNS_ID_COLUMN);
    493                     if (msgId != 0) {
    494                         if (Utility.hasUnloadedAttachments(mContext, msgId)) {
    495                             // We'll just have to wait on this...
    496                             continue;
    497                         }
    498                         int result = sendMessage(cacheDir, msgId);
    499                         // If there's an error, it should stop the service; we will distinguish
    500                         // at least between login failures and everything else
    501                         if (result == EmailServiceStatus.LOGIN_FAILED) {
    502                             mExitStatus = EXIT_LOGIN_FAILURE;
    503                             return;
    504                         } else if (result == EmailServiceStatus.SECURITY_FAILURE) {
    505                             mExitStatus = EXIT_SECURITY_FAILURE;
    506                             return;
    507                         } else if (result == EmailServiceStatus.REMOTE_EXCEPTION) {
    508                             mExitStatus = EXIT_EXCEPTION;
    509                             return;
    510                         }
    511                     }
    512                 }
    513             } finally {
    514                 c.close();
    515             }
    516             mExitStatus = EXIT_DONE;
    517         } catch (IOException e) {
    518             mExitStatus = EXIT_IO_ERROR;
    519         } catch (Exception e) {
    520             userLog("Exception caught in EasOutboxService", e);
    521             mExitStatus = EXIT_EXCEPTION;
    522         } finally {
    523             userLog(mMailbox.mDisplayName, ": sync finished");
    524             userLog("Outbox exited with status ", mExitStatus);
    525             ExchangeService.done(this);
    526         }
    527     }
    528 
    529     /**
    530      * Convenience method for adding a Message to an account's outbox
    531      * @param context the context of the caller
    532      * @param accountId the accountId for the sending account
    533      * @param msg the message to send
    534      */
    535     public static void sendMessage(Context context, long accountId, Message msg) {
    536         Mailbox mailbox = Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_OUTBOX);
    537         if (mailbox != null) {
    538             msg.mMailboxKey = mailbox.mId;
    539             msg.mAccountKey = accountId;
    540             msg.save(context);
    541         }
    542     }
    543 }