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 }