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