1 package com.android.exchange.eas; 2 3 import android.content.ContentUris; 4 import android.content.Context; 5 import android.net.Uri; 6 import android.provider.BaseColumns; 7 import android.text.format.DateUtils; 8 import android.util.Log; 9 10 import com.android.emailcommon.internet.MimeUtility; 11 import com.android.emailcommon.internet.Rfc822Output; 12 import com.android.emailcommon.provider.Account; 13 import com.android.emailcommon.provider.Mailbox; 14 import com.android.emailcommon.provider.EmailContent.Attachment; 15 import com.android.emailcommon.provider.EmailContent.Body; 16 import com.android.emailcommon.provider.EmailContent.MailboxColumns; 17 import com.android.emailcommon.provider.EmailContent.Message; 18 import com.android.emailcommon.provider.EmailContent.MessageColumns; 19 import com.android.emailcommon.provider.EmailContent.SyncColumns; 20 import com.android.emailcommon.utility.Utility; 21 import com.android.exchange.CommandStatusException; 22 import com.android.exchange.Eas; 23 import com.android.exchange.EasResponse; 24 import com.android.exchange.CommandStatusException.CommandStatus; 25 import com.android.exchange.adapter.SendMailParser; 26 import com.android.exchange.adapter.Serializer; 27 import com.android.exchange.adapter.Tags; 28 import com.android.exchange.adapter.Parser.EmptyStreamException; 29 import com.android.mail.utils.LogUtils; 30 31 import org.apache.http.HttpEntity; 32 import org.apache.http.HttpStatus; 33 import org.apache.http.entity.InputStreamEntity; 34 35 import java.io.ByteArrayOutputStream; 36 import java.io.File; 37 import java.io.FileInputStream; 38 import java.io.FileNotFoundException; 39 import java.io.FileOutputStream; 40 import java.io.IOException; 41 import java.io.OutputStream; 42 import java.util.ArrayList; 43 44 public class EasOutboxSync extends EasOperation { 45 46 // Value for a message's server id when sending fails. 47 public static final int SEND_FAILED = 1; 48 // This needs to be long enough to send the longest reasonable message, without being so long 49 // as to effectively "hang" sending of mail. The standard 30 second timeout isn't long enough 50 // for pictures and the like. For now, we'll use 15 minutes, in the knowledge that any socket 51 // failure would probably generate an Exception before timing out anyway 52 public static final long SEND_MAIL_TIMEOUT = 15 * DateUtils.MINUTE_IN_MILLIS; 53 54 public static final int RESULT_OK = 1; 55 public static final int RESULT_IO_ERROR = -100; 56 public static final int RESULT_ITEM_NOT_FOUND = -101; 57 public static final int RESULT_SEND_FAILED = -102; 58 59 private final Message mMessage; 60 private boolean mIsEas14; 61 private final File mCacheDir; 62 private final SmartSendInfo mSmartSendInfo; 63 private final int mModeTag; 64 private File mTmpFile; 65 private FileInputStream mFileStream; 66 67 public EasOutboxSync(final Context context, final Account account, final Message message, 68 final boolean useSmartSend) { 69 super(context, account); 70 mMessage = message; 71 initEas14(); 72 mCacheDir = context.getCacheDir(); 73 if (useSmartSend) { 74 mSmartSendInfo = SmartSendInfo.getSmartSendInfo(mContext, mAccount, mMessage); 75 } else { 76 mSmartSendInfo = null; 77 } 78 mModeTag = getModeTag(mSmartSendInfo); 79 } 80 81 /** 82 * Have to override EasOperation::init because it reloads mAccount, so we 83 * need to reset any derived values (eg, mIsEas14). 84 */ 85 @Override 86 public boolean init() { 87 initEas14(); 88 return true; 89 } 90 91 private void initEas14() { 92 mIsEas14 = Eas.isProtocolEas14(mAccount.mProtocolVersion); 93 } 94 95 @Override 96 protected String getCommand() { 97 String cmd = "SendMail"; 98 if (mSmartSendInfo != null) { 99 // In EAS 14, we don't send itemId and collectionId in the command 100 if (mIsEas14) { 101 cmd = mSmartSendInfo.isForward() ? "SmartForward" : "SmartReply"; 102 } else { 103 cmd = mSmartSendInfo.generateSmartSendCmd(); 104 } 105 } 106 // If we're not EAS 14, add our save-in-sent setting here 107 if (!mIsEas14) { 108 cmd += "&SaveInSent=T"; 109 } 110 return cmd; 111 } 112 113 @Override 114 protected HttpEntity getRequestEntity() throws IOException, MessageInvalidException { 115 try { 116 mTmpFile = File.createTempFile("eas_", "tmp", mCacheDir); 117 } catch (final IOException e) { 118 LogUtils.w(LOG_TAG, "IO error creating temp file"); 119 throw new IllegalStateException("Failure creating temp file"); 120 } 121 122 if (!writeMessageToTempFile(mTmpFile, mMessage, mSmartSendInfo)) { 123 // There are several reasons this could happen, possibly the message is corrupt (e.g. 124 // the To header is null) or the disk is too full to handle the temporary message. 125 // We can't send this message, but we don't want to abort the entire sync. Returning 126 // this error code will let the caller recognize that this operation failed, but we 127 // should continue on with the rest of the sync. 128 LogUtils.w(LOG_TAG, "IO error writing to temp file"); 129 throw new MessageInvalidException("Failure writing to temp file"); 130 } 131 132 try { 133 mFileStream = new FileInputStream(mTmpFile); 134 } catch (final FileNotFoundException e) { 135 LogUtils.w(LOG_TAG, "IO error creating fileInputStream"); 136 throw new IllegalStateException("Failure creating fileInputStream"); 137 } 138 final long fileLength = mTmpFile.length(); 139 final HttpEntity entity; 140 if (mIsEas14) { 141 entity = new SendMailEntity(mFileStream, fileLength, mModeTag, mMessage, 142 mSmartSendInfo); 143 } else { 144 entity = new InputStreamEntity(mFileStream, fileLength); 145 } 146 147 return entity; 148 } 149 150 @Override 151 protected int handleHttpError(int httpStatus) { 152 if (httpStatus == HttpStatus.SC_INTERNAL_SERVER_ERROR && mSmartSendInfo != null) { 153 // Let's retry without "smart" commands. 154 return RESULT_ITEM_NOT_FOUND; 155 } else { 156 return RESULT_OTHER_FAILURE; 157 } 158 } 159 160 /** 161 * This routine is called in a finally block in EasOperation.performOperation, 162 * so the request may have failed part way through and there is no guarantee 163 * what state we're in. 164 */ 165 @Override 166 protected void onRequestMade() { 167 if (mFileStream != null) { 168 try { 169 mFileStream.close(); 170 } catch (IOException e) { 171 LogUtils.w(LOG_TAG, "IOException closing fileStream %s", e); 172 } 173 mFileStream = null; 174 } 175 if (mTmpFile != null) { 176 if (mTmpFile.exists()) { 177 mTmpFile.delete(); 178 } 179 mTmpFile = null; 180 } 181 } 182 183 @Override 184 protected int handleResponse(EasResponse response) throws IOException, CommandStatusException { 185 if (mIsEas14) { 186 try { 187 // Try to parse the result 188 final SendMailParser p = new SendMailParser(response.getInputStream(), mModeTag); 189 // If we get here, the SendMail failed; go figure 190 p.parse(); 191 // The parser holds the status 192 final int status = p.getStatus(); 193 if (CommandStatus.isNeedsProvisioning(status)) { 194 LogUtils.w(LOG_TAG, "Needs provisioning before sending message: %d", 195 mMessage.mId); 196 return RESULT_PROVISIONING_ERROR; 197 } else if (status == CommandStatus.ITEM_NOT_FOUND && mSmartSendInfo != null) { 198 // Let's retry without "smart" commands. 199 LogUtils.w(LOG_TAG, "ITEM_NOT_FOUND smart sending message: %d", mMessage.mId); 200 return RESULT_ITEM_NOT_FOUND; 201 } 202 // TODO: Set syncServerId = SEND_FAILED in DB? 203 LogUtils.w(LOG_TAG, "General failure sending message: %d", mMessage.mId); 204 return RESULT_SEND_FAILED; 205 } catch (final EmptyStreamException e) { 206 // This is actually fine; an empty stream means SendMail succeeded 207 LogUtils.d(LOG_TAG, "Empty response sending message: %d", mMessage.mId); 208 // Don't return here, fall through so that we'll delete the sent message. 209 } catch (final IOException e) { 210 // Parsing failed in some other way. 211 LogUtils.e(LOG_TAG, e, "IOException sending message: %d", mMessage.mId); 212 return RESULT_IO_ERROR; 213 } 214 } else { 215 // FLAG: Do we need to parse results for earlier versions? 216 } 217 LogUtils.d(LOG_TAG, "Returning RESULT_OK after sending: %d", mMessage.mId); 218 mContext.getContentResolver().delete( 219 ContentUris.withAppendedId(Message.CONTENT_URI, mMessage.mId), null, null); 220 return RESULT_OK; 221 } 222 223 /** 224 * Writes message to the temp file. 225 * @param tmpFile The temp file to use. 226 * @param message The {@link Message} to write. 227 * @param smartSendInfo The {@link SmartSendInfo} for this message send attempt. 228 * @return Whether we could successfully write the file. 229 */ 230 private boolean writeMessageToTempFile(final File tmpFile, final Message message, 231 final SmartSendInfo smartSendInfo) { 232 final FileOutputStream fileStream; 233 try { 234 fileStream = new FileOutputStream(tmpFile); 235 Log.d(LogUtils.TAG, "created outputstream"); 236 } catch (final FileNotFoundException e) { 237 Log.e(LogUtils.TAG, "Failed to create message file", e); 238 return false; 239 } 240 try { 241 final boolean smartSend = smartSendInfo != null; 242 final ArrayList<Attachment> attachments = 243 smartSend ? smartSendInfo.mRequiredAtts : null; 244 Rfc822Output.writeTo(mContext, message, fileStream, smartSend, true, attachments); 245 } catch (final Exception e) { 246 Log.e(LogUtils.TAG, "Failed to write message file", e); 247 return false; 248 } finally { 249 try { 250 fileStream.close(); 251 } catch (final IOException e) { 252 // should not happen 253 Log.e(LogUtils.TAG, "Failed to close file - should not happen", e); 254 } 255 } 256 return true; 257 } 258 259 private int getModeTag(final SmartSendInfo smartSendInfo) { 260 if (mIsEas14) { 261 if (smartSendInfo == null) { 262 return Tags.COMPOSE_SEND_MAIL; 263 } else if (smartSendInfo.isForward()) { 264 return Tags.COMPOSE_SMART_FORWARD; 265 } else { 266 return Tags.COMPOSE_SMART_REPLY; 267 } 268 } 269 return 0; 270 } 271 272 /** 273 * Information needed for SmartReply/SmartForward. 274 */ 275 private static class SmartSendInfo { 276 final String mItemId; 277 final String mCollectionId; 278 final boolean mIsReply; 279 final ArrayList<Attachment> mRequiredAtts; 280 281 private SmartSendInfo(final String itemId, final String collectionId, 282 final boolean isReply,ArrayList<Attachment> requiredAtts) { 283 mItemId = itemId; 284 mCollectionId = collectionId; 285 mIsReply = isReply; 286 mRequiredAtts = requiredAtts; 287 } 288 289 public String generateSmartSendCmd() { 290 final StringBuilder sb = new StringBuilder(); 291 sb.append(isForward() ? "SmartForward" : "SmartReply"); 292 sb.append("&ItemId="); 293 sb.append(Uri.encode(mItemId, ":")); 294 sb.append("&CollectionId="); 295 sb.append(Uri.encode(mCollectionId, ":")); 296 return sb.toString(); 297 } 298 299 public boolean isForward() { 300 return !mIsReply; 301 } 302 303 /** 304 * See if a given attachment is among an array of attachments; it is if the locations of 305 * both are the same (we're looking to see if they represent the same attachment on the 306 * server. Note that an attachment that isn't on the server (e.g. an outbound attachment 307 * picked from the gallery) won't have a location, so the result will always be false. 308 * 309 * @param att the attachment to test 310 * @param atts the array of attachments to look in 311 * @return whether the test attachment is among the array of attachments 312 */ 313 private static boolean amongAttachments(final Attachment att, final Attachment[] atts) { 314 final String location = att.mLocation; 315 if (location == null) return false; 316 for (final Attachment a: atts) { 317 if (location.equals(a.mLocation)) { 318 return true; 319 } 320 } 321 return false; 322 } 323 324 /** 325 * If this message should use SmartReply or SmartForward, return an object with the data 326 * for the smart send. 327 * 328 * @param context the caller's context 329 * @param account the Account we're sending from 330 * @param message the Message being sent 331 * @return an object to support smart sending, or null if not applicable. 332 */ 333 public static SmartSendInfo getSmartSendInfo(final Context context, 334 final Account account, final Message message) { 335 final int flags = message.mFlags; 336 // We only care about the original message if we include quoted text. 337 if ((flags & Message.FLAG_NOT_INCLUDE_QUOTED_TEXT) != 0) { 338 return null; 339 } 340 final boolean reply = (flags & Message.FLAG_TYPE_REPLY) != 0; 341 final boolean forward = (flags & Message.FLAG_TYPE_FORWARD) != 0; 342 // We also only care for replies or forwards. 343 if (!reply && !forward) { 344 return null; 345 } 346 // Just a sanity check here, since we assume that reply and forward are mutually 347 // exclusive throughout this class. 348 if (reply && forward) { 349 return null; 350 } 351 // If we don't support SmartForward, then don't proceed. 352 // TODO: For now, we assume that if we do not support Smart Forward, we also don't 353 // support Smart Reply. At some point, perhaps these should be separate flags. 354 if ((account.mFlags & Account.FLAGS_SUPPORTS_SMART_FORWARD) == 0) { 355 return null; 356 } 357 358 // Note: itemId and collectionId are the terms used by EAS to refer to the serverId and 359 // mailboxId of a Message 360 String itemId = null; 361 String collectionId = null; 362 363 // First, we need to get the id of the reply/forward message, 0 is the default value 364 // so we are looking for something greater than 0. 365 final long refId = Body.restoreBodySourceKey(context, message.mId); 366 LogUtils.d(LOG_TAG, "getSmartSendInfo - found refId: %d for %d", refId, message.mId); 367 if (refId > 0) { 368 // Then, we need the serverId and mailboxKey of the message 369 final String[] colsMailboxKey = Utility.getRowColumns(context, Message.CONTENT_URI, 370 refId, SyncColumns.SERVER_ID, MessageColumns.MAILBOX_KEY, 371 MessageColumns.PROTOCOL_SEARCH_INFO); 372 if (colsMailboxKey != null) { 373 itemId = colsMailboxKey[0]; 374 final long boxId = Long.parseLong(colsMailboxKey[1]); 375 // Then, we need the serverId of the mailbox 376 final String[] colsServerId = Utility.getRowColumns(context, 377 Mailbox.CONTENT_URI, boxId, MailboxColumns.SERVER_ID); 378 if (colsServerId != null) { 379 collectionId = colsServerId[0]; 380 } 381 } 382 } 383 // We need either a longId or both itemId (serverId) and collectionId (mailboxId) to 384 // process a smart reply or a smart forward 385 if (itemId != null && collectionId != null) { 386 final ArrayList<Attachment> requiredAtts; 387 if (forward) { 388 // See if we can really smart forward (all reference attachments must be sent) 389 final Attachment[] outAtts = 390 Attachment.restoreAttachmentsWithMessageId(context, message.mId); 391 final Attachment[] refAtts = 392 Attachment.restoreAttachmentsWithMessageId(context, refId); 393 for (final Attachment refAtt: refAtts) { 394 // If an original attachment isn't among what's going out, we can't be smart 395 if (!amongAttachments(refAtt, outAtts)) { 396 return null; 397 } 398 } 399 requiredAtts = new ArrayList<Attachment>(); 400 for (final Attachment outAtt: outAtts) { 401 // If an outgoing attachment isn't in original message, we must send it 402 if (!amongAttachments(outAtt, refAtts)) { 403 requiredAtts.add(outAtt); 404 } 405 } 406 } else { 407 requiredAtts = null; 408 } 409 return new SmartSendInfo(itemId, collectionId, reply, requiredAtts); 410 } else { 411 LogUtils.w(LOG_TAG, 412 "getSmartSendInfo - Skipping SmartSend, could not find IDs for: %d", 413 message.mId); 414 } 415 return null; 416 } 417 } 418 419 @Override 420 public String getRequestContentType() { 421 // When using older protocols, we need to use a different MIME type for sending messages. 422 if (getProtocolVersion() < Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) { 423 return MimeUtility.MIME_TYPE_RFC822; 424 } else { 425 return super.getRequestContentType(); 426 } 427 } 428 429 /** 430 * Our own HttpEntity subclass that is able to insert opaque data (in this case the MIME 431 * representation of the message body as stored in a temporary file) into the serializer stream 432 */ 433 private static class SendMailEntity extends InputStreamEntity { 434 private final FileInputStream mFileStream; 435 private final long mFileLength; 436 private final int mSendTag; 437 private final Message mMessage; 438 private final SmartSendInfo mSmartSendInfo; 439 440 public SendMailEntity(final FileInputStream instream, final long length, final int tag, 441 final Message message, final SmartSendInfo smartSendInfo) { 442 super(instream, length); 443 mFileStream = instream; 444 mFileLength = length; 445 mSendTag = tag; 446 mMessage = message; 447 mSmartSendInfo = smartSendInfo; 448 } 449 450 /** 451 * We always return -1 because we don't know the actual length of the POST data (this 452 * causes HttpClient to send the data in "chunked" mode) 453 */ 454 @Override 455 public long getContentLength() { 456 final ByteArrayOutputStream baos = new ByteArrayOutputStream(); 457 try { 458 // Calculate the overhead for the WBXML data 459 writeTo(baos, false); 460 // Return the actual size that will be sent 461 return baos.size() + mFileLength; 462 } catch (final IOException e) { 463 // Just return -1 (unknown) 464 } finally { 465 try { 466 baos.close(); 467 } catch (final IOException e) { 468 // Ignore 469 } 470 } 471 return -1; 472 } 473 474 @Override 475 public void writeTo(final OutputStream outstream) throws IOException { 476 writeTo(outstream, true); 477 } 478 479 /** 480 * Write the message to the output stream 481 * @param outstream the output stream to write 482 * @param withData whether or not the actual data is to be written; true when sending 483 * mail; false when calculating size only 484 * @throws IOException 485 */ 486 public void writeTo(final OutputStream outstream, final boolean withData) 487 throws IOException { 488 // Not sure if this is possible; the check is taken from the superclass 489 if (outstream == null) { 490 throw new IllegalArgumentException("Output stream may not be null"); 491 } 492 493 // We'll serialize directly into the output stream 494 final Serializer s = new Serializer(outstream); 495 // Send the appropriate initial tag 496 s.start(mSendTag); 497 // The Message-Id for this message (note that we cannot use the messageId stored in 498 // the message, as EAS 14 limits the length to 40 chars and we use 70+) 499 s.data(Tags.COMPOSE_CLIENT_ID, "SendMail-" + System.nanoTime()); 500 // We always save sent mail 501 s.tag(Tags.COMPOSE_SAVE_IN_SENT_ITEMS); 502 503 // If we're using smart reply/forward, we need info about the original message 504 if (mSendTag != Tags.COMPOSE_SEND_MAIL) { 505 if (mSmartSendInfo != null) { 506 s.start(Tags.COMPOSE_SOURCE); 507 // For search results, use the long id (stored in mProtocolSearchInfo); else, 508 // use folder id/item id combo 509 if (mMessage.mProtocolSearchInfo != null) { 510 s.data(Tags.COMPOSE_LONG_ID, mMessage.mProtocolSearchInfo); 511 } else { 512 s.data(Tags.COMPOSE_ITEM_ID, mSmartSendInfo.mItemId); 513 s.data(Tags.COMPOSE_FOLDER_ID, mSmartSendInfo.mCollectionId); 514 } 515 s.end(); // Tags.COMPOSE_SOURCE 516 } 517 } 518 519 // Start the MIME tag; this is followed by "opaque" data (byte array) 520 s.start(Tags.COMPOSE_MIME); 521 // Send opaque data from the file stream 522 if (withData) { 523 s.opaque(mFileStream, (int) mFileLength); 524 } else { 525 s.writeOpaqueHeader((int) mFileLength); 526 } 527 // And we're done 528 s.end().end().done(); 529 } 530 } 531 } 532