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 EasOutboxSync(final Context context, final Account account, final Message message, final boolean useSmartSend)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 getCommand()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 getRequestEntity()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 handleHttpError(int httpStatus)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 onRequestMade()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 handleResponse(EasResponse response)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 */ writeMessageToTempFile(final File tmpFile, final Message message, final SmartSendInfo smartSendInfo)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 getModeTag(final SmartSendInfo smartSendInfo)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 SmartSendInfo(final String itemId, final String collectionId, final boolean isReply,ArrayList<Attachment> requiredAtts)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 generateSmartSendCmd()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 isForward()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 */ amongAttachments(final Attachment att, final Attachment[] atts)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 */ getSmartSendInfo(final Context context, final Account account, final Message message)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 getRequestContentType()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 SendMailEntity(final FileInputStream instream, final long length, final int tag, final Message message, final SmartSendInfo smartSendInfo)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 getContentLength()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 writeTo(final OutputStream outstream)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 */ writeTo(final OutputStream outstream, final boolean withData)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