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