• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
EasOutboxService(Context _context, Mailbox _mailbox)62     public EasOutboxService(Context _context, Mailbox _mailbox) {
63         super(_context, _mailbox);
64     }
65 
sendCallback(long msgId, String subject, int status)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      */
sendMessage(File cacheDir, long msgId)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
run()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      */
sendMessage(Context context, long accountId, Message msg)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 }