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