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