EasOutboxService.java revision bb9b5163c19449efab72746351479d193be5e122
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
18package com.android.exchange;
19
20import com.android.email.mail.MessagingException;
21import com.android.email.mail.transport.Rfc822Output;
22import com.android.email.provider.EmailContent.Body;
23import com.android.email.provider.EmailContent.BodyColumns;
24import com.android.email.provider.EmailContent.Mailbox;
25import com.android.email.provider.EmailContent.MailboxColumns;
26import com.android.email.provider.EmailContent.Message;
27import com.android.email.provider.EmailContent.MessageColumns;
28import com.android.email.provider.EmailContent.SyncColumns;
29import com.android.email.service.EmailServiceStatus;
30
31import org.apache.http.HttpResponse;
32import org.apache.http.HttpStatus;
33import org.apache.http.entity.InputStreamEntity;
34
35import android.content.ContentUris;
36import android.content.ContentValues;
37import android.content.Context;
38import android.database.Cursor;
39import android.os.RemoteException;
40
41import java.io.File;
42import java.io.FileInputStream;
43import java.io.FileOutputStream;
44import java.io.IOException;
45
46public 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}