@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; }
public void fetchInternal(Message[] messages, FetchProfile fp, MessageRetrievalListener listener) throws MessagingException { if (messages.length == 0) { return; } checkOpen(); HashMap<String, Message> messageMap = new HashMap<String, Message>(); for (Message m : messages) { messageMap.put(m.getUid(), m); } /* * Figure out what command we are going to run: * FLAGS - UID FETCH (FLAGS) * ENVELOPE - UID FETCH (INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[ * HEADER.FIELDS (date subject from content-type to cc)]) * STRUCTURE - UID FETCH (BODYSTRUCTURE) * BODY_SANE - UID FETCH (BODY.PEEK[]<0.N>) where N = max bytes returned * BODY - UID FETCH (BODY.PEEK[]) * Part - UID FETCH (BODY.PEEK[ID]) where ID = mime part ID */ final LinkedHashSet<String> fetchFields = new LinkedHashSet<String>(); fetchFields.add(ImapConstants.UID); if (fp.contains(FetchProfile.Item.FLAGS)) { fetchFields.add(ImapConstants.FLAGS); } if (fp.contains(FetchProfile.Item.ENVELOPE)) { fetchFields.add(ImapConstants.INTERNALDATE); fetchFields.add(ImapConstants.RFC822_SIZE); fetchFields.add(ImapConstants.FETCH_FIELD_HEADERS); } if (fp.contains(FetchProfile.Item.STRUCTURE)) { fetchFields.add(ImapConstants.BODYSTRUCTURE); } if (fp.contains(FetchProfile.Item.BODY_SANE)) { fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_SANE); } if (fp.contains(FetchProfile.Item.BODY)) { fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK); } final Part fetchPart = fp.getFirstPart(); if (fetchPart != null) { String[] partIds = fetchPart.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA); if (partIds != null) { fetchFields.add( ImapConstants.FETCH_FIELD_BODY_PEEK_BARE + "[" + partIds[0] + "]" + (mFetchSize > 0 ? String.format("<0.%d>", mFetchSize) : "")); } } try { mConnection.sendCommand( String.format( ImapConstants.UID_FETCH + " %s (%s)", ImapStore.joinMessageUids(messages), Utility.combine(fetchFields.toArray(new String[fetchFields.size()]), ' ')), false); ImapResponse response; int messageNumber = 0; do { response = null; try { response = mConnection.readResponse(listener); if (!response.isDataResponse(1, ImapConstants.FETCH)) { continue; // Ignore } final ImapList fetchList = response.getListOrEmpty(2); final String uid = fetchList.getKeyedStringOrEmpty(ImapConstants.UID).getString(); if (TextUtils.isEmpty(uid)) continue; ImapMessage message = (ImapMessage) messageMap.get(uid); if (message == null) continue; if (fp.contains(FetchProfile.Item.FLAGS)) { final ImapList flags = fetchList.getKeyedListOrEmpty(ImapConstants.FLAGS); for (int i = 0, count = flags.size(); i < count; i++) { final ImapString flag = flags.getStringOrEmpty(i); if (flag.is(ImapConstants.FLAG_DELETED)) { message.setFlagInternal(Flag.DELETED, true); } else if (flag.is(ImapConstants.FLAG_ANSWERED)) { message.setFlagInternal(Flag.ANSWERED, true); } else if (flag.is(ImapConstants.FLAG_SEEN)) { message.setFlagInternal(Flag.SEEN, true); } else if (flag.is(ImapConstants.FLAG_FLAGGED)) { message.setFlagInternal(Flag.FLAGGED, true); } } } if (fp.contains(FetchProfile.Item.ENVELOPE)) { final Date internalDate = fetchList.getKeyedStringOrEmpty(ImapConstants.INTERNALDATE).getDateOrNull(); final int size = fetchList.getKeyedStringOrEmpty(ImapConstants.RFC822_SIZE).getNumberOrZero(); final InputStream header = fetchList .getKeyedStringOrEmpty(ImapConstants.BODY_BRACKET_HEADER, true) .getAsStream(); message.setInternalDate(internalDate); message.setSize(size); message.parse(header); } if (fp.contains(FetchProfile.Item.STRUCTURE)) { ImapList bs = fetchList.getKeyedListOrEmpty(ImapConstants.BODYSTRUCTURE); if (!bs.isEmpty()) { try { parseBodyStructure(bs, message, ImapConstants.TEXT); } catch (MessagingException e) { if (Logging.LOGD) { Log.v(Logging.LOG_TAG, "Error handling message", e); } message.setBody(null); } } } if (fp.contains(FetchProfile.Item.BODY) || fp.contains(FetchProfile.Item.BODY_SANE)) { // Body is keyed by "BODY[]...". // Previously used "BODY[..." but this can be confused with "BODY[HEADER..." // TODO Should we accept "RFC822" as well?? ImapString body = fetchList.getKeyedStringOrEmpty("BODY[]", true); String bodyText = body.getString(); InputStream bodyStream = body.getAsStream(); message.parse(bodyStream); } if (fetchPart != null && fetchPart.getSize() > 0) { InputStream bodyStream = fetchList.getKeyedStringOrEmpty("BODY[", true).getAsStream(); String contentType = fetchPart.getContentType(); String[] encodingHeader = fetchPart.getHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING); String contentTransferEncoding = null; if (encodingHeader != null) { contentTransferEncoding = encodingHeader[0]; } // TODO Don't create 2 temp files. // decodeBody creates BinaryTempFileBody, but we could avoid this // if we implement ImapStringBody. // (We'll need to share a temp file. Protect it with a ref-count.) fetchPart.setBody( decodeBody(bodyStream, contentTransferEncoding, fetchPart.getSize(), listener)); } if (listener != null) { listener.messageRetrieved(message); } } finally { destroyResponses(); } } while (!response.isTagged()); } catch (IOException ioe) { throw ioExceptionHandler(mConnection, ioe); } }
/** * Appends the given messages to the selected folder. This implementation also determines the new * UID of the given message on the IMAP server and sets the Message's UID to the new server UID. */ @Override public void appendMessages(Message[] messages) throws MessagingException { checkOpen(); try { for (Message message : messages) { // Create output count CountingOutputStream out = new CountingOutputStream(); EOLConvertingOutputStream eolOut = new EOLConvertingOutputStream(out); message.writeTo(eolOut); eolOut.flush(); // Create flag list (most often this will be "\SEEN") String flagList = ""; Flag[] flags = message.getFlags(); if (flags.length > 0) { StringBuilder sb = new StringBuilder(); for (int i = 0, count = flags.length; i < count; i++) { Flag flag = flags[i]; if (flag == Flag.SEEN) { sb.append(" " + ImapConstants.FLAG_SEEN); } else if (flag == Flag.FLAGGED) { sb.append(" " + ImapConstants.FLAG_FLAGGED); } } if (sb.length() > 0) { flagList = sb.substring(1); } } mConnection.sendCommand( String.format( ImapConstants.APPEND + " \"%s\" (%s) {%d}", ImapStore.encodeFolderName(mName, mStore.mPathPrefix), flagList, out.getCount()), false); ImapResponse response; do { response = mConnection.readResponse(); if (response.isContinuationRequest()) { eolOut = new EOLConvertingOutputStream(mConnection.mTransport.getOutputStream()); message.writeTo(eolOut); eolOut.write('\r'); eolOut.write('\n'); eolOut.flush(); } else if (!response.isTagged()) { handleUntaggedResponse(response); } } while (!response.isTagged()); // TODO Why not check the response? /* * Try to recover the UID of the message from an APPENDUID response. * e.g. 11 OK [APPENDUID 2 238268] APPEND completed */ final ImapList appendList = response.getListOrEmpty(1); if ((appendList.size() >= 3) && appendList.is(0, ImapConstants.APPENDUID)) { String serverUid = appendList.getStringOrEmpty(2).getString(); if (!TextUtils.isEmpty(serverUid)) { message.setUid(serverUid); continue; } } /* * Try to find the UID of the message we just appended using the * Message-ID header. If there are more than one response, take the * last one, as it's most likely the newest (the one we just uploaded). */ String messageId = message.getMessageId(); if (messageId == null || messageId.length() == 0) { continue; } // Most servers don't care about parenthesis in the search query [and, some // fail to work if they are used] String[] uids = searchForUids(String.format("HEADER MESSAGE-ID %s", messageId)); if (uids.length > 0) { message.setUid(uids[0]); } // However, there's at least one server [AOL] that fails to work unless there // are parenthesis, so, try this as a last resort uids = searchForUids(String.format("(HEADER MESSAGE-ID %s)", messageId)); if (uids.length > 0) { message.setUid(uids[0]); } } } catch (IOException ioe) { throw ioExceptionHandler(mConnection, ioe); } finally { destroyResponses(); } }
@Override public void copyMessages(Message[] messages, Folder folder, MessageUpdateCallbacks callbacks) throws MessagingException { checkOpen(); try { List<ImapResponse> responseList = mConnection.executeSimpleCommand( String.format( ImapConstants.UID_COPY + " %s \"%s\"", ImapStore.joinMessageUids(messages), ImapStore.encodeFolderName(folder.getName(), mStore.mPathPrefix))); // Build a message map for faster UID matching HashMap<String, Message> messageMap = new HashMap<String, Message>(); boolean handledUidPlus = false; for (Message m : messages) { messageMap.put(m.getUid(), m); } // Process response to get the new UIDs for (ImapResponse response : responseList) { // All "BAD" responses are bad. Only "NO", tagged responses are bad. if (response.isBad() || (response.isNo() && response.isTagged())) { String responseText = response.getStatusResponseTextOrEmpty().getString(); throw new MessagingException(responseText); } // Skip untagged responses; they're just status if (!response.isTagged()) { continue; } // No callback provided to report of UID changes; nothing more to do here // NOTE: We check this here to catch any server errors if (callbacks == null) { continue; } ImapList copyResponse = response.getListOrEmpty(1); String responseCode = copyResponse.getStringOrEmpty(0).getString(); if (ImapConstants.COPYUID.equals(responseCode)) { handledUidPlus = true; String origIdSet = copyResponse.getStringOrEmpty(2).getString(); String newIdSet = copyResponse.getStringOrEmpty(3).getString(); String[] origIdArray = ImapUtility.getImapSequenceValues(origIdSet); String[] newIdArray = ImapUtility.getImapSequenceValues(newIdSet); // There has to be a 1:1 mapping between old and new IDs if (origIdArray.length != newIdArray.length) { throw new MessagingException( "Set length mis-match; orig IDs \"" + origIdSet + "\" new IDs \"" + newIdSet + "\""); } for (int i = 0; i < origIdArray.length; i++) { final String id = origIdArray[i]; final Message m = messageMap.get(id); if (m != null) { callbacks.onMessageUidChange(m, newIdArray[i]); } } } } // If the server doesn't support UIDPLUS, try a different way to get the new UID(s) if (callbacks != null && !handledUidPlus) { ImapFolder newFolder = (ImapFolder) folder; try { // Temporarily select the destination folder newFolder.open(OpenMode.READ_WRITE); // Do the search(es) ... for (Message m : messages) { String searchString = "HEADER Message-Id \"" + m.getMessageId() + "\""; String[] newIdArray = newFolder.searchForUids(searchString); if (newIdArray.length == 1) { callbacks.onMessageUidChange(m, newIdArray[0]); } } } catch (MessagingException e) { // Log, but, don't abort; failures here don't need to be propagated Log.d(Logging.LOG_TAG, "Failed to find message", e); } finally { newFolder.close(false); } // Re-select the original folder doSelect(); } } catch (IOException ioe) { throw ioExceptionHandler(mConnection, ioe); } finally { destroyResponses(); } }