Home | History | Annotate | Download | only in service
      1 package com.android.exchange.service;
      2 
      3 import android.content.ContentUris;
      4 import android.content.Context;
      5 import android.database.Cursor;
      6 import android.net.TrafficStats;
      7 import android.net.Uri;
      8 import android.text.format.DateUtils;
      9 import android.util.Log;
     10 
     11 import com.android.emailcommon.TrafficFlags;
     12 import com.android.emailcommon.internet.Rfc822Output;
     13 import com.android.emailcommon.provider.Account;
     14 import com.android.emailcommon.provider.EmailContent.Attachment;
     15 import com.android.emailcommon.provider.EmailContent.Body;
     16 import com.android.emailcommon.provider.EmailContent.BodyColumns;
     17 import com.android.emailcommon.provider.EmailContent.MailboxColumns;
     18 import com.android.emailcommon.provider.EmailContent.Message;
     19 import com.android.emailcommon.provider.EmailContent.MessageColumns;
     20 import com.android.emailcommon.provider.EmailContent.SyncColumns;
     21 import com.android.emailcommon.provider.Mailbox;
     22 import com.android.emailcommon.utility.Utility;
     23 import com.android.exchange.CommandStatusException.CommandStatus;
     24 import com.android.exchange.Eas;
     25 import com.android.exchange.EasResponse;
     26 import com.android.exchange.adapter.Parser;
     27 import com.android.exchange.adapter.Parser.EmptyStreamException;
     28 import com.android.exchange.adapter.Serializer;
     29 import com.android.exchange.adapter.Tags;
     30 import com.android.mail.utils.LogUtils;
     31 
     32 import org.apache.http.HttpEntity;
     33 import org.apache.http.HttpStatus;
     34 import org.apache.http.entity.InputStreamEntity;
     35 
     36 import java.io.ByteArrayOutputStream;
     37 import java.io.File;
     38 import java.io.FileInputStream;
     39 import java.io.FileNotFoundException;
     40 import java.io.FileOutputStream;
     41 import java.io.IOException;
     42 import java.io.InputStream;
     43 import java.io.OutputStream;
     44 import java.security.cert.CertificateException;
     45 import java.util.ArrayList;
     46 
     47 /**
     48  * Performs an Exchange Outbox sync, i.e. sends all mail from the Outbox.
     49  */
     50 public class EasOutboxSyncHandler extends EasServerConnection {
     51     // Value for a message's server id when sending fails.
     52     public static final int SEND_FAILED = 1;
     53 
     54     // WHERE clause to query for unsent messages.
     55     // TODO: Is the SEND_FAILED check actually what we want?
     56     public static final String MAILBOX_KEY_AND_NOT_SEND_FAILED =
     57             MessageColumns.MAILBOX_KEY + "=? and (" + SyncColumns.SERVER_ID + " is null or " +
     58             SyncColumns.SERVER_ID + "!=" + SEND_FAILED + ')';
     59 
     60     // This needs to be long enough to send the longest reasonable message, without being so long
     61     // as to effectively "hang" sending of mail.  The standard 30 second timeout isn't long enough
     62     // for pictures and the like.  For now, we'll use 15 minutes, in the knowledge that any socket
     63     // failure would probably generate an Exception before timing out anyway
     64     public static final long SEND_MAIL_TIMEOUT = 15 * DateUtils.MINUTE_IN_MILLIS;
     65 
     66     private final Mailbox mMailbox;
     67     private final File mCacheDir;
     68 
     69     public EasOutboxSyncHandler(final Context context, final Account account,
     70             final Mailbox mailbox) {
     71         super(context, account);
     72         mMailbox = mailbox;
     73         mCacheDir = context.getCacheDir();
     74     }
     75 
     76     public void performSync() {
     77         // Use SMTP flags for sending mail
     78         TrafficStats.setThreadStatsTag(TrafficFlags.getSmtpFlags(mContext, mAccount));
     79         // Get a cursor to Outbox messages
     80         final Cursor c = mContext.getContentResolver().query(Message.CONTENT_URI,
     81                 Message.CONTENT_PROJECTION, MAILBOX_KEY_AND_NOT_SEND_FAILED,
     82                 new String[] {Long.toString(mMailbox.mId)}, null);
     83         try {
     84             // Loop through the messages, sending each one
     85             while (c.moveToNext()) {
     86                 final Message message = new Message();
     87                 message.restore(c);
     88                 if (Utility.hasUnloadedAttachments(mContext, message.mId)) {
     89                     // We'll just have to wait on this...
     90                     continue;
     91                 }
     92 
     93                 // TODO: Fix -- how do we want to signal to UI that we started syncing?
     94                 // Note the entire callback mechanism here needs improving.
     95                 //sendMessageStatus(message.mId, null, EmailServiceStatus.IN_PROGRESS, 0);
     96 
     97                 if (!sendOneMessage(message,
     98                         SmartSendInfo.getSmartSendInfo(mContext, mAccount, message))) {
     99                     break;
    100                 }
    101             }
    102         } finally {
    103             // TODO: Some sort of sendMessageStatus() is needed here.
    104             c.close();
    105         }
    106     }
    107 
    108     /**
    109      * Information needed for SmartReply/SmartForward.
    110      */
    111     private static class SmartSendInfo {
    112         public static final String[] BODY_SOURCE_PROJECTION =
    113                 new String[] {BodyColumns.SOURCE_MESSAGE_KEY};
    114         public static final String WHERE_MESSAGE_KEY = Body.MESSAGE_KEY + "=?";
    115 
    116         final String mItemId;
    117         final String mCollectionId;
    118         final boolean mIsReply;
    119         final ArrayList<Attachment> mRequiredAtts;
    120 
    121         private SmartSendInfo(final String itemId, final String collectionId, final boolean isReply,
    122                 final ArrayList<Attachment> requiredAtts) {
    123             mItemId = itemId;
    124             mCollectionId = collectionId;
    125             mIsReply = isReply;
    126             mRequiredAtts = requiredAtts;
    127         }
    128 
    129         public String generateSmartSendCmd() {
    130             final StringBuilder sb = new StringBuilder();
    131             sb.append(isForward() ? "SmartForward" : "SmartReply");
    132             sb.append("&ItemId=");
    133             sb.append(Uri.encode(mItemId, ":"));
    134             sb.append("&CollectionId=");
    135             sb.append(Uri.encode(mCollectionId, ":"));
    136             return sb.toString();
    137         }
    138 
    139         public boolean isForward() {
    140             return !mIsReply;
    141         }
    142 
    143         /**
    144          * See if a given attachment is among an array of attachments; it is if the locations of
    145          * both are the same (we're looking to see if they represent the same attachment on the
    146          * server. Note that an attachment that isn't on the server (e.g. an outbound attachment
    147          * picked from the  gallery) won't have a location, so the result will always be false.
    148          *
    149          * @param att the attachment to test
    150          * @param atts the array of attachments to look in
    151          * @return whether the test attachment is among the array of attachments
    152          */
    153         private static boolean amongAttachments(final Attachment att, final Attachment[] atts) {
    154             final String location = att.mLocation;
    155             if (location == null) return false;
    156             for (final Attachment a: atts) {
    157                 if (location.equals(a.mLocation)) {
    158                     return true;
    159                 }
    160             }
    161             return false;
    162         }
    163 
    164         /**
    165          * If this message should use SmartReply or SmartForward, return an object with the data
    166          * for the smart send.
    167          *
    168          * @param context the caller's context
    169          * @param account the Account we're sending from
    170          * @param message the Message being sent
    171          * @return an object to support smart sending, or null if not applicable.
    172          */
    173         public static SmartSendInfo getSmartSendInfo(final Context context,
    174                 final Account account, final Message message) {
    175             final int flags = message.mFlags;
    176             // We only care about the original message if we include quoted text.
    177             if ((flags & Message.FLAG_NOT_INCLUDE_QUOTED_TEXT) != 0) {
    178                 return null;
    179             }
    180             final boolean reply = (flags & Message.FLAG_TYPE_REPLY) != 0;
    181             final boolean forward = (flags & Message.FLAG_TYPE_FORWARD) != 0;
    182             // We also only care for replies or forwards.
    183             if (!reply && !forward) {
    184                 return null;
    185             }
    186             // Just a sanity check here, since we assume that reply and forward are mutually
    187             // exclusive throughout this class.
    188             if (reply && forward) {
    189                 return null;
    190             }
    191             // If we don't support SmartForward and it's a forward, then don't proceed.
    192             if (forward && (account.mFlags & Account.FLAGS_SUPPORTS_SMART_FORWARD) == 0) {
    193                 return null;
    194             }
    195 
    196             // Note: itemId and collectionId are the terms used by EAS to refer to the serverId and
    197             // mailboxId of a Message
    198             String itemId = null;
    199             String collectionId = null;
    200 
    201             // First, we need to get the id of the reply/forward message
    202             String[] cols = Utility.getRowColumns(context, Body.CONTENT_URI, BODY_SOURCE_PROJECTION,
    203                     WHERE_MESSAGE_KEY, new String[] {Long.toString(message.mId)});
    204             long refId = 0;
    205             // TODO: We can probably just write a smarter query to do this all at once.
    206             if (cols != null && cols[0] != null) {
    207                 refId = Long.parseLong(cols[0]);
    208                 // Then, we need the serverId and mailboxKey of the message
    209                 cols = Utility.getRowColumns(context, Message.CONTENT_URI, refId,
    210                         SyncColumns.SERVER_ID, MessageColumns.MAILBOX_KEY,
    211                         MessageColumns.PROTOCOL_SEARCH_INFO);
    212                 if (cols != null) {
    213                     itemId = cols[0];
    214                     final long boxId = Long.parseLong(cols[1]);
    215                     // Then, we need the serverId of the mailbox
    216                     cols = Utility.getRowColumns(context, Mailbox.CONTENT_URI, boxId,
    217                             MailboxColumns.SERVER_ID);
    218                     if (cols != null) {
    219                         collectionId = cols[0];
    220                     }
    221                 }
    222             }
    223             // We need either a longId or both itemId (serverId) and collectionId (mailboxId) to
    224             // process a smart reply or a smart forward
    225             if (itemId != null && collectionId != null) {
    226                 final ArrayList<Attachment> requiredAtts;
    227                 if (forward) {
    228                     // See if we can really smart forward (all reference attachments must be sent)
    229                     final Attachment[] outAtts =
    230                             Attachment.restoreAttachmentsWithMessageId(context, message.mId);
    231                     final Attachment[] refAtts =
    232                             Attachment.restoreAttachmentsWithMessageId(context, refId);
    233                     for (final Attachment refAtt: refAtts) {
    234                         // If an original attachment isn't among what's going out, we can't be smart
    235                         if (!amongAttachments(refAtt, outAtts)) {
    236                             return null;
    237                         }
    238                     }
    239                     requiredAtts = new ArrayList<Attachment>();
    240                     for (final Attachment outAtt: outAtts) {
    241                         // If an outgoing attachment isn't in original message, we must send it
    242                         if (!amongAttachments(outAtt, refAtts)) {
    243                             requiredAtts.add(outAtt);
    244                         }
    245                     }
    246                 } else {
    247                     requiredAtts = null;
    248                 }
    249                 return new SmartSendInfo(itemId, collectionId, reply, requiredAtts);
    250             }
    251             return null;
    252         }
    253     }
    254 
    255     /**
    256      * Our own HttpEntity subclass that is able to insert opaque data (in this case the MIME
    257      * representation of the message body as stored in a temporary file) into the serializer stream
    258      */
    259     private static class SendMailEntity extends InputStreamEntity {
    260         private final FileInputStream mFileStream;
    261         private final long mFileLength;
    262         private final int mSendTag;
    263         private final Message mMessage;
    264         private final SmartSendInfo mSmartSendInfo;
    265 
    266         public SendMailEntity(final FileInputStream instream, final long length, final int tag,
    267                 final Message message, final SmartSendInfo smartSendInfo) {
    268             super(instream, length);
    269             mFileStream = instream;
    270             mFileLength = length;
    271             mSendTag = tag;
    272             mMessage = message;
    273             mSmartSendInfo = smartSendInfo;
    274         }
    275 
    276         /**
    277          * We always return -1 because we don't know the actual length of the POST data (this
    278          * causes HttpClient to send the data in "chunked" mode)
    279          */
    280         @Override
    281         public long getContentLength() {
    282             final ByteArrayOutputStream baos = new ByteArrayOutputStream();
    283             try {
    284                 // Calculate the overhead for the WBXML data
    285                 writeTo(baos, false);
    286                 // Return the actual size that will be sent
    287                 return baos.size() + mFileLength;
    288             } catch (final IOException e) {
    289                 // Just return -1 (unknown)
    290             } finally {
    291                 try {
    292                     baos.close();
    293                 } catch (final IOException e) {
    294                     // Ignore
    295                 }
    296             }
    297             return -1;
    298         }
    299 
    300         @Override
    301         public void writeTo(final OutputStream outstream) throws IOException {
    302             writeTo(outstream, true);
    303         }
    304 
    305         /**
    306          * Write the message to the output stream
    307          * @param outstream the output stream to write
    308          * @param withData whether or not the actual data is to be written; true when sending
    309          *   mail; false when calculating size only
    310          * @throws IOException
    311          */
    312         public void writeTo(final OutputStream outstream, final boolean withData)
    313                 throws IOException {
    314             // Not sure if this is possible; the check is taken from the superclass
    315             if (outstream == null) {
    316                 throw new IllegalArgumentException("Output stream may not be null");
    317             }
    318 
    319             // We'll serialize directly into the output stream
    320             final Serializer s = new Serializer(outstream);
    321             // Send the appropriate initial tag
    322             s.start(mSendTag);
    323             // The Message-Id for this message (note that we cannot use the messageId stored in
    324             // the message, as EAS 14 limits the length to 40 chars and we use 70+)
    325             s.data(Tags.COMPOSE_CLIENT_ID, "SendMail-" + System.nanoTime());
    326             // We always save sent mail
    327             s.tag(Tags.COMPOSE_SAVE_IN_SENT_ITEMS);
    328 
    329             // If we're using smart reply/forward, we need info about the original message
    330             if (mSendTag != Tags.COMPOSE_SEND_MAIL) {
    331                 if (mSmartSendInfo != null) {
    332                     s.start(Tags.COMPOSE_SOURCE);
    333                     // For search results, use the long id (stored in mProtocolSearchInfo); else,
    334                     // use folder id/item id combo
    335                     if (mMessage.mProtocolSearchInfo != null) {
    336                         s.data(Tags.COMPOSE_LONG_ID, mMessage.mProtocolSearchInfo);
    337                     } else {
    338                         s.data(Tags.COMPOSE_ITEM_ID, mSmartSendInfo.mItemId);
    339                         s.data(Tags.COMPOSE_FOLDER_ID, mSmartSendInfo.mCollectionId);
    340                     }
    341                     s.end();  // Tags.COMPOSE_SOURCE
    342                 }
    343             }
    344 
    345             // Start the MIME tag; this is followed by "opaque" data (byte array)
    346             s.start(Tags.COMPOSE_MIME);
    347             // Send opaque data from the file stream
    348             if (withData) {
    349                 s.opaque(mFileStream, (int)mFileLength);
    350             } else {
    351                 s.opaqueWithoutData((int)mFileLength);
    352             }
    353             // And we're done
    354             s.end().end().done();
    355         }
    356     }
    357 
    358     private static class SendMailParser extends Parser {
    359         private final int mStartTag;
    360         private int mStatus;
    361 
    362         public SendMailParser(final InputStream in, final int startTag) throws IOException {
    363             super(in);
    364             mStartTag = startTag;
    365         }
    366 
    367         public int getStatus() {
    368             return mStatus;
    369         }
    370 
    371         /**
    372          * The only useful info in the SendMail response is the status; we capture and save it
    373          */
    374         @Override
    375         public boolean parse() throws IOException {
    376             if (nextTag(START_DOCUMENT) != mStartTag) {
    377                 throw new IOException();
    378             }
    379             while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
    380                 if (tag == Tags.COMPOSE_STATUS) {
    381                     mStatus = getValueInt();
    382                 } else {
    383                     skipTag();
    384                 }
    385             }
    386             return true;
    387         }
    388     }
    389 
    390     /**
    391      * Attempt to send one message.
    392      * @param message The message to send.
    393      * @param smartSendInfo The SmartSendInfo for this message, or null if we don't have or don't
    394      *      want to use smart send.
    395      * @return Whether or not sending this message succeeded.
    396      * TODO: Improve how we handle the types of failures. I've left the old error codes in as TODOs
    397      * for future reference.
    398      */
    399     private boolean sendOneMessage(final Message message, final SmartSendInfo smartSendInfo) {
    400         final File tmpFile;
    401         try {
    402             tmpFile = File.createTempFile("eas_", "tmp", mCacheDir);
    403         } catch (final IOException e) {
    404             return false; // TODO: Handle SyncStatus.FAILURE_IO;
    405         }
    406 
    407         final EasResponse resp;
    408         // Send behavior differs pre and post EAS14.
    409         final boolean isEas14 = (Double.parseDouble(mAccount.mProtocolVersion) >=
    410                 Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE);
    411         final int modeTag = getModeTag(isEas14, smartSendInfo);
    412         try {
    413             if (!writeMessageToTempFile(tmpFile, message, smartSendInfo)) {
    414                 return false; // TODO: Handle SyncStatus.FAILURE_IO;
    415             }
    416 
    417             final FileInputStream fileStream;
    418             try {
    419                 fileStream = new FileInputStream(tmpFile);
    420             } catch (final FileNotFoundException e) {
    421                 return false; // TODO: Handle SyncStatus.FAILURE_IO;
    422             }
    423             try {
    424 
    425                 final long fileLength = tmpFile.length();
    426                 final HttpEntity entity;
    427                 if (isEas14) {
    428                     entity = new SendMailEntity(fileStream, fileLength, modeTag, message,
    429                             smartSendInfo);
    430                 } else {
    431                     entity = new InputStreamEntity(fileStream, fileLength);
    432                 }
    433 
    434                 // Create the appropriate command.
    435                 String cmd = "SendMail";
    436                 if (smartSendInfo != null) {
    437                     // In EAS 14, we don't send itemId and collectionId in the command
    438                     if (isEas14) {
    439                         cmd = smartSendInfo.isForward() ? "SmartForward" : "SmartReply";
    440                     } else {
    441                         cmd = smartSendInfo.generateSmartSendCmd();
    442                     }
    443                 }
    444                 // If we're not EAS 14, add our save-in-sent setting here
    445                 if (!isEas14) {
    446                     cmd += "&SaveInSent=T";
    447                 }
    448                 // Finally, post SendMail to the server
    449                 try {
    450                     resp = sendHttpClientPost(cmd, entity, SEND_MAIL_TIMEOUT);
    451                 } catch (final IOException e) {
    452                     return false; // TODO: Handle SyncStatus.FAILURE_IO;
    453                 } catch (final CertificateException e) {
    454                     return false;
    455                 }
    456 
    457             } finally {
    458                 try {
    459                     fileStream.close();
    460                 } catch (final IOException e) {
    461                     // TODO: Should we do anything here, or is it ok to just proceed?
    462                 }
    463             }
    464         } finally {
    465             if (tmpFile.exists()) {
    466                 tmpFile.delete();
    467             }
    468         }
    469 
    470         try {
    471             final int code = resp.getStatus();
    472             if (code == HttpStatus.SC_OK) {
    473                 // HTTP OK before EAS 14 is a thumbs up; in EAS 14, we've got to parse
    474                 // the reply
    475                 if (isEas14) {
    476                     try {
    477                         // Try to parse the result
    478                         final SendMailParser p = new SendMailParser(resp.getInputStream(), modeTag);
    479                         // If we get here, the SendMail failed; go figure
    480                         p.parse();
    481                         // The parser holds the status
    482                         final int status = p.getStatus();
    483                         if (CommandStatus.isNeedsProvisioning(status)) {
    484                             return false; // TODO: Handle SyncStatus.FAILURE_SECURITY;
    485                         } else if (status == CommandStatus.ITEM_NOT_FOUND &&
    486                                 smartSendInfo != null) {
    487                             // Let's retry without "smart" commands.
    488                             return sendOneMessage(message, null);
    489                         }
    490                         // TODO: Set syncServerId = SEND_FAILED in DB?
    491                         return false; // TODO: Handle SyncStatus.FAILURE_MESSAGE;
    492                     } catch (final EmptyStreamException e) {
    493                         // This is actually fine; an empty stream means SendMail succeeded
    494                     } catch (final IOException e) {
    495                         // Parsing failed in some other way.
    496                         return false; // TODO: Handle SyncStatus.FAILURE_IO;
    497                     }
    498                 }
    499             } else if (code == HttpStatus.SC_INTERNAL_SERVER_ERROR && smartSendInfo != null) {
    500                 // Let's retry without "smart" commands.
    501                 return sendOneMessage(message, null);
    502             } else {
    503                 if (resp.isAuthError()) {
    504                     LogUtils.d(LogUtils.TAG, "Got auth error from server during outbox sync");
    505                     return false; // TODO: Handle SyncStatus.FAILURE_LOGIN;
    506                 } else if (resp.isProvisionError()) {
    507                     LogUtils.d(LogUtils.TAG, "Got provision error from server during outbox sync.");
    508                     return false; // TODO: Handle SyncStatus.FAILURE_SECURITY;
    509                 } else {
    510                     // TODO: Handle some other error
    511                     LogUtils.d(LogUtils.TAG,
    512                             "Got other HTTP error from server during outbox sync: %d", code);
    513                     return false;
    514                 }
    515             }
    516         } finally {
    517             resp.close();
    518         }
    519 
    520         // If we manage to get here, the message sent successfully. Hooray!
    521         // Delete the sent message.
    522         mContext.getContentResolver().delete(
    523                 ContentUris.withAppendedId(Message.CONTENT_URI, message.mId), null, null);
    524         return true;
    525     }
    526 
    527     /**
    528      * Writes message to the temp file.
    529      * @param tmpFile The temp file to use.
    530      * @param message The {@link Message} to write.
    531      * @param smartSendInfo The {@link SmartSendInfo} for this message send attempt.
    532      * @return Whether we could successfully write the file.
    533      */
    534     private boolean writeMessageToTempFile(final File tmpFile, final Message message,
    535             final SmartSendInfo smartSendInfo) {
    536         final FileOutputStream fileStream;
    537         try {
    538             fileStream = new FileOutputStream(tmpFile);
    539         } catch (final FileNotFoundException e) {
    540             Log.e(LogUtils.TAG, "Failed to create message file", e);
    541             return false;
    542         }
    543         try {
    544             final boolean smartSend = smartSendInfo != null;
    545             final ArrayList<Attachment> attachments =
    546                     smartSend ? smartSendInfo.mRequiredAtts : null;
    547             Rfc822Output.writeTo(mContext, message, fileStream, smartSend, true, attachments);
    548         } catch (final Exception e) {
    549             Log.e(LogUtils.TAG, "Failed to write message file", e);
    550             return false;
    551         } finally {
    552             try {
    553                 fileStream.close();
    554             } catch (final IOException e) {
    555                 // should not happen
    556                 Log.e(LogUtils.TAG, "Failed to close file - should not happen", e);
    557             }
    558         }
    559         return true;
    560     }
    561 
    562     private static int getModeTag(final boolean isEas14, final SmartSendInfo smartSendInfo) {
    563         if (isEas14) {
    564             if (smartSendInfo == null) {
    565                 return Tags.COMPOSE_SEND_MAIL;
    566             } else if (smartSendInfo.isForward()) {
    567                 return Tags.COMPOSE_SMART_FORWARD;
    568             } else {
    569                 return Tags.COMPOSE_SMART_REPLY;
    570             }
    571         }
    572         return 0;
    573     }
    574 }
    575