/** * Parse a message from the server stream. * * @return the parsed Message * @throws IOException */ private EmailContent.Message addParser(final int endingTag) throws IOException, CommandStatusException { EmailContent.Message msg = new EmailContent.Message(); msg.mAccountKey = mAccount.mId; msg.mMailboxKey = mMailbox.mId; msg.mFlagLoaded = EmailContent.Message.FLAG_LOADED_COMPLETE; // Default to 1 (success) in case we don't get this tag int status = 1; while (nextTag(endingTag) != END) { switch (tag) { case Tags.SYNC_SERVER_ID: msg.mServerId = getValue(); LogUtils.d(TAG, "ServerId: %s", msg.mServerId); break; case Tags.SYNC_STATUS: status = getValueInt(); break; case Tags.SYNC_APPLICATION_DATA: addData(msg, tag); break; default: skipTag(); } } // For sync, status 1 = success if (status != 1) { throw new CommandStatusException(status, msg.mServerId); } return msg; }
/** * Add a single attachment part to the message * * <p>This will skip adding attachments if they are already found in the attachments table. The * heuristic for this will fail (false-positive) if two identical attachments are included in a * single POP3 message. TODO: Fix that, by (elsewhere) simulating an mLocation value based on the * attachments position within the list of multipart/mixed elements. This would make every POP3 * attachment unique, and might also simplify the code (since we could just look at the positions, * and ignore the filename, etc.) * * <p>TODO: Take a closer look at encoding and deal with it if necessary. * * @param context a context for file operations * @param localMessage the attachments will be built against this message * @param part a single attachment part from POP or IMAP */ public static void addOneAttachment( final Context context, final EmailContent.Message localMessage, final Part part) throws MessagingException, IOException { final Attachment localAttachment = mimePartToAttachment(part); localAttachment.mMessageKey = localMessage.mId; localAttachment.mAccountKey = localMessage.mAccountKey; if (DEBUG_ATTACHMENTS) { LogUtils.d(Logging.LOG_TAG, "Add attachment " + localAttachment); } // To prevent duplication - do we already have a matching attachment? // The fields we'll check for equality are: // mFileName, mMimeType, mContentId, mMessageKey, mLocation // NOTE: This will false-positive if you attach the exact same file, twice, to a POP3 // message. We can live with that - you'll get one of the copies. final Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, localMessage.mId); final Cursor cursor = context.getContentResolver().query(uri, Attachment.CONTENT_PROJECTION, null, null, null); boolean attachmentFoundInDb = false; try { while (cursor.moveToNext()) { final Attachment dbAttachment = new Attachment(); dbAttachment.restore(cursor); // We test each of the fields here (instead of in SQL) because they may be // null, or may be strings. if (!TextUtils.equals(dbAttachment.mFileName, localAttachment.mFileName) || !TextUtils.equals(dbAttachment.mMimeType, localAttachment.mMimeType) || !TextUtils.equals(dbAttachment.mContentId, localAttachment.mContentId) || !TextUtils.equals(dbAttachment.mLocation, localAttachment.mLocation)) { continue; } // We found a match, so use the existing attachment id, and stop looking/looping attachmentFoundInDb = true; localAttachment.mId = dbAttachment.mId; if (DEBUG_ATTACHMENTS) { LogUtils.d(Logging.LOG_TAG, "Skipped, found db attachment " + dbAttachment); } break; } } finally { cursor.close(); } // Save the attachment (so far) in order to obtain an id if (!attachmentFoundInDb) { localAttachment.save(context); } // If an attachment body was actually provided, we need to write the file now saveAttachmentBody(context, part, localAttachment, localMessage.mAccountKey); if (localMessage.mAttachments == null) { localMessage.mAttachments = new ArrayList<Attachment>(); } localMessage.mAttachments.add(localAttachment); localMessage.mFlagAttachment = true; }
@Override public void loadMore(long messageId) throws RemoteException { /// M: We Can't load more in low storage state @{ if (StorageLowState.checkIfStorageLow(mContext)) { LogUtils.e(Logging.LOG_TAG, "Can't load more due to low storage"); return; } /// @} // Load a message for view... try { // 1. Resample the message, in case it disappeared or synced while // this command was in queue final EmailContent.Message message = EmailContent.Message.restoreMessageWithId(mContext, messageId); if (message == null) { return; } if (message.mFlagLoaded == EmailContent.Message.FLAG_LOADED_COMPLETE) { // We should NEVER get here return; } // 2. Open the remote folder. // TODO combine with common code in loadAttachment final Account account = Account.restoreAccountWithId(mContext, message.mAccountKey); final Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey); if (account == null || mailbox == null) { // mListeners.loadMessageForViewFailed(messageId, "null account or mailbox"); return; } TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account)); final Store remoteStore = Store.getInstance(account, mContext); final String remoteServerId; // If this is a search result, use the protocolSearchInfo field to get the // correct remote location if (!TextUtils.isEmpty(message.mProtocolSearchInfo)) { remoteServerId = message.mProtocolSearchInfo; } else { remoteServerId = mailbox.mServerId; } final Folder remoteFolder = remoteStore.getFolder(remoteServerId); remoteFolder.open(OpenMode.READ_WRITE); // 3. Set up to download the entire message final Message remoteMessage = remoteFolder.getMessage(message.mServerId); final FetchProfile fp = new FetchProfile(); fp.add(FetchProfile.Item.BODY); remoteFolder.fetch(new Message[] {remoteMessage}, fp, null); // 4. Write to provider Utilities.copyOneMessageToProvider( mContext, remoteMessage, account, mailbox, EmailContent.Message.FLAG_LOADED_COMPLETE); } catch (MessagingException me) { if (Logging.LOGD) LogUtils.v(Logging.LOG_TAG, "", me); } catch (RuntimeException rte) { LogUtils.d(Logging.LOG_TAG, "RTE During loadMore"); } }
private void bodyParser(EmailContent.Message msg) throws IOException { String bodyType = Eas.BODY_PREFERENCE_TEXT; String body = ""; while (nextTag(Tags.BASE_BODY) != END) { switch (tag) { case Tags.BASE_TYPE: bodyType = getValue(); break; case Tags.BASE_DATA: body = getValue(); break; case Tags.BASE_TRUNCATED: // M: Message is partial loaded if AirSyncBaseBody::Truncated is True; // Otherwise message is complete loaded if False String bodyTruncated = getValue(); if ("1".equals(bodyTruncated) || "true".equals(bodyTruncated)) { msg.mFlagLoaded = EmailContent.Message.FLAG_LOADED_PARTIAL; } else { msg.mFlagLoaded = EmailContent.Message.FLAG_LOADED_COMPLETE; } LogUtils.d(TAG, "_____________ EAS 12+ body truncated: " + bodyTruncated); break; default: skipTag(); } } // We always ask for TEXT or HTML; there's no third option if (bodyType.equals(Eas.BODY_PREFERENCE_HTML)) { msg.mHtml = body; } else { msg.mText = body; } /** * M: What if a mail with bodyTruncated tag, but both body html and text are empty? In this * case, client should set a suitable load flag: FLAG_LOADED_COMPLETE. @{ */ if (TextUtils.isEmpty(msg.mHtml) && TextUtils.isEmpty(msg.mText)) { LogUtils.d(TAG, " for empty mailbody, reset load complete tag."); msg.mFlagLoaded = EmailContent.Message.FLAG_LOADED_COMPLETE; } /** @} */ }
/** * Copy attachments from MimeMessage to provider Message. * * @param context a context for file operations * @param localMessage the attachments will be built against this message * @param attachments the attachments to add */ public static void updateAttachments( final Context context, final EmailContent.Message localMessage, final ArrayList<Part> attachments) throws MessagingException, IOException { localMessage.mAttachments = null; for (Part attachmentPart : attachments) { addOneAttachment(context, localMessage, attachmentPart); } }
/** * Parses untruncated MIME data, saving away the text parts * * @param msg the message we're building * @param mimeData the MIME data we've received from the server * @throws IOException */ private static void mimeBodyParser(EmailContent.Message msg, String mimeData) throws IOException { try { ByteArrayInputStream in = new ByteArrayInputStream(mimeData.getBytes()); // The constructor parses the message MimeMessage mimeMessage = new MimeMessage(in); // Now process body parts & attachments ArrayList<Part> viewables = new ArrayList<Part>(); // We'll ignore the attachments, as we'll get them directly from EAS ArrayList<Part> attachments = new ArrayList<Part>(); MimeUtility.collectParts(mimeMessage, viewables, attachments); // parseBodyFields fills in the content fields of the Body ConversionUtilities.BodyFieldData data = ConversionUtilities.parseBodyFields(viewables); // But we need them in the message itself for handling during commit() msg.setFlags(data.isQuotedReply, data.isQuotedForward); msg.mSnippet = data.snippet; msg.mHtml = data.htmlContent; msg.mText = data.textContent; } catch (MessagingException e) { // This would most likely indicate a broken stream throw new IOException(e); } }
/** * Set up the meetingInfo field in the message with various pieces of information gleaned from * MeetingRequest tags. This information will be used later to generate an appropriate reply email * if the user chooses to respond * * @param msg the Message being built * @throws IOException */ private void meetingRequestParser(EmailContent.Message msg) throws IOException { PackedString.Builder packedString = new PackedString.Builder(); while (nextTag(Tags.EMAIL_MEETING_REQUEST) != END) { switch (tag) { case Tags.EMAIL_DTSTAMP: packedString.put(MeetingInfo.MEETING_DTSTAMP, getValue()); break; case Tags.EMAIL_START_TIME: packedString.put(MeetingInfo.MEETING_DTSTART, getValue()); break; case Tags.EMAIL_END_TIME: packedString.put(MeetingInfo.MEETING_DTEND, getValue()); break; case Tags.EMAIL_ORGANIZER: packedString.put(MeetingInfo.MEETING_ORGANIZER_EMAIL, getValue()); break; case Tags.EMAIL_LOCATION: packedString.put(MeetingInfo.MEETING_LOCATION, getValue()); break; case Tags.EMAIL_GLOBAL_OBJID: packedString.put( MeetingInfo.MEETING_UID, CalendarUtilities.getUidFromGlobalObjId(getValue())); break; case Tags.EMAIL_CATEGORIES: skipParser(tag); break; case Tags.EMAIL_RECURRENCES: recurrencesParser(); break; case Tags.EMAIL_RESPONSE_REQUESTED: packedString.put(MeetingInfo.MEETING_RESPONSE_REQUESTED, getValue()); break; case Tags.EMAIL_ALL_DAY_EVENT: if (getValueInt() == 1) { packedString.put(MeetingInfo.MEETING_ALL_DAY, "1"); } break; default: skipTag(); } } if (msg.mSubject != null) { packedString.put(MeetingInfo.MEETING_TITLE, msg.mSubject); } msg.mMeetingInfo = packedString.toString(); }
public void commitImpl(int maxOpsPerBatch) throws RemoteException, OperationApplicationException { // Use a batch operation to handle the changes ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); // Maximum size of message text per fetch int numFetched = fetchedEmails.size(); LogUtils.d( TAG, "commitImpl: maxOpsPerBatch=%d numFetched=%d numNew=%d " + "numDeleted=%d numChanged=%d", maxOpsPerBatch, numFetched, newEmails.size(), deletedEmails.size(), changedEmails.size()); for (EmailContent.Message msg : fetchedEmails) { // Find the original message's id (by serverId and mailbox) Cursor c = getServerIdCursor(msg.mServerId, EmailContent.ID_PROJECTION); String id = null; try { if (c.moveToFirst()) { id = c.getString(EmailContent.ID_PROJECTION_COLUMN); while (c.moveToNext()) { // This shouldn't happen, but clean up if it does Long dupId = Long.parseLong(c.getString(EmailContent.ID_PROJECTION_COLUMN)); userLog("Delete duplicate with id: " + dupId); deletedEmails.add(dupId); } } } finally { c.close(); } // If we find one, we do two things atomically: 1) set the body text for the // message, and 2) mark the message loaded (i.e. completely loaded) if (id != null) { LogUtils.i(TAG, "Fetched body successfully for %s", id); final String[] bindArgument = new String[] {id}; ops.add( ContentProviderOperation.newUpdate(EmailContent.Body.CONTENT_URI) .withSelection(EmailContent.Body.SELECTION_BY_MESSAGE_KEY, bindArgument) .withValue(EmailContent.BodyColumns.TEXT_CONTENT, msg.mText) .build()); ops.add( ContentProviderOperation.newUpdate(EmailContent.Message.CONTENT_URI) .withSelection(MessageColumns._ID + "=?", bindArgument) .withValue(MessageColumns.FLAG_LOADED, EmailContent.Message.FLAG_LOADED_COMPLETE) .build()); } applyBatchIfNeeded(ops, maxOpsPerBatch, false); } /** M: "Bad Sync Key" recovery process @{ */ if (mMailbox.mId == Exchange.sBadSyncKeyMailboxId && newEmails.size() > 0) { LogUtils.i(Eas.BSK_TAG, "newEmails count:" + newEmails.size()); // Delete the local mails older than the oldest mail timestamp in the 1st window ExchangePreferences pref = ExchangePreferences.getPreferences(mContext); boolean isStaleMailsRemoved = pref.getRemovedStaleMails(); if (!isStaleMailsRemoved) { // Get the oldest mail's time stamp long oldestTimestamp = 0; oldestTimestamp = newEmails.get(0).mTimeStamp; for (Message msg : newEmails) { if (msg.mTimeStamp < oldestTimestamp) { oldestTimestamp = msg.mTimeStamp; } } LogUtils.i(Eas.BSK_TAG, "Oldest timestamp: " + oldestTimestamp); // Delete all the local mails older than the time stamp int rowDeleted = mContentResolver.delete( Message.CONTENT_URI, WHERE_MESSAGE_TIMESTAMP_LESS_THAN, new String[] {String.valueOf(oldestTimestamp), String.valueOf(mMailbox.mId)}); LogUtils.i(Eas.BSK_TAG, rowDeleted + " local stale mails were deleted"); // Set all mails of this mailbox as "dirty" at first. Then matching local mail // with the new mail by their timestamp. If found, clear the "dirty" flag. In // this way, we can ultimately mark the stale local mails and remove them ContentValues cv = new ContentValues(); cv.put(MessageColumns.DIRTY, "1"); mContentResolver.update( Message.CONTENT_URI, cv, WHERE_MAILBOX_KEY, new String[] {String.valueOf(mMailbox.mId)}); pref.setRemovedStaleMails(true); } // Remove the stale flag of the local mails which can be found in the new-synchronized mails // (by compare their time-stamp). Note that server id can not regarded as the identity of // a mail because it might be changed by server in the process of the full re-sync LogUtils.i(Eas.BSK_TAG, "Finding all the local mails with the same timestamp"); StringBuilder timestampList = new StringBuilder("("); for (Message msg : newEmails) { timestampList.append(msg.mTimeStamp); timestampList.append(','); } // Delete the last comma if (!newEmails.isEmpty()) { timestampList.deleteCharAt(timestampList.length() - 1); } timestampList.append(")"); String selection = MessageColumns.MAILBOX_KEY + "=" + Long.toString(mMailbox.mId) + " AND " + MessageColumns.TIMESTAMP + " IN " + timestampList.toString(); LogUtils.i(Eas.BSK_TAG, "selection clause: " + selection); Cursor c = mContentResolver.query( Message.CONTENT_URI, new String[] {MessageColumns._ID, MessageColumns.TIMESTAMP}, selection, null, null); try { if (c != null) { final int columnMessageId = 0; final int columnTimestamp = 1; ArrayList<ContentProviderOperation> bskOps = new ArrayList<ContentProviderOperation>(); LogUtils.i( Eas.BSK_TAG, "For current window, found " + c.getCount() + " mails existed in local DB"); while (c.moveToNext()) { long timestamp = c.getLong(columnTimestamp); for (Message msg : newEmails) { if (msg.mTimeStamp == timestamp) { // Update the properties of local mails ContentValues cv = new ContentValues(); cv.put(MessageColumns.SERVER_ID, msg.mServerId); cv.put(MessageColumns.FLAG_FAVORITE, msg.mFlagFavorite); cv.put(MessageColumns.FLAGS, msg.mFlags); cv.put(MessageColumns.FLAG_READ, msg.mFlagRead); cv.put(MessageColumns.DIRTY, "0"); bskOps.add( ContentProviderOperation.newUpdate( ContentUris.withAppendedId( Message.CONTENT_URI, c.getInt(columnMessageId))) .withValues(cv) .build()); // Remove the existed mail from new mail list, then it would not be store again newEmails.remove(msg); applyBatchIfNeeded(bskOps, maxOpsPerBatch, false); break; } } } applyBatchIfNeeded(bskOps, maxOpsPerBatch, true); } } catch (RemoteException e) { // There is nothing to be done here; fail by returning null LogUtils.i(Eas.BSK_TAG, "RemoteException when applyBatch"); } catch (OperationApplicationException e) { // There is nothing to be done here; fail by returning null LogUtils.i(Eas.BSK_TAG, "OperationApplicationException when applyBatch"); } finally { if (c != null) { c.close(); } } LogUtils.i( Eas.BSK_TAG, "There are " + newEmails.size() + " mail(s) remaining in the newEmails list"); } /** @} */ /// M: For smart push, record new mails' coming DataCollectUtils.recordNewMails(mContext, newEmails); for (EmailContent.Message msg : newEmails) { msg.addSaveOps(ops); applyBatchIfNeeded(ops, maxOpsPerBatch, false); } /// M: Log receive new message. @{ EmailContent.Message.logMessageReceived(mContext, newEmails.toArray(new Message[] {})); /// @} for (Long id : deletedEmails) { /** * M: 1. The attachments were belonged to the com.android.email application, delete * attachments here, it would fail, so mask the function call to * AttachmentUtilities.deleteAllAttachmentFiles here and do the attachment delete operation on * Email provider. 2. Add a new parameter (account id) to URI for delete attachments on Email * side. @{ */ Uri.Builder builder = EmailContent.Message.CONTENT_URI.buildUpon(); builder.appendEncodedPath(String.valueOf(id)); builder.appendQueryParameter( AttachmentUtilities.KEY_ACCOUNT_ID, String.valueOf(mAccount.mId)); ops.add(ContentProviderOperation.newDelete(builder.build()).build()); // AttachmentUtilities.deleteAllAttachmentFiles(mContext, mAccount.mId, id); /** @} */ applyBatchIfNeeded(ops, maxOpsPerBatch, false); } if (!changedEmails.isEmpty()) { // Server wins in a conflict... for (ServerChange change : changedEmails) { ContentValues cv = new ContentValues(); if (change.read != null) { cv.put(EmailContent.MessageColumns.FLAG_READ, change.read); } if (change.flag != null) { cv.put(EmailContent.MessageColumns.FLAG_FAVORITE, change.flag); } if (change.flags != null) { cv.put(EmailContent.MessageColumns.FLAGS, change.flags); } ops.add( ContentProviderOperation.newUpdate( ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, change.id)) .withValues(cv) .build()); } applyBatchIfNeeded(ops, maxOpsPerBatch, false); } // We only want to update the sync key here ContentValues mailboxValues = new ContentValues(); mailboxValues.put(Mailbox.SYNC_KEY, mMailbox.mSyncKey); ops.add( ContentProviderOperation.newUpdate( ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId)) .withValues(mailboxValues) .build()); applyBatchIfNeeded(ops, maxOpsPerBatch, true); userLog(mMailbox.mDisplayName, " SyncKey saved as: ", mMailbox.mSyncKey); }
private void attachmentParser( final ArrayList<EmailContent.Attachment> atts, final EmailContent.Message msg, final int endingTag) throws IOException { String fileName = null; String length = null; String location = null; boolean isInline = false; String contentId = null; while (nextTag(endingTag) != END) { switch (tag) { // We handle both EAS 2.5 and 12.0+ attachments here case Tags.EMAIL_DISPLAY_NAME: case Tags.BASE_DISPLAY_NAME: fileName = getValue(); break; case Tags.EMAIL_ATT_NAME: case Tags.BASE_FILE_REFERENCE: location = getValue(); break; case Tags.EMAIL_ATT_SIZE: case Tags.BASE_ESTIMATED_DATA_SIZE: length = getValue(); break; case Tags.BASE_IS_INLINE: String isInlineStr = getValue(); isInline = "true".equalsIgnoreCase(isInlineStr) || "1".equals(isInlineStr); break; case Tags.BASE_CONTENT_ID: contentId = getValue(); break; default: skipTag(); } } if ((fileName != null) && (length != null) && (location != null)) { EmailContent.Attachment att = new EmailContent.Attachment(); att.mEncoding = "base64"; att.mSize = Long.parseLong(length); att.mFileName = fileName; att.mLocation = location; att.mMimeType = getMimeTypeFromFileName(fileName); att.mAccountKey = mAccount.mId; // Save away the contentId, if we've got one (for inline images); note that the // EAS docs appear to be wrong about the tags used; inline images come with // contentId rather than contentLocation, when sent from Ex03, Ex07, and Ex10 if (isInline && !TextUtils.isEmpty(contentId)) { att.mContentId = contentId; } // Check if this attachment can't be downloaded due to an account policy if (mPolicy != null) { if (mPolicy.mDontAllowAttachments || (mPolicy.mMaxAttachmentSize > 0 && (att.mSize > mPolicy.mMaxAttachmentSize))) { att.mFlags = EmailContent.Attachment.FLAG_POLICY_DISALLOWS_DOWNLOAD; } } atts.add(att); msg.mFlagAttachment = true; } }
public void addData(EmailContent.Message msg, int endingTag) throws IOException { ArrayList<EmailContent.Attachment> atts = new ArrayList<EmailContent.Attachment>(); boolean truncated = false; while (nextTag(endingTag) != END) { switch (tag) { case Tags.EMAIL_ATTACHMENTS: case Tags.BASE_ATTACHMENTS: // BASE_ATTACHMENTS is used in EAS 12.0 and up attachmentsParser(atts, msg, tag); break; case Tags.EMAIL_TO: msg.mTo = Address.pack(Address.parse(getValue(), false)); break; case Tags.EMAIL_FROM: Address[] froms = Address.parse(getValue(), false); if (froms != null && froms.length > 0) { msg.mDisplayName = froms[0].toFriendly(); } msg.mFrom = Address.toString(froms); break; case Tags.EMAIL_CC: msg.mCc = Address.pack(Address.parse(getValue(), false)); break; case Tags.EMAIL_REPLY_TO: msg.mReplyTo = Address.pack(Address.parse(getValue(), false)); break; case Tags.EMAIL_DATE_RECEIVED: try { msg.mTimeStamp = Utility.parseEmailDateTimeToMillis(getValue()); } catch (ParseException e) { LogUtils.w(TAG, "Parse error for EMAIL_DATE_RECEIVED tag.", e); } break; case Tags.EMAIL_SUBJECT: msg.mSubject = getValue(); break; case Tags.EMAIL_READ: msg.mFlagRead = getValueInt() == 1; break; case Tags.BASE_BODY: bodyParser(msg); break; case Tags.EMAIL_FLAG: msg.mFlagFavorite = flagParser(); break; case Tags.EMAIL_MIME_TRUNCATED: truncated = getValueInt() == 1; break; case Tags.EMAIL_MIME_DATA: // We get MIME data for EAS 2.5. First we parse it, then we take the // html and/or plain text data and store it in the message if (truncated) { // If the MIME data is truncated, don't bother parsing it, because // it will take time and throw an exception anyway when EOF is reached // In this case, we will load the body separately by tagging the message // "partially loaded". // Get the data (and ignore it) getValue(); userLog("Partially loaded: ", msg.mServerId); msg.mFlagLoaded = EmailContent.Message.FLAG_LOADED_PARTIAL; mFetchNeeded = true; } else { mimeBodyParser(msg, getValue()); } break; case Tags.EMAIL_BODY: String text = getValue(); msg.mText = text; break; case Tags.EMAIL_MESSAGE_CLASS: String messageClass = getValue(); if (messageClass.equals("IPM.Schedule.Meeting.Request")) { msg.mFlags |= EmailContent.Message.FLAG_INCOMING_MEETING_INVITE; } else if (messageClass.equals("IPM.Schedule.Meeting.Canceled")) { msg.mFlags |= EmailContent.Message.FLAG_INCOMING_MEETING_CANCEL; } break; case Tags.EMAIL_MEETING_REQUEST: meetingRequestParser(msg); break; case Tags.EMAIL_THREAD_TOPIC: msg.mThreadTopic = getValue(); break; case Tags.RIGHTS_LICENSE: skipParser(tag); break; case Tags.EMAIL2_CONVERSATION_ID: msg.mServerConversationId = Base64.encodeToString(getValueBytes(), Base64.URL_SAFE); break; case Tags.EMAIL2_CONVERSATION_INDEX: // Ignore this byte array since we're not constructing a tree. getValueBytes(); break; case Tags.EMAIL2_LAST_VERB_EXECUTED: int val = getValueInt(); if (val == LAST_VERB_REPLY || val == LAST_VERB_REPLY_ALL) { // We aren't required to distinguish between reply and reply all here msg.mFlags |= EmailContent.Message.FLAG_REPLIED_TO; } else if (val == LAST_VERB_FORWARD) { msg.mFlags |= EmailContent.Message.FLAG_FORWARDED; } break; default: skipTag(); } } if (atts.size() > 0) { msg.mAttachments = atts; } if ((msg.mFlags & EmailContent.Message.FLAG_INCOMING_MEETING_MASK) != 0) { String text = TextUtilities.makeSnippetFromHtmlText(msg.mText != null ? msg.mText : msg.mHtml); if (TextUtils.isEmpty(text)) { // Create text for this invitation String meetingInfo = msg.mMeetingInfo; if (!TextUtils.isEmpty(meetingInfo)) { PackedString ps = new PackedString(meetingInfo); ContentValues values = new ContentValues(); putFromMeeting( ps, MeetingInfo.MEETING_LOCATION, values, CalendarContract.Events.EVENT_LOCATION); String dtstart = ps.get(MeetingInfo.MEETING_DTSTART); if (!TextUtils.isEmpty(dtstart)) { try { final long startTime = Utility.parseEmailDateTimeToMillis(dtstart); values.put(CalendarContract.Events.DTSTART, startTime); } catch (ParseException e) { LogUtils.w(TAG, "Parse error for MEETING_DTSTART tag.", e); } } putFromMeeting(ps, MeetingInfo.MEETING_ALL_DAY, values, CalendarContract.Events.ALL_DAY); msg.mText = CalendarUtilities.buildMessageTextFromEntityValues(mContext, values, null); msg.mHtml = Html.toHtml(new SpannedString(msg.mText)); } } } }
public static void sendMailImpl(Context context, long accountId) { /// M: We Can't send mail in low storage state @{ if (StorageLowState.checkIfStorageLow(context)) { LogUtils.e(Logging.LOG_TAG, "Can't send mail due to low storage"); return; } /// @} /** M: Get the sendable mails count of the account and notify this sending @{ */ final int count = getSendableMessageCount(context, accountId); LogUtils.logFeature(LogTag.SENDMAIL_TAG, "sendable message count [%d]", count); if (count <= 0) { return; } SendNotificationProxy.getInstance(context) .showSendingNotification(accountId, NotificationController.SEND_MAIL, count); /** @} */ final Account account = Account.restoreAccountWithId(context, accountId); TrafficStats.setThreadStatsTag(TrafficFlags.getSmtpFlags(context, account)); final NotificationController nc = NotificationController.getInstance(context); // 1. Loop through all messages in the account's outbox final long outboxId = Mailbox.findMailboxOfType(context, account.mId, Mailbox.TYPE_OUTBOX); if (outboxId == Mailbox.NO_MAILBOX) { return; } final ContentResolver resolver = context.getContentResolver(); final Cursor c = resolver.query( EmailContent.Message.CONTENT_URI, EmailContent.Message.ID_COLUMN_PROJECTION, EmailContent.Message.MAILBOX_KEY + "=?", new String[] {Long.toString(outboxId)}, null); try { // 2. exit early if (c.getCount() <= 0) { return; } final Sender sender = Sender.getInstance(context, account); final Store remoteStore = Store.getInstance(account, context); final ContentValues moveToSentValues; if (remoteStore.requireCopyMessageToSentFolder()) { Mailbox sentFolder = Mailbox.restoreMailboxOfType(context, accountId, Mailbox.TYPE_SENT); moveToSentValues = new ContentValues(); moveToSentValues.put(MessageColumns.MAILBOX_KEY, sentFolder.mId); } else { moveToSentValues = null; } // 3. loop through the available messages and send them /** M: mark should we cancel the Login Failed Notification. */ boolean shouldCancelNf = false; while (c.moveToNext()) { long messageId = -1; if (moveToSentValues != null) { moveToSentValues.remove(EmailContent.MessageColumns.FLAGS); } try { messageId = c.getLong(0); // Don't send messages with unloaded attachments if (Utility.hasUnloadedAttachments(context, messageId)) { LogUtils.logFeature( LogTag.SENDMAIL_TAG, "Can't send #" + messageId + "; unloaded attachments"); continue; } sender.sendMessage(messageId); } catch (MessagingException me) { LogUtils.logFeature( LogTag.SENDMAIL_TAG, "<<< Smtp send message failed id [%s], exception: %s", messageId, me); // report error for this message, but keep trying others if (me instanceof AuthenticationFailedException) { shouldCancelNf = false; nc.showLoginFailedNotification(account.mId); } /// M: One mail sent failed SendNotificationProxy.getInstance(context) .showSendingNotification(account.mId, NotificationController.SEND_FAILED, 1); continue; } /// M: One mail sent complete SendNotificationProxy.getInstance(context) .showSendingNotification(account.mId, NotificationController.SEND_COMPLETE, 1); // 4. move to sent, or delete final Uri syncedUri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId); // Delete all cached files AttachmentUtilities.deleteAllCachedAttachmentFiles(context, account.mId, messageId); if (moveToSentValues != null) { // If this is a forwarded message and it has attachments, delete them, as they // duplicate information found elsewhere (on the server). This saves storage. final EmailContent.Message msg = EmailContent.Message.restoreMessageWithId(context, messageId); if ((msg.mFlags & EmailContent.Message.FLAG_TYPE_FORWARD) != 0) { AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId, messageId); } /// M: un-mark sending status after sending final int flags = msg.mFlags & ~(EmailContent.Message.FLAG_TYPE_REPLY | EmailContent.Message.FLAG_TYPE_FORWARD | EmailContent.Message.FLAG_TYPE_REPLY_ALL | EmailContent.Message.FLAG_TYPE_ORIGINAL | EmailContent.Message.FLAG_STATUS_SENDING); moveToSentValues.put(EmailContent.MessageColumns.FLAGS, flags); resolver.update(syncedUri, moveToSentValues, null, null); } else { AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId, messageId); final Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, messageId); resolver.delete(uri, null, null); resolver.delete(syncedUri, null, null); } shouldCancelNf = true; } if (shouldCancelNf) { nc.cancelLoginFailedNotification(account.mId); } } catch (MessagingException me) { if (me instanceof AuthenticationFailedException) { nc.showLoginFailedNotification(account.mId); } /// M: All mails failed to be sent, caused by fail to get instance of store SendNotificationProxy.getInstance(context) .showSendingNotification(account.mId, NotificationController.SEND_FAILED, c.getCount()); } finally { c.close(); } }
@Override public void loadAttachment( final IEmailServiceCallback cb, final long attachmentId, final boolean background) throws RemoteException { /// M: We Can't load attachment in low storage state @{ if (StorageLowState.checkIfStorageLow(mContext)) { LogUtils.e(Logging.LOG_TAG, "Can't load attachment due to low storage"); cb.loadAttachmentStatus(0, attachmentId, EmailServiceStatus.SUCCESS, 0); return; } /// @} Folder remoteFolder = null; try { // 1. Check if the attachment is already here and return early in that case Attachment attachment = Attachment.restoreAttachmentWithId(mContext, attachmentId); if (attachment == null) { cb.loadAttachmentStatus(0, attachmentId, EmailServiceStatus.ATTACHMENT_NOT_FOUND, 0); return; } final long messageId = attachment.mMessageKey; final EmailContent.Message message = EmailContent.Message.restoreMessageWithId(mContext, attachment.mMessageKey); if (message == null) { cb.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.MESSAGE_NOT_FOUND, 0); return; } // If the message is loaded, just report that we're finished if (Utility.attachmentExists(mContext, attachment) && attachment.mUiState == UIProvider.AttachmentState.SAVED) { cb.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.SUCCESS, 0); return; } // Say we're starting... cb.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.IN_PROGRESS, 0); // 2. Open the remote folder. final Account account = Account.restoreAccountWithId(mContext, message.mAccountKey); Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey); if (mailbox.mType == Mailbox.TYPE_OUTBOX /// M: View an attachment which comes from refMessage need sourceKey to identify || mailbox.mType == Mailbox.TYPE_DRAFTS) { long sourceId = Utility.getFirstRowLong( mContext, Body.CONTENT_URI, new String[] {BodyColumns.SOURCE_MESSAGE_KEY}, BodyColumns.MESSAGE_KEY + "=?", new String[] {Long.toString(messageId)}, null, 0, -1L); if (sourceId != -1) { EmailContent.Message sourceMsg = EmailContent.Message.restoreMessageWithId(mContext, sourceId); if (sourceMsg != null) { mailbox = Mailbox.restoreMailboxWithId(mContext, sourceMsg.mMailboxKey); message.mServerId = sourceMsg.mServerId; } } } else if (mailbox.mType == Mailbox.TYPE_SEARCH && message.mMainMailboxKey != 0) { mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMainMailboxKey); } if (account == null || mailbox == null) { // If the account/mailbox are gone, just report success; the UI handles this cb.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.SUCCESS, 0); return; } TrafficStats.setThreadStatsTag(TrafficFlags.getAttachmentFlags(mContext, account)); final Store remoteStore = Store.getInstance(account, mContext); remoteFolder = remoteStore.getFolder(mailbox.mServerId); remoteFolder.open(OpenMode.READ_WRITE); // 3. Generate a shell message in which to retrieve the attachment, // and a shell BodyPart for the attachment. Then glue them together. final Message storeMessage = remoteFolder.createMessage(message.mServerId); final MimeBodyPart storePart = new MimeBodyPart(); storePart.setSize((int) attachment.mSize); storePart.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, attachment.mLocation); storePart.setHeader( MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\n name=\"%s\"", attachment.mMimeType, attachment.mFileName)); // TODO is this always true for attachments? I think we dropped the // true encoding along the way /// M: set encoding type according to data base record. String encoding = attachment.mEncoding; if (TextUtils.isEmpty(encoding)) { encoding = "base64"; } storePart.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, encoding); final MimeMultipart multipart = new MimeMultipart(); multipart.setSubType("mixed"); multipart.addBodyPart(storePart); storeMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "multipart/mixed"); storeMessage.setBody(multipart); // 4. Now ask for the attachment to be fetched final FetchProfile fp = new FetchProfile(); fp.add(storePart); remoteFolder.fetch( new Message[] {storeMessage}, fp, new MessageRetrievalListenerBridge(messageId, attachmentId, cb)); // If we failed to load the attachment, throw an Exception here, so that // AttachmentDownloadService knows that we failed if (storePart.getBody() == null) { throw new MessagingException("Attachment not loaded."); } // Save the attachment to wherever it's going AttachmentUtilities.saveAttachment( mContext, storePart.getBody().getInputStream(), attachment); // 6. Report success cb.loadAttachmentStatus(messageId, attachmentId, EmailServiceStatus.SUCCESS, 0); } catch (MessagingException me) { LogUtils.i(Logging.LOG_TAG, me, "Error loading attachment"); final ContentValues cv = new ContentValues(1); cv.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.FAILED); final Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachmentId); mContext.getContentResolver().update(uri, cv, null, null); cb.loadAttachmentStatus(0, attachmentId, EmailServiceStatus.CONNECTION_ERROR, 0); } finally { if (remoteFolder != null) { remoteFolder.close(false); } } }
/** * Copy field-by-field from a "store" message to a "provider" message * * @param message The message we've just downloaded (must be a MimeMessage) * @param localMessage The message we'd like to write into the DB * @return true if dirty (changes were made) */ public static boolean updateMessageFields( final EmailContent.Message localMessage, final Message message, final long accountId, final long mailboxId) throws MessagingException { final Address[] from = message.getFrom(); final Address[] to = message.getRecipients(Message.RecipientType.TO); final Address[] cc = message.getRecipients(Message.RecipientType.CC); final Address[] bcc = message.getRecipients(Message.RecipientType.BCC); final Address[] replyTo = message.getReplyTo(); final String subject = message.getSubject(); final Date sentDate = message.getSentDate(); final Date internalDate = message.getInternalDate(); if (from != null && from.length > 0) { localMessage.mDisplayName = from[0].toFriendly(); } if (sentDate != null) { localMessage.mTimeStamp = sentDate.getTime(); } else if (internalDate != null) { LogUtils.w(Logging.LOG_TAG, "No sentDate, falling back to internalDate"); localMessage.mTimeStamp = internalDate.getTime(); } if (subject != null) { localMessage.mSubject = subject; } localMessage.mFlagRead = message.isSet(Flag.SEEN); if (message.isSet(Flag.ANSWERED)) { localMessage.mFlags |= EmailContent.Message.FLAG_REPLIED_TO; } // Keep the message in the "unloaded" state until it has (at least) a display name. // This prevents early flickering of empty messages in POP download. if (localMessage.mFlagLoaded != EmailContent.Message.FLAG_LOADED_COMPLETE) { if (localMessage.mDisplayName == null || "".equals(localMessage.mDisplayName)) { localMessage.mFlagLoaded = EmailContent.Message.FLAG_LOADED_UNLOADED; } else { localMessage.mFlagLoaded = EmailContent.Message.FLAG_LOADED_PARTIAL; } } localMessage.mFlagFavorite = message.isSet(Flag.FLAGGED); // public boolean mFlagAttachment = false; // public int mFlags = 0; localMessage.mServerId = message.getUid(); if (internalDate != null) { localMessage.mServerTimeStamp = internalDate.getTime(); } // public String mClientId; // Only replace the local message-id if a new one was found. This is seen in some ISP's // which may deliver messages w/o a message-id header. final String messageId = message.getMessageId(); if (messageId != null) { localMessage.mMessageId = messageId; } // public long mBodyKey; localMessage.mMailboxKey = mailboxId; localMessage.mAccountKey = accountId; if (from != null && from.length > 0) { localMessage.mFrom = Address.toString(from); } localMessage.mTo = Address.toString(to); localMessage.mCc = Address.toString(cc); localMessage.mBcc = Address.toString(bcc); localMessage.mReplyTo = Address.toString(replyTo); // public String mText; // public String mHtml; // public String mTextReply; // public String mHtmlReply; // // Can be used while building messages, but is NOT saved by the Provider // transient public ArrayList<Attachment> mAttachments = null; return true; }