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