/* * Copyright (C) 2008-2009 Marc Blank * Licensed to The Android Open Source Project. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.exchange; import com.android.email.mail.MessagingException; import com.android.email.mail.transport.Rfc822Output; import com.android.email.provider.EmailContent.Body; import com.android.email.provider.EmailContent.BodyColumns; import com.android.email.provider.EmailContent.Mailbox; import com.android.email.provider.EmailContent.MailboxColumns; import com.android.email.provider.EmailContent.Message; import com.android.email.provider.EmailContent.MessageColumns; import com.android.email.provider.EmailContent.SyncColumns; import com.android.email.service.EmailServiceStatus; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.entity.InputStreamEntity; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.os.RemoteException; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; public class EasOutboxService extends EasSyncService { public static final int SEND_FAILED = 1; public static final String MAILBOX_KEY_AND_NOT_SEND_FAILED = MessageColumns.MAILBOX_KEY + "=? and (" + SyncColumns.SERVER_ID + " is null or " + SyncColumns.SERVER_ID + "!=" + SEND_FAILED + ')'; public static final String[] BODY_SOURCE_PROJECTION = new String[] {BodyColumns.SOURCE_MESSAGE_KEY}; public static final String WHERE_MESSAGE_KEY = Body.MESSAGE_KEY + "=?"; // This needs to be long enough to send the longest reasonable message, without being so long // as to effectively "hang" sending of mail. The standard 30 second timeout isn't long enough // for pictures and the like. For now, we'll use 15 minutes, in the knowledge that any socket // failure would probably generate an Exception before timing out anyway public static final int SEND_MAIL_TIMEOUT = 15*MINUTES; public EasOutboxService(Context _context, Mailbox _mailbox) { super(_context, _mailbox); } private void sendCallback(long msgId, String subject, int status) { try { SyncManager.callback().sendMessageStatus(msgId, subject, status, 0); } catch (RemoteException e) { // It's all good } } /** * Send a single message via EAS * Note that we mark messages SEND_FAILED when there is a permanent failure, rather than an * IOException, which is handled by SyncManager with retries, backoffs, etc. * * @param cacheDir the cache directory for this context * @param msgId the _id of the message to send * @throws IOException */ int sendMessage(File cacheDir, long msgId) throws IOException, MessagingException { int result; sendCallback(msgId, null, EmailServiceStatus.IN_PROGRESS); File tmpFile = File.createTempFile("eas_", "tmp", cacheDir); // Write the output to a temporary file try { String[] cols = getRowColumns(Message.CONTENT_URI, msgId, MessageColumns.FLAGS, MessageColumns.SUBJECT); int flags = Integer.parseInt(cols[0]); String subject = cols[1]; boolean reply = (flags & Message.FLAG_TYPE_REPLY) != 0; boolean forward = (flags & Message.FLAG_TYPE_FORWARD) != 0; // The reference message and mailbox are called item and collection in EAS String itemId = null; String collectionId = null; if (reply || forward) { // First, we need to get the id of the reply/forward message cols = getRowColumns(Body.CONTENT_URI, BODY_SOURCE_PROJECTION, WHERE_MESSAGE_KEY, new String[] {Long.toString(msgId)}); if (cols != null) { long refId = Long.parseLong(cols[0]); // Then, we need the serverId and mailboxKey of the message cols = getRowColumns(Message.CONTENT_URI, refId, SyncColumns.SERVER_ID, MessageColumns.MAILBOX_KEY); if (cols != null) { itemId = cols[0]; long boxId = Long.parseLong(cols[1]); // Then, we need the serverId of the mailbox cols = getRowColumns(Mailbox.CONTENT_URI, boxId, MailboxColumns.SERVER_ID); if (cols != null) { collectionId = cols[0]; } } } } boolean smartSend = itemId != null && collectionId != null; // Write the message in rfc822 format to the temporary file FileOutputStream fileStream = new FileOutputStream(tmpFile); Rfc822Output.writeTo(mContext, msgId, fileStream, !smartSend, true); fileStream.close(); // Now, get an input stream to our temporary file and create an entity with it FileInputStream inputStream = new FileInputStream(tmpFile); InputStreamEntity inputEntity = new InputStreamEntity(inputStream, tmpFile.length()); // Create the appropriate command and POST it to the server String cmd = "SendMail&SaveInSent=T"; if (smartSend) { cmd = reply ? "SmartReply" : "SmartForward"; cmd += "&ItemId=" + itemId + "&CollectionId=" + collectionId + "&SaveInSent=T"; } userLog("Send cmd: " + cmd); HttpResponse resp = sendHttpClientPost(cmd, inputEntity, SEND_MAIL_TIMEOUT); inputStream.close(); int code = resp.getStatusLine().getStatusCode(); if (code == HttpStatus.SC_OK) { userLog("Deleting message..."); mContentResolver.delete(ContentUris.withAppendedId(Message.CONTENT_URI, msgId), null, null); result = EmailServiceStatus.SUCCESS; sendCallback(-1, subject, EmailServiceStatus.SUCCESS); } else { userLog("Message sending failed, code: " + code); ContentValues cv = new ContentValues(); cv.put(SyncColumns.SERVER_ID, SEND_FAILED); Message.update(mContext, Message.CONTENT_URI, msgId, cv); // We mark the result as SUCCESS on a non-auth failure since the message itself is // already marked failed and we don't want to stop other messages from trying to // send. if (isAuthError(code)) { result = EmailServiceStatus.LOGIN_FAILED; } else { result = EmailServiceStatus.SUCCESS; } sendCallback(msgId, null, result); } } catch (IOException e) { // We catch this just to send the callback sendCallback(msgId, null, EmailServiceStatus.CONNECTION_ERROR); throw e; } finally { // Clean up the temporary file if (tmpFile.exists()) { tmpFile.delete(); } } return result; } @Override public void run() { setupService(); File cacheDir = mContext.getCacheDir(); try { mDeviceId = SyncManager.getDeviceId(); Cursor c = mContext.getContentResolver().query(Message.CONTENT_URI, Message.ID_COLUMN_PROJECTION, MAILBOX_KEY_AND_NOT_SEND_FAILED, new String[] {Long.toString(mMailbox.mId)}, null); try { while (c.moveToNext()) { long msgId = c.getLong(0); if (msgId != 0) { int result = sendMessage(cacheDir, msgId); // If there's an error, it should stop the service; we will distinguish // at least between login failures and everything else if (result == EmailServiceStatus.LOGIN_FAILED) { mExitStatus = EXIT_LOGIN_FAILURE; return; } else if (result == EmailServiceStatus.REMOTE_EXCEPTION) { mExitStatus = EXIT_EXCEPTION; return; } } } } finally { c.close(); } mExitStatus = EXIT_DONE; } catch (IOException e) { mExitStatus = EXIT_IO_ERROR; } catch (Exception e) { userLog("Exception caught in EasOutboxService", e); mExitStatus = EXIT_EXCEPTION; } finally { userLog(mMailbox.mDisplayName, ": sync finished"); userLog("Outbox exited with status ", mExitStatus); SyncManager.done(this); } } /** * Convenience method for adding a Message to an account's outbox * @param context the context of the caller * @param accountId the accountId for the sending account * @param msg the message to send */ public static void sendMessage(Context context, long accountId, Message msg) { Mailbox mailbox = Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_OUTBOX); if (mailbox != null) { msg.mMailboxKey = mailbox.mId; msg.mAccountKey = accountId; msg.save(context); } } }