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 com.android.email.mail.MessagingException;
     21 import com.android.email.mail.transport.Rfc822Output;
     22 import com.android.email.provider.EmailContent.Body;
     23 import com.android.email.provider.EmailContent.BodyColumns;
     24 import com.android.email.provider.EmailContent.Mailbox;
     25 import com.android.email.provider.EmailContent.MailboxColumns;
     26 import com.android.email.provider.EmailContent.Message;
     27 import com.android.email.provider.EmailContent.MessageColumns;
     28 import com.android.email.provider.EmailContent.SyncColumns;
     29 import com.android.email.service.EmailServiceStatus;
     30 
     31 import org.apache.http.HttpResponse;
     32 import org.apache.http.HttpStatus;
     33 import org.apache.http.entity.InputStreamEntity;
     34 
     35 import android.content.ContentUris;
     36 import android.content.ContentValues;
     37 import android.content.Context;
     38 import android.database.Cursor;
     39 import android.os.RemoteException;
     40 
     41 import java.io.File;
     42 import java.io.FileInputStream;
     43 import java.io.FileOutputStream;
     44 import java.io.IOException;
     45 
     46 public class EasOutboxService extends EasSyncService {
     47 
     48     public static final int SEND_FAILED = 1;
     49     public static final String MAILBOX_KEY_AND_NOT_SEND_FAILED =
     50         MessageColumns.MAILBOX_KEY + "=? and (" + SyncColumns.SERVER_ID + " is null or " +
     51             SyncColumns.SERVER_ID + "!=" + SEND_FAILED + ')';
     52     public static final String[] BODY_SOURCE_PROJECTION =
     53         new String[] {BodyColumns.SOURCE_MESSAGE_KEY};
     54     public static final String WHERE_MESSAGE_KEY = Body.MESSAGE_KEY + "=?";
     55 
     56     // This needs to be long enough to send the longest reasonable message, without being so long
     57     // as to effectively "hang" sending of mail.  The standard 30 second timeout isn't long enough
     58     // for pictures and the like.  For now, we'll use 15 minutes, in the knowledge that any socket
     59     // failure would probably generate an Exception before timing out anyway
     60     public static final int SEND_MAIL_TIMEOUT = 15*MINUTES;
     61 
     62     public EasOutboxService(Context _context, Mailbox _mailbox) {
     63         super(_context, _mailbox);
     64     }
     65 
     66     private void sendCallback(long msgId, String subject, int status) {
     67         try {
     68             SyncManager.callback().sendMessageStatus(msgId, subject, status, 0);
     69         } catch (RemoteException e) {
     70             // It's all good
     71         }
     72     }
     73 
     74     /**
     75      * Send a single message via EAS
     76      * Note that we mark messages SEND_FAILED when there is a permanent failure, rather than an
     77      * IOException, which is handled by SyncManager with retries, backoffs, etc.
     78      *
     79      * @param cacheDir the cache directory for this context
     80      * @param msgId the _id of the message to send
     81      * @throws IOException
     82      */
     83     int sendMessage(File cacheDir, long msgId) throws IOException, MessagingException {
     84         int result;
     85         sendCallback(msgId, null, EmailServiceStatus.IN_PROGRESS);
     86         File tmpFile = File.createTempFile("eas_", "tmp", cacheDir);
     87         // Write the output to a temporary file
     88         try {
     89             String[] cols = getRowColumns(Message.CONTENT_URI, msgId, MessageColumns.FLAGS,
     90                     MessageColumns.SUBJECT);
     91             int flags = Integer.parseInt(cols[0]);
     92             String subject = cols[1];
     93 
     94             boolean reply = (flags & Message.FLAG_TYPE_REPLY) != 0;
     95             boolean forward = (flags & Message.FLAG_TYPE_FORWARD) != 0;
     96             // The reference message and mailbox are called item and collection in EAS
     97             String itemId = null;
     98             String collectionId = null;
     99             if (reply || forward) {
    100                 // First, we need to get the id of the reply/forward message
    101                 cols = getRowColumns(Body.CONTENT_URI, BODY_SOURCE_PROJECTION,
    102                         WHERE_MESSAGE_KEY, new String[] {Long.toString(msgId)});
    103                 if (cols != null) {
    104                     long refId = Long.parseLong(cols[0]);
    105                     // Then, we need the serverId and mailboxKey of the message
    106                     cols = getRowColumns(Message.CONTENT_URI, refId, SyncColumns.SERVER_ID,
    107                             MessageColumns.MAILBOX_KEY);
    108                     if (cols != null) {
    109                         itemId = cols[0];
    110                         long boxId = Long.parseLong(cols[1]);
    111                         // Then, we need the serverId of the mailbox
    112                         cols = getRowColumns(Mailbox.CONTENT_URI, boxId, MailboxColumns.SERVER_ID);
    113                         if (cols != null) {
    114                             collectionId = cols[0];
    115                         }
    116                     }
    117                 }
    118             }
    119 
    120             boolean smartSend = itemId != null && collectionId != null;
    121 
    122             // Write the message in rfc822 format to the temporary file
    123             FileOutputStream fileStream = new FileOutputStream(tmpFile);
    124             Rfc822Output.writeTo(mContext, msgId, fileStream, !smartSend, true);
    125             fileStream.close();
    126 
    127             // Now, get an input stream to our temporary file and create an entity with it
    128             FileInputStream inputStream = new FileInputStream(tmpFile);
    129             InputStreamEntity inputEntity =
    130                 new InputStreamEntity(inputStream, tmpFile.length());
    131 
    132             // Create the appropriate command and POST it to the server
    133             String cmd = "SendMail&SaveInSent=T";
    134             if (smartSend) {
    135                 cmd = reply ? "SmartReply" : "SmartForward";
    136                 cmd += "&ItemId=" + itemId + "&CollectionId=" + collectionId + "&SaveInSent=T";
    137             }
    138             userLog("Send cmd: " + cmd);
    139             HttpResponse resp = sendHttpClientPost(cmd, inputEntity, SEND_MAIL_TIMEOUT);
    140 
    141             inputStream.close();
    142             int code = resp.getStatusLine().getStatusCode();
    143             if (code == HttpStatus.SC_OK) {
    144                 userLog("Deleting message...");
    145                 mContentResolver.delete(ContentUris.withAppendedId(Message.CONTENT_URI, msgId),
    146                         null, null);
    147                 result = EmailServiceStatus.SUCCESS;
    148                 sendCallback(-1, subject, EmailServiceStatus.SUCCESS);
    149             } else {
    150                 userLog("Message sending failed, code: " + code);
    151                 ContentValues cv = new ContentValues();
    152                 cv.put(SyncColumns.SERVER_ID, SEND_FAILED);
    153                 Message.update(mContext, Message.CONTENT_URI, msgId, cv);
    154                 // We mark the result as SUCCESS on a non-auth failure since the message itself is
    155                 // already marked failed and we don't want to stop other messages from trying to
    156                 // send.
    157                 if (isAuthError(code)) {
    158                     result = EmailServiceStatus.LOGIN_FAILED;
    159                 } else {
    160                     result = EmailServiceStatus.SUCCESS;
    161                 }
    162                 sendCallback(msgId, null, result);
    163 
    164             }
    165         } catch (IOException e) {
    166             // We catch this just to send the callback
    167             sendCallback(msgId, null, EmailServiceStatus.CONNECTION_ERROR);
    168             throw e;
    169         } finally {
    170             // Clean up the temporary file
    171             if (tmpFile.exists()) {
    172                 tmpFile.delete();
    173             }
    174         }
    175         return result;
    176     }
    177 
    178     @Override
    179     public void run() {
    180         setupService();
    181         File cacheDir = mContext.getCacheDir();
    182         try {
    183             mDeviceId = SyncManager.getDeviceId();
    184             Cursor c = mContext.getContentResolver().query(Message.CONTENT_URI,
    185                     Message.ID_COLUMN_PROJECTION, MAILBOX_KEY_AND_NOT_SEND_FAILED,
    186                     new String[] {Long.toString(mMailbox.mId)}, null);
    187              try {
    188                 while (c.moveToNext()) {
    189                     long msgId = c.getLong(0);
    190                     if (msgId != 0) {
    191                         int result = sendMessage(cacheDir, msgId);
    192                         // If there's an error, it should stop the service; we will distinguish
    193                         // at least between login failures and everything else
    194                         if (result == EmailServiceStatus.LOGIN_FAILED) {
    195                             mExitStatus = EXIT_LOGIN_FAILURE;
    196                             return;
    197                         } else if (result == EmailServiceStatus.REMOTE_EXCEPTION) {
    198                             mExitStatus = EXIT_EXCEPTION;
    199                             return;
    200                         }
    201                     }
    202                 }
    203             } finally {
    204                  c.close();
    205             }
    206             mExitStatus = EXIT_DONE;
    207         } catch (IOException e) {
    208             mExitStatus = EXIT_IO_ERROR;
    209         } catch (Exception e) {
    210             userLog("Exception caught in EasOutboxService", e);
    211             mExitStatus = EXIT_EXCEPTION;
    212         } finally {
    213             userLog(mMailbox.mDisplayName, ": sync finished");
    214             userLog("Outbox exited with status ", mExitStatus);
    215             SyncManager.done(this);
    216         }
    217     }
    218 
    219     /**
    220      * Convenience method for adding a Message to an account's outbox
    221      * @param context the context of the caller
    222      * @param accountId the accountId for the sending account
    223      * @param msg the message to send
    224      */
    225     public static void sendMessage(Context context, long accountId, Message msg) {
    226         Mailbox mailbox = Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_OUTBOX);
    227         if (mailbox != null) {
    228             msg.mMailboxKey = mailbox.mId;
    229             msg.mAccountKey = accountId;
    230             msg.save(context);
    231         }
    232     }
    233 }