Home | History | Annotate | Download | only in eas
      1 package com.android.exchange.eas;
      2 
      3 import android.content.ContentUris;
      4 import android.content.Context;
      5 import android.net.Uri;
      6 import android.provider.BaseColumns;
      7 import android.text.format.DateUtils;
      8 import android.util.Log;
      9 
     10 import com.android.emailcommon.internet.MimeUtility;
     11 import com.android.emailcommon.internet.Rfc822Output;
     12 import com.android.emailcommon.provider.Account;
     13 import com.android.emailcommon.provider.Mailbox;
     14 import com.android.emailcommon.provider.EmailContent.Attachment;
     15 import com.android.emailcommon.provider.EmailContent.Body;
     16 import com.android.emailcommon.provider.EmailContent.MailboxColumns;
     17 import com.android.emailcommon.provider.EmailContent.Message;
     18 import com.android.emailcommon.provider.EmailContent.MessageColumns;
     19 import com.android.emailcommon.provider.EmailContent.SyncColumns;
     20 import com.android.emailcommon.utility.Utility;
     21 import com.android.exchange.CommandStatusException;
     22 import com.android.exchange.Eas;
     23 import com.android.exchange.EasResponse;
     24 import com.android.exchange.CommandStatusException.CommandStatus;
     25 import com.android.exchange.adapter.SendMailParser;
     26 import com.android.exchange.adapter.Serializer;
     27 import com.android.exchange.adapter.Tags;
     28 import com.android.exchange.adapter.Parser.EmptyStreamException;
     29 import com.android.mail.utils.LogUtils;
     30 
     31 import org.apache.http.HttpEntity;
     32 import org.apache.http.HttpStatus;
     33 import org.apache.http.entity.InputStreamEntity;
     34 
     35 import java.io.ByteArrayOutputStream;
     36 import java.io.File;
     37 import java.io.FileInputStream;
     38 import java.io.FileNotFoundException;
     39 import java.io.FileOutputStream;
     40 import java.io.IOException;
     41 import java.io.OutputStream;
     42 import java.util.ArrayList;
     43 
     44 public class EasOutboxSync extends EasOperation {
     45 
     46     // Value for a message's server id when sending fails.
     47     public static final int SEND_FAILED = 1;
     48     // This needs to be long enough to send the longest reasonable message, without being so long
     49     // as to effectively "hang" sending of mail.  The standard 30 second timeout isn't long enough
     50     // for pictures and the like.  For now, we'll use 15 minutes, in the knowledge that any socket
     51     // failure would probably generate an Exception before timing out anyway
     52     public static final long SEND_MAIL_TIMEOUT = 15 * DateUtils.MINUTE_IN_MILLIS;
     53 
     54     public static final int RESULT_OK = 1;
     55     public static final int RESULT_IO_ERROR = -100;
     56     public static final int RESULT_ITEM_NOT_FOUND = -101;
     57     public static final int RESULT_SEND_FAILED = -102;
     58 
     59     private final Message mMessage;
     60     private boolean mIsEas14;
     61     private final File mCacheDir;
     62     private final SmartSendInfo mSmartSendInfo;
     63     private final int mModeTag;
     64     private File mTmpFile;
     65     private FileInputStream mFileStream;
     66 
     67     public EasOutboxSync(final Context context, final Account account, final Message message,
     68             final boolean useSmartSend) {
     69         super(context, account);
     70         mMessage = message;
     71         initEas14();
     72         mCacheDir = context.getCacheDir();
     73         if (useSmartSend) {
     74             mSmartSendInfo = SmartSendInfo.getSmartSendInfo(mContext, mAccount, mMessage);
     75         } else {
     76             mSmartSendInfo = null;
     77         }
     78         mModeTag = getModeTag(mSmartSendInfo);
     79     }
     80 
     81     /**
     82      * Have to override EasOperation::init because it reloads mAccount, so we
     83      * need to reset any derived values (eg, mIsEas14).
     84      */
     85     @Override
     86     public boolean init() {
     87         initEas14();
     88         return true;
     89     }
     90 
     91     private void initEas14() {
     92         mIsEas14 = Eas.isProtocolEas14(mAccount.mProtocolVersion);
     93     }
     94 
     95     @Override
     96     protected String getCommand() {
     97         String cmd = "SendMail";
     98         if (mSmartSendInfo != null) {
     99             // In EAS 14, we don't send itemId and collectionId in the command
    100             if (mIsEas14) {
    101                 cmd = mSmartSendInfo.isForward() ? "SmartForward" : "SmartReply";
    102             } else {
    103                 cmd = mSmartSendInfo.generateSmartSendCmd();
    104             }
    105         }
    106         // If we're not EAS 14, add our save-in-sent setting here
    107         if (!mIsEas14) {
    108             cmd += "&SaveInSent=T";
    109         }
    110         return cmd;
    111     }
    112 
    113     @Override
    114     protected HttpEntity getRequestEntity() throws IOException, MessageInvalidException {
    115         try {
    116             mTmpFile = File.createTempFile("eas_", "tmp", mCacheDir);
    117         } catch (final IOException e) {
    118             LogUtils.w(LOG_TAG, "IO error creating temp file");
    119             throw new IllegalStateException("Failure creating temp file");
    120         }
    121 
    122         if (!writeMessageToTempFile(mTmpFile, mMessage, mSmartSendInfo)) {
    123             // There are several reasons this could happen, possibly the message is corrupt (e.g.
    124             // the To header is null) or the disk is too full to handle the temporary message.
    125             // We can't send this message, but we don't want to abort the entire sync. Returning
    126             // this error code will let the caller recognize that this operation failed, but we
    127             // should continue on with the rest of the sync.
    128             LogUtils.w(LOG_TAG, "IO error writing to temp file");
    129             throw new MessageInvalidException("Failure writing to temp file");
    130         }
    131 
    132         try {
    133             mFileStream = new FileInputStream(mTmpFile);
    134         } catch (final FileNotFoundException e) {
    135             LogUtils.w(LOG_TAG, "IO error creating fileInputStream");
    136             throw new IllegalStateException("Failure creating fileInputStream");
    137         }
    138           final long fileLength = mTmpFile.length();
    139           final HttpEntity entity;
    140           if (mIsEas14) {
    141               entity = new SendMailEntity(mFileStream, fileLength, mModeTag, mMessage,
    142                       mSmartSendInfo);
    143           } else {
    144               entity = new InputStreamEntity(mFileStream, fileLength);
    145           }
    146 
    147           return entity;
    148     }
    149 
    150     @Override
    151     protected int handleHttpError(int httpStatus) {
    152         if (httpStatus == HttpStatus.SC_INTERNAL_SERVER_ERROR && mSmartSendInfo != null) {
    153             // Let's retry without "smart" commands.
    154             return RESULT_ITEM_NOT_FOUND;
    155         } else {
    156             return RESULT_OTHER_FAILURE;
    157         }
    158     }
    159 
    160     /**
    161      * This routine is called in a finally block in EasOperation.performOperation,
    162      * so the request may have failed part way through and there is no guarantee
    163      * what state we're in.
    164      */
    165     @Override
    166     protected void onRequestMade() {
    167         if (mFileStream != null) {
    168             try {
    169                 mFileStream.close();
    170             } catch (IOException e) {
    171                 LogUtils.w(LOG_TAG, "IOException closing fileStream %s", e);
    172             }
    173             mFileStream = null;
    174         }
    175         if (mTmpFile != null) {
    176             if (mTmpFile.exists()) {
    177                 mTmpFile.delete();
    178             }
    179             mTmpFile = null;
    180         }
    181     }
    182 
    183     @Override
    184     protected int handleResponse(EasResponse response) throws IOException, CommandStatusException {
    185         if (mIsEas14) {
    186             try {
    187                 // Try to parse the result
    188                 final SendMailParser p = new SendMailParser(response.getInputStream(), mModeTag);
    189                 // If we get here, the SendMail failed; go figure
    190                 p.parse();
    191                 // The parser holds the status
    192                 final int status = p.getStatus();
    193                 if (CommandStatus.isNeedsProvisioning(status)) {
    194                     LogUtils.w(LOG_TAG, "Needs provisioning before sending message: %d",
    195                             mMessage.mId);
    196                     return RESULT_PROVISIONING_ERROR;
    197                 } else if (status == CommandStatus.ITEM_NOT_FOUND && mSmartSendInfo != null) {
    198                     // Let's retry without "smart" commands.
    199                     LogUtils.w(LOG_TAG, "ITEM_NOT_FOUND smart sending message: %d", mMessage.mId);
    200                     return RESULT_ITEM_NOT_FOUND;
    201                 }
    202                 // TODO: Set syncServerId = SEND_FAILED in DB?
    203                 LogUtils.w(LOG_TAG, "General failure sending message: %d", mMessage.mId);
    204                 return RESULT_SEND_FAILED;
    205             } catch (final EmptyStreamException e) {
    206                 // This is actually fine; an empty stream means SendMail succeeded
    207                 LogUtils.d(LOG_TAG, "Empty response sending message: %d", mMessage.mId);
    208                 // Don't return here, fall through so that we'll delete the sent message.
    209             } catch (final IOException e) {
    210                 // Parsing failed in some other way.
    211                 LogUtils.e(LOG_TAG, e, "IOException sending message: %d", mMessage.mId);
    212                 return RESULT_IO_ERROR;
    213             }
    214         } else {
    215             // FLAG: Do we need to parse results for earlier versions?
    216         }
    217         LogUtils.d(LOG_TAG, "Returning RESULT_OK after sending: %d", mMessage.mId);
    218         mContext.getContentResolver().delete(
    219             ContentUris.withAppendedId(Message.CONTENT_URI, mMessage.mId), null, null);
    220         return RESULT_OK;
    221     }
    222 
    223     /**
    224      * Writes message to the temp file.
    225      * @param tmpFile The temp file to use.
    226      * @param message The {@link Message} to write.
    227      * @param smartSendInfo The {@link SmartSendInfo} for this message send attempt.
    228      * @return Whether we could successfully write the file.
    229      */
    230     private boolean writeMessageToTempFile(final File tmpFile, final Message message,
    231             final SmartSendInfo smartSendInfo) {
    232         final FileOutputStream fileStream;
    233         try {
    234             fileStream = new FileOutputStream(tmpFile);
    235             Log.d(LogUtils.TAG, "created outputstream");
    236         } catch (final FileNotFoundException e) {
    237             Log.e(LogUtils.TAG, "Failed to create message file", e);
    238             return false;
    239         }
    240         try {
    241             final boolean smartSend = smartSendInfo != null;
    242             final ArrayList<Attachment> attachments =
    243                     smartSend ? smartSendInfo.mRequiredAtts : null;
    244             Rfc822Output.writeTo(mContext, message, fileStream, smartSend, true, attachments);
    245         } catch (final Exception e) {
    246             Log.e(LogUtils.TAG, "Failed to write message file", e);
    247             return false;
    248         } finally {
    249             try {
    250                 fileStream.close();
    251             } catch (final IOException e) {
    252                 // should not happen
    253                 Log.e(LogUtils.TAG, "Failed to close file - should not happen", e);
    254             }
    255         }
    256         return true;
    257     }
    258 
    259     private int getModeTag(final SmartSendInfo smartSendInfo) {
    260         if (mIsEas14) {
    261             if (smartSendInfo == null) {
    262                 return Tags.COMPOSE_SEND_MAIL;
    263             } else if (smartSendInfo.isForward()) {
    264                 return Tags.COMPOSE_SMART_FORWARD;
    265             } else {
    266                 return Tags.COMPOSE_SMART_REPLY;
    267             }
    268         }
    269         return 0;
    270     }
    271 
    272     /**
    273      * Information needed for SmartReply/SmartForward.
    274      */
    275     private static class SmartSendInfo {
    276         final String mItemId;
    277         final String mCollectionId;
    278         final boolean mIsReply;
    279         final ArrayList<Attachment> mRequiredAtts;
    280 
    281         private SmartSendInfo(final String itemId, final String collectionId,
    282                 final boolean isReply,ArrayList<Attachment> requiredAtts) {
    283             mItemId = itemId;
    284             mCollectionId = collectionId;
    285             mIsReply = isReply;
    286             mRequiredAtts = requiredAtts;
    287         }
    288 
    289         public String generateSmartSendCmd() {
    290             final StringBuilder sb = new StringBuilder();
    291             sb.append(isForward() ? "SmartForward" : "SmartReply");
    292             sb.append("&ItemId=");
    293             sb.append(Uri.encode(mItemId, ":"));
    294             sb.append("&CollectionId=");
    295             sb.append(Uri.encode(mCollectionId, ":"));
    296             return sb.toString();
    297         }
    298 
    299         public boolean isForward() {
    300             return !mIsReply;
    301         }
    302 
    303         /**
    304          * See if a given attachment is among an array of attachments; it is if the locations of
    305          * both are the same (we're looking to see if they represent the same attachment on the
    306          * server. Note that an attachment that isn't on the server (e.g. an outbound attachment
    307          * picked from the  gallery) won't have a location, so the result will always be false.
    308          *
    309          * @param att the attachment to test
    310          * @param atts the array of attachments to look in
    311          * @return whether the test attachment is among the array of attachments
    312          */
    313         private static boolean amongAttachments(final Attachment att, final Attachment[] atts) {
    314             final String location = att.mLocation;
    315             if (location == null) return false;
    316             for (final Attachment a: atts) {
    317                 if (location.equals(a.mLocation)) {
    318                     return true;
    319                 }
    320             }
    321             return false;
    322         }
    323 
    324         /**
    325          * If this message should use SmartReply or SmartForward, return an object with the data
    326          * for the smart send.
    327          *
    328          * @param context the caller's context
    329          * @param account the Account we're sending from
    330          * @param message the Message being sent
    331          * @return an object to support smart sending, or null if not applicable.
    332          */
    333         public static SmartSendInfo getSmartSendInfo(final Context context,
    334                 final Account account, final Message message) {
    335             final int flags = message.mFlags;
    336             // We only care about the original message if we include quoted text.
    337             if ((flags & Message.FLAG_NOT_INCLUDE_QUOTED_TEXT) != 0) {
    338                 return null;
    339             }
    340             final boolean reply = (flags & Message.FLAG_TYPE_REPLY) != 0;
    341             final boolean forward = (flags & Message.FLAG_TYPE_FORWARD) != 0;
    342             // We also only care for replies or forwards.
    343             if (!reply && !forward) {
    344                 return null;
    345             }
    346             // Just a sanity check here, since we assume that reply and forward are mutually
    347             // exclusive throughout this class.
    348             if (reply && forward) {
    349                 return null;
    350             }
    351             // If we don't support SmartForward, then don't proceed.
    352             // TODO: For now, we assume that if we do not support Smart Forward, we also don't
    353             // support Smart Reply. At some point, perhaps these should be separate flags.
    354             if ((account.mFlags & Account.FLAGS_SUPPORTS_SMART_FORWARD) == 0) {
    355                 return null;
    356             }
    357 
    358             // Note: itemId and collectionId are the terms used by EAS to refer to the serverId and
    359             // mailboxId of a Message
    360             String itemId = null;
    361             String collectionId = null;
    362 
    363             // First, we need to get the id of the reply/forward message, 0 is the default value
    364             // so we are looking for something greater than 0.
    365             final long refId = Body.restoreBodySourceKey(context, message.mId);
    366             LogUtils.d(LOG_TAG, "getSmartSendInfo - found refId: %d for %d", refId, message.mId);
    367             if (refId > 0) {
    368                 // Then, we need the serverId and mailboxKey of the message
    369                 final String[] colsMailboxKey = Utility.getRowColumns(context, Message.CONTENT_URI,
    370                         refId, SyncColumns.SERVER_ID, MessageColumns.MAILBOX_KEY,
    371                         MessageColumns.PROTOCOL_SEARCH_INFO);
    372                 if (colsMailboxKey != null) {
    373                     itemId = colsMailboxKey[0];
    374                     final long boxId = Long.parseLong(colsMailboxKey[1]);
    375                     // Then, we need the serverId of the mailbox
    376                     final String[] colsServerId = Utility.getRowColumns(context,
    377                             Mailbox.CONTENT_URI, boxId, MailboxColumns.SERVER_ID);
    378                     if (colsServerId != null) {
    379                         collectionId = colsServerId[0];
    380                     }
    381                 }
    382             }
    383             // We need either a longId or both itemId (serverId) and collectionId (mailboxId) to
    384             // process a smart reply or a smart forward
    385             if (itemId != null && collectionId != null) {
    386                 final ArrayList<Attachment> requiredAtts;
    387                 if (forward) {
    388                     // See if we can really smart forward (all reference attachments must be sent)
    389                     final Attachment[] outAtts =
    390                             Attachment.restoreAttachmentsWithMessageId(context, message.mId);
    391                     final Attachment[] refAtts =
    392                             Attachment.restoreAttachmentsWithMessageId(context, refId);
    393                     for (final Attachment refAtt: refAtts) {
    394                         // If an original attachment isn't among what's going out, we can't be smart
    395                         if (!amongAttachments(refAtt, outAtts)) {
    396                             return null;
    397                         }
    398                     }
    399                     requiredAtts = new ArrayList<Attachment>();
    400                     for (final Attachment outAtt: outAtts) {
    401                         // If an outgoing attachment isn't in original message, we must send it
    402                         if (!amongAttachments(outAtt, refAtts)) {
    403                             requiredAtts.add(outAtt);
    404                         }
    405                     }
    406                 } else {
    407                     requiredAtts = null;
    408                 }
    409                 return new SmartSendInfo(itemId, collectionId, reply, requiredAtts);
    410             } else {
    411                 LogUtils.w(LOG_TAG,
    412                         "getSmartSendInfo - Skipping SmartSend, could not find IDs for: %d",
    413                         message.mId);
    414             }
    415             return null;
    416         }
    417     }
    418 
    419     @Override
    420     public String getRequestContentType() {
    421         // When using older protocols, we need to use a different MIME type for sending messages.
    422         if (getProtocolVersion() < Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) {
    423             return MimeUtility.MIME_TYPE_RFC822;
    424         } else {
    425             return super.getRequestContentType();
    426         }
    427     }
    428 
    429     /**
    430      * Our own HttpEntity subclass that is able to insert opaque data (in this case the MIME
    431      * representation of the message body as stored in a temporary file) into the serializer stream
    432      */
    433     private static class SendMailEntity extends InputStreamEntity {
    434         private final FileInputStream mFileStream;
    435         private final long mFileLength;
    436         private final int mSendTag;
    437         private final Message mMessage;
    438         private final SmartSendInfo mSmartSendInfo;
    439 
    440         public SendMailEntity(final FileInputStream instream, final long length, final int tag,
    441                 final Message message, final SmartSendInfo smartSendInfo) {
    442             super(instream, length);
    443             mFileStream = instream;
    444             mFileLength = length;
    445             mSendTag = tag;
    446             mMessage = message;
    447             mSmartSendInfo = smartSendInfo;
    448         }
    449 
    450         /**
    451          * We always return -1 because we don't know the actual length of the POST data (this
    452          * causes HttpClient to send the data in "chunked" mode)
    453          */
    454         @Override
    455         public long getContentLength() {
    456             final ByteArrayOutputStream baos = new ByteArrayOutputStream();
    457             try {
    458                 // Calculate the overhead for the WBXML data
    459                 writeTo(baos, false);
    460                 // Return the actual size that will be sent
    461                 return baos.size() + mFileLength;
    462             } catch (final IOException e) {
    463                 // Just return -1 (unknown)
    464             } finally {
    465                 try {
    466                     baos.close();
    467                 } catch (final IOException e) {
    468                     // Ignore
    469                 }
    470             }
    471             return -1;
    472         }
    473 
    474         @Override
    475         public void writeTo(final OutputStream outstream) throws IOException {
    476             writeTo(outstream, true);
    477         }
    478 
    479         /**
    480          * Write the message to the output stream
    481          * @param outstream the output stream to write
    482          * @param withData whether or not the actual data is to be written; true when sending
    483          *   mail; false when calculating size only
    484          * @throws IOException
    485          */
    486         public void writeTo(final OutputStream outstream, final boolean withData)
    487                 throws IOException {
    488             // Not sure if this is possible; the check is taken from the superclass
    489             if (outstream == null) {
    490                 throw new IllegalArgumentException("Output stream may not be null");
    491             }
    492 
    493             // We'll serialize directly into the output stream
    494             final Serializer s = new Serializer(outstream);
    495             // Send the appropriate initial tag
    496             s.start(mSendTag);
    497             // The Message-Id for this message (note that we cannot use the messageId stored in
    498             // the message, as EAS 14 limits the length to 40 chars and we use 70+)
    499             s.data(Tags.COMPOSE_CLIENT_ID, "SendMail-" + System.nanoTime());
    500             // We always save sent mail
    501             s.tag(Tags.COMPOSE_SAVE_IN_SENT_ITEMS);
    502 
    503             // If we're using smart reply/forward, we need info about the original message
    504             if (mSendTag != Tags.COMPOSE_SEND_MAIL) {
    505                 if (mSmartSendInfo != null) {
    506                     s.start(Tags.COMPOSE_SOURCE);
    507                     // For search results, use the long id (stored in mProtocolSearchInfo); else,
    508                     // use folder id/item id combo
    509                     if (mMessage.mProtocolSearchInfo != null) {
    510                         s.data(Tags.COMPOSE_LONG_ID, mMessage.mProtocolSearchInfo);
    511                     } else {
    512                         s.data(Tags.COMPOSE_ITEM_ID, mSmartSendInfo.mItemId);
    513                         s.data(Tags.COMPOSE_FOLDER_ID, mSmartSendInfo.mCollectionId);
    514                     }
    515                     s.end();  // Tags.COMPOSE_SOURCE
    516                 }
    517             }
    518 
    519             // Start the MIME tag; this is followed by "opaque" data (byte array)
    520             s.start(Tags.COMPOSE_MIME);
    521             // Send opaque data from the file stream
    522             if (withData) {
    523                 s.opaque(mFileStream, (int) mFileLength);
    524             } else {
    525                 s.writeOpaqueHeader((int) mFileLength);
    526             }
    527             // And we're done
    528             s.end().end().done();
    529         }
    530     }
    531 }
    532