diff options
| author | test@dangofactory.cos <you@example.com> | 2026-08-20 16:57:18 +0000 |
|---|---|---|
| committer | test@dangofactory.cos <you@example.com> | 2026-08-20 16:57:18 +0000 |
| commit | 50ce5d5e6cd0d720ff437d35eace545816848428 (patch) | |
| tree | b59af206a36265cf081945e434900b32b0c43edd /backend/pop3/src/main/java | |
| download | monocles_mail-50ce5d5e6cd0d720ff437d35eace545816848428.tar.gz monocles_mail-50ce5d5e6cd0d720ff437d35eace545816848428.tar.bz2 monocles_mail-50ce5d5e6cd0d720ff437d35eace545816848428.zip | |
Inital
Diffstat (limited to 'backend/pop3/src/main/java')
5 files changed, 826 insertions, 0 deletions
diff --git a/backend/pop3/src/main/java/com/fsck/k9/backend/pop3/CommandDownloadMessage.kt b/backend/pop3/src/main/java/com/fsck/k9/backend/pop3/CommandDownloadMessage.kt new file mode 100644 index 0000000..d8f950a --- /dev/null +++ b/backend/pop3/src/main/java/com/fsck/k9/backend/pop3/CommandDownloadMessage.kt @@ -0,0 +1,26 @@ +package com.fsck.k9.backend.pop3 + +import com.fsck.k9.backend.api.BackendStorage +import com.fsck.k9.mail.FetchProfile.Item.BODY +import com.fsck.k9.mail.FetchProfile.Item.FLAGS +import com.fsck.k9.mail.MessageDownloadState +import com.fsck.k9.mail.helper.fetchProfileOf +import com.fsck.k9.mail.store.pop3.Pop3Store + +internal class CommandDownloadMessage(private val backendStorage: BackendStorage, private val pop3Store: Pop3Store) { + + fun downloadCompleteMessage(folderServerId: String, messageServerId: String) { + val folder = pop3Store.getFolder(folderServerId) + try { + folder.open() + + val message = folder.getMessage(messageServerId) + folder.fetch(listOf(message), fetchProfileOf(FLAGS, BODY), null, 0) + + val backendFolder = backendStorage.getFolder(folderServerId) + backendFolder.saveMessage(message, MessageDownloadState.FULL) + } finally { + folder.close() + } + } +} diff --git a/backend/pop3/src/main/java/com/fsck/k9/backend/pop3/CommandRefreshFolderList.kt b/backend/pop3/src/main/java/com/fsck/k9/backend/pop3/CommandRefreshFolderList.kt new file mode 100644 index 0000000..493e22d --- /dev/null +++ b/backend/pop3/src/main/java/com/fsck/k9/backend/pop3/CommandRefreshFolderList.kt @@ -0,0 +1,19 @@ +package com.fsck.k9.backend.pop3 + +import com.fsck.k9.backend.api.BackendStorage +import com.fsck.k9.backend.api.FolderInfo +import com.fsck.k9.backend.api.updateFolders +import com.fsck.k9.mail.FolderType +import com.fsck.k9.mail.store.pop3.Pop3Folder + +internal class CommandRefreshFolderList(private val backendStorage: BackendStorage) { + fun refreshFolderList() { + val folderServerIds = backendStorage.getFolderServerIds() + if (Pop3Folder.INBOX !in folderServerIds) { + backendStorage.updateFolders { + val inbox = FolderInfo(Pop3Folder.INBOX, Pop3Folder.INBOX, FolderType.INBOX) + createFolders(listOf(inbox)) + } + } + } +} diff --git a/backend/pop3/src/main/java/com/fsck/k9/backend/pop3/CommandSetFlag.java b/backend/pop3/src/main/java/com/fsck/k9/backend/pop3/CommandSetFlag.java new file mode 100644 index 0000000..8d0f06b --- /dev/null +++ b/backend/pop3/src/main/java/com/fsck/k9/backend/pop3/CommandSetFlag.java @@ -0,0 +1,47 @@ +package com.fsck.k9.backend.pop3; + + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.fsck.k9.mail.Flag; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.store.pop3.Pop3Folder; +import com.fsck.k9.mail.store.pop3.Pop3Message; +import com.fsck.k9.mail.store.pop3.Pop3Store; +import org.jetbrains.annotations.NotNull; + + +class CommandSetFlag { + private final Pop3Store pop3Store; + + + CommandSetFlag(Pop3Store pop3Store) { + this.pop3Store = pop3Store; + } + + void setFlag(@NotNull String folderServerId, @NotNull List<String> messageServerIds, @NotNull Flag flag, + boolean newState) throws MessagingException { + + Pop3Folder remoteFolder = pop3Store.getFolder(folderServerId); + if (!remoteFolder.isFlagSupported(flag)) { + return; + } + + try { + remoteFolder.open(); + List<Pop3Message> messages = new ArrayList<>(); + for (String uid : messageServerIds) { + messages.add(remoteFolder.getMessage(uid)); + } + + if (messages.isEmpty()) { + return; + } + remoteFolder.setFlags(messages, Collections.singleton(flag), newState); + } finally { + remoteFolder.close(); + } + } +} diff --git a/backend/pop3/src/main/java/com/fsck/k9/backend/pop3/Pop3Backend.kt b/backend/pop3/src/main/java/com/fsck/k9/backend/pop3/Pop3Backend.kt new file mode 100644 index 0000000..f3974ae --- /dev/null +++ b/backend/pop3/src/main/java/com/fsck/k9/backend/pop3/Pop3Backend.kt @@ -0,0 +1,141 @@ +package com.fsck.k9.backend.pop3 + +import com.fsck.k9.backend.api.Backend +import com.fsck.k9.backend.api.BackendPusher +import com.fsck.k9.backend.api.BackendPusherCallback +import com.fsck.k9.backend.api.BackendStorage +import com.fsck.k9.backend.api.SyncConfig +import com.fsck.k9.backend.api.SyncListener +import com.fsck.k9.mail.BodyFactory +import com.fsck.k9.mail.Flag +import com.fsck.k9.mail.Message +import com.fsck.k9.mail.Part +import com.fsck.k9.mail.store.pop3.Pop3Store +import com.fsck.k9.mail.transport.smtp.SmtpTransport + +class Pop3Backend( + accountName: String, + backendStorage: BackendStorage, + private val pop3Store: Pop3Store, + private val smtpTransport: SmtpTransport +) : Backend { + private val pop3Sync: Pop3Sync = Pop3Sync(accountName, backendStorage, pop3Store) + private val commandRefreshFolderList = CommandRefreshFolderList(backendStorage) + private val commandSetFlag = CommandSetFlag(pop3Store) + private val commandDownloadMessage = CommandDownloadMessage(backendStorage, pop3Store) + + override val supportsFlags = false + override val supportsExpunge = false + override val supportsMove = false + override val supportsCopy = false + override val supportsUpload = false + override val supportsTrashFolder = false + override val supportsSearchByDate = false + override val isPushCapable = false + + override fun refreshFolderList() { + commandRefreshFolderList.refreshFolderList() + } + + override fun sync(folderServerId: String, syncConfig: SyncConfig, listener: SyncListener) { + pop3Sync.sync(folderServerId, syncConfig, listener) + } + + override fun downloadMessage(syncConfig: SyncConfig, folderServerId: String, messageServerId: String) { + throw UnsupportedOperationException("not implemented") + } + + override fun downloadMessageStructure(folderServerId: String, messageServerId: String) { + throw UnsupportedOperationException("not implemented") + } + + override fun downloadCompleteMessage(folderServerId: String, messageServerId: String) { + commandDownloadMessage.downloadCompleteMessage(folderServerId, messageServerId) + } + + override fun setFlag(folderServerId: String, messageServerIds: List<String>, flag: Flag, newState: Boolean) { + commandSetFlag.setFlag(folderServerId, messageServerIds, flag, newState) + } + + override fun markAllAsRead(folderServerId: String) { + throw UnsupportedOperationException("not supported") + } + + override fun expunge(folderServerId: String) { + throw UnsupportedOperationException("not supported") + } + + override fun expungeMessages(folderServerId: String, messageServerIds: List<String>) { + throw UnsupportedOperationException("not supported") + } + + override fun deleteMessages(folderServerId: String, messageServerIds: List<String>) { + commandSetFlag.setFlag(folderServerId, messageServerIds, Flag.DELETED, true) + } + + override fun deleteAllMessages(folderServerId: String) { + throw UnsupportedOperationException("not supported") + } + + override fun moveMessages( + sourceFolderServerId: String, + targetFolderServerId: String, + messageServerIds: List<String> + ): Map<String, String>? { + throw UnsupportedOperationException("not supported") + } + + override fun copyMessages( + sourceFolderServerId: String, + targetFolderServerId: String, + messageServerIds: List<String> + ): Map<String, String>? { + throw UnsupportedOperationException("not supported") + } + + override fun moveMessagesAndMarkAsRead( + sourceFolderServerId: String, + targetFolderServerId: String, + messageServerIds: List<String> + ): Map<String, String>? { + throw UnsupportedOperationException("not supported") + } + + override fun search( + folderServerId: String, + query: String?, + requiredFlags: Set<Flag>?, + forbiddenFlags: Set<Flag>?, + performFullTextSearch: Boolean + ): List<String> { + throw UnsupportedOperationException("not supported") + } + + override fun fetchPart(folderServerId: String, messageServerId: String, part: Part, bodyFactory: BodyFactory) { + throw UnsupportedOperationException("not supported") + } + + override fun findByMessageId(folderServerId: String, messageId: String): String? { + return null + } + + override fun uploadMessage(folderServerId: String, message: Message): String? { + throw UnsupportedOperationException("not supported") + } + + override fun checkIncomingServerSettings() { + pop3Store.checkSettings() + } + + override fun sendMessage(message: Message) { + smtpTransport.sendMessage(message) + } + + override fun checkOutgoingServerSettings() { + smtpTransport.checkSettings() + } + + override fun createPusher(callback: BackendPusherCallback): BackendPusher { + throw UnsupportedOperationException("not implemented") + } +} diff --git a/backend/pop3/src/main/java/com/fsck/k9/backend/pop3/Pop3Sync.java b/backend/pop3/src/main/java/com/fsck/k9/backend/pop3/Pop3Sync.java new file mode 100644 index 0000000..8eae32f --- /dev/null +++ b/backend/pop3/src/main/java/com/fsck/k9/backend/pop3/Pop3Sync.java @@ -0,0 +1,593 @@ +package com.fsck.k9.backend.pop3; + + +import com.fsck.k9.backend.api.BackendFolder; +import com.fsck.k9.backend.api.BackendFolder.MoreMessages; +import com.fsck.k9.backend.api.BackendStorage; +import com.fsck.k9.backend.api.SyncConfig; +import com.fsck.k9.backend.api.SyncListener; +import com.fsck.k9.helper.ExceptionHelper; +import com.fsck.k9.logging.Timber; +import com.fsck.k9.mail.AuthenticationFailedException; +import com.fsck.k9.mail.FetchProfile; +import com.fsck.k9.mail.Flag; +import com.fsck.k9.mail.MessageDownloadState; +import com.fsck.k9.mail.MessageRetrievalListener; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.store.pop3.Pop3Folder; +import com.fsck.k9.mail.store.pop3.Pop3Message; +import com.fsck.k9.mail.store.pop3.Pop3Store; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + + +class Pop3Sync { + private static final String EXTRA_LATEST_OLD_MESSAGE_SEEN_TIME = "latestOldMessageSeenTime"; + + private final String accountName; + private final BackendStorage backendStorage; + private final Pop3Store remoteStore; + + + Pop3Sync(String accountName, BackendStorage backendStorage, Pop3Store pop3Store) { + this.accountName = accountName; + this.backendStorage = backendStorage; + this.remoteStore = pop3Store; + } + + void sync(String folder, SyncConfig syncConfig, SyncListener listener) { + synchronizeMailboxSynchronous(folder, syncConfig, listener); + } + + void synchronizeMailboxSynchronous(String folder, SyncConfig syncConfig, SyncListener listener) { + Pop3Folder remoteFolder = null; + + Timber.i("Synchronizing folder %s:%s", accountName, folder); + + BackendFolder backendFolder = null; + try { + Timber.d("SYNC: About to process pending commands for account %s", accountName); + + Timber.v("SYNC: About to get local folder %s", folder); + backendFolder = backendStorage.getFolder(folder); + + listener.syncStarted(folder); + + /* + * Get the message list from the local store and create an index of + * the uids within the list. + */ + + Map<String, Long> localUidMap = backendFolder.getAllMessagesAndEffectiveDates(); + + Timber.v("SYNC: About to get remote folder %s", folder); + remoteFolder = remoteStore.getFolder(folder); + + /* + * Synchronization process: + * + Open the folder + Upload any local messages that are marked as PENDING_UPLOAD (Drafts, Sent, Trash) + Get the message count + Get the list of the newest K9.DEFAULT_VISIBLE_LIMIT messages + getMessages(messageCount - K9.DEFAULT_VISIBLE_LIMIT, messageCount) + See if we have each message locally, if not fetch it's flags and envelope + Get and update the unread count for the folder + Update the remote flags of any messages we have locally with an internal date newer than the remote message. + Get the current flags for any messages we have locally but did not just download + Update local flags + For any message we have locally but not remotely, delete the local message to keep cache clean. + Download larger parts of any new messages. + (Optional) Download small attachments in the background. + */ + + /* + * Open the remote folder. This pre-loads certain metadata like message count. + */ + Timber.v("SYNC: About to open remote folder %s", folder); + + remoteFolder.open(); + + listener.syncAuthenticationSuccess(); + + /* + * Get the remote message count. + */ + int remoteMessageCount = remoteFolder.getMessageCount(); + + int visibleLimit = backendFolder.getVisibleLimit(); + + if (visibleLimit < 0) { + visibleLimit = syncConfig.getDefaultVisibleLimit(); + } + + final List<Pop3Message> remoteMessages = new ArrayList<>(); + Map<String, Pop3Message> remoteUidMap = new HashMap<>(); + + Timber.v("SYNC: Remote message count for folder %s is %d", folder, remoteMessageCount); + + final Date earliestDate = syncConfig.getEarliestPollDate(); + long earliestTimestamp = earliestDate != null ? earliestDate.getTime() : 0L; + + + int remoteStart = 1; + if (remoteMessageCount > 0) { + /* Message numbers start at 1. */ + if (visibleLimit > 0) { + remoteStart = Math.max(0, remoteMessageCount - visibleLimit) + 1; + } else { + remoteStart = 1; + } + + Timber.v("SYNC: About to get messages %d through %d for folder %s", + remoteStart, remoteMessageCount, folder); + + final AtomicInteger headerProgress = new AtomicInteger(0); + listener.syncHeadersStarted(folder); + + + List<Pop3Message> remoteMessageArray = + remoteFolder.getMessages(remoteStart, remoteMessageCount, null); + + int messageCount = remoteMessageArray.size(); + + for (Pop3Message thisMess : remoteMessageArray) { + headerProgress.incrementAndGet(); + listener.syncHeadersProgress(folder, headerProgress.get(), messageCount); + + Long localMessageTimestamp = localUidMap.get(thisMess.getUid()); + if (localMessageTimestamp == null || localMessageTimestamp >= earliestTimestamp) { + remoteMessages.add(thisMess); + remoteUidMap.put(thisMess.getUid(), thisMess); + } + } + + Timber.v("SYNC: Got %d messages for folder %s", remoteUidMap.size(), folder); + + listener.syncHeadersFinished(folder, headerProgress.get(), remoteUidMap.size()); + } else if (remoteMessageCount < 0) { + throw new Exception("Message count " + remoteMessageCount + " for folder " + folder); + } + + /* + * Remove any messages that are in the local store but no longer on the remote store or are too old + */ + MoreMessages moreMessages = backendFolder.getMoreMessages(); + if (syncConfig.getSyncRemoteDeletions()) { + List<String> destroyMessageUids = new ArrayList<>(); + for (String localMessageUid : localUidMap.keySet()) { + if (remoteUidMap.get(localMessageUid) == null) { + destroyMessageUids.add(localMessageUid); + } + } + + if (!destroyMessageUids.isEmpty()) { + moreMessages = MoreMessages.UNKNOWN; + + backendFolder.destroyMessages(destroyMessageUids); + for (String uid : destroyMessageUids) { + listener.syncRemovedMessage(folder, uid); + } + } + + } + // noinspection UnusedAssignment, free memory early? (better break up the method!) + localUidMap = null; + + if (moreMessages == MoreMessages.UNKNOWN) { + updateMoreMessages(remoteFolder, backendFolder, remoteStart); + } + + /* + * Now we download the actual content of messages. + */ + int newMessages = downloadMessages(syncConfig, remoteFolder, backendFolder, remoteMessages, + listener); + + listener.folderStatusChanged(folder); + + /* Notify listeners that we're finally done. */ + + backendFolder.setLastChecked(System.currentTimeMillis()); + backendFolder.setStatus(null); + + Timber.d("Done synchronizing folder %s:%s @ %tc with %d new messages", + accountName, + folder, + System.currentTimeMillis(), + newMessages); + + listener.syncFinished(folder); + + Timber.i("Done synchronizing folder %s:%s", accountName, folder); + + } catch (AuthenticationFailedException e) { + listener.syncFailed(folder, "Authentication failure", e); + } catch (Exception e) { + Timber.e(e, "synchronizeMailbox"); + // If we don't set the last checked, it can try too often during + // failure conditions + String rootMessage = ExceptionHelper.getRootCauseMessage(e); + if (backendFolder != null) { + try { + backendFolder.setStatus(rootMessage); + backendFolder.setLastChecked(System.currentTimeMillis()); + } catch (Exception e1) { + Timber.e(e1, "Could not set last checked on folder %s:%s", accountName, folder); + } + } + + listener.syncFailed(folder, rootMessage, e); + + Timber.e("Failed synchronizing folder %s:%s @ %tc", accountName, folder, + System.currentTimeMillis()); + + } finally { + if (remoteFolder != null) { + remoteFolder.close(); + } + } + } + + private void updateMoreMessages(Pop3Folder remoteFolder, BackendFolder backendFolder, + int remoteStart) { + + if (remoteStart == 1) { + backendFolder.setMoreMessages(MoreMessages.FALSE); + } else { + boolean moreMessagesAvailable = remoteFolder.areMoreMessagesAvailable(remoteStart); + + MoreMessages newMoreMessages = (moreMessagesAvailable) ? MoreMessages.TRUE : MoreMessages.FALSE; + backendFolder.setMoreMessages(newMoreMessages); + } + } + + private int downloadMessages(final SyncConfig syncConfig, final Pop3Folder remoteFolder, + final BackendFolder backendFolder, List<Pop3Message> inputMessages, + final SyncListener listener) throws MessagingException { + + final Date earliestDate = syncConfig.getEarliestPollDate(); + Date downloadStarted = new Date(); // now + + if (earliestDate != null) { + Timber.d("Only syncing messages after %s", earliestDate); + } + final String folder = remoteFolder.getServerId(); + + List<Pop3Message> syncFlagMessages = new ArrayList<>(); + List<Pop3Message> unsyncedMessages = new ArrayList<>(); + final AtomicInteger newMessages = new AtomicInteger(0); + + List<Pop3Message> messages = new ArrayList<>(inputMessages); + + for (Pop3Message message : messages) { + evaluateMessageForDownload(message, folder, backendFolder, unsyncedMessages, syncFlagMessages, listener); + } + + final AtomicInteger progress = new AtomicInteger(0); + final int todo = unsyncedMessages.size() + syncFlagMessages.size(); + listener.syncProgress(folder, progress.get(), todo); + + Timber.d("SYNC: Have %d unsynced messages", unsyncedMessages.size()); + + messages.clear(); + final List<Pop3Message> largeMessages = new ArrayList<>(); + final List<Pop3Message> smallMessages = new ArrayList<>(); + if (!unsyncedMessages.isEmpty()) { + int visibleLimit = backendFolder.getVisibleLimit(); + int listSize = unsyncedMessages.size(); + + if ((visibleLimit > 0) && (listSize > visibleLimit)) { + unsyncedMessages = unsyncedMessages.subList(0, visibleLimit); + } + + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.ENVELOPE); + + Timber.d("SYNC: About to fetch %d unsynced messages for folder %s", unsyncedMessages.size(), folder); + + fetchUnsyncedMessages(syncConfig, remoteFolder, unsyncedMessages, smallMessages, largeMessages, progress, + todo, fp, listener); + + Timber.d("SYNC: Synced unsynced messages for folder %s", folder); + } + + Timber.d("SYNC: Have %d large messages and %d small messages out of %d unsynced messages", + largeMessages.size(), smallMessages.size(), unsyncedMessages.size()); + + unsyncedMessages.clear(); + /* + * Grab the content of the small messages first. This is going to + * be very fast and at very worst will be a single up of a few bytes and a single + * download of 625k. + */ + FetchProfile fp = new FetchProfile(); + //TODO: Only fetch small and large messages if we have some + fp.add(FetchProfile.Item.BODY); + // fp.add(FetchProfile.Item.FLAGS); + // fp.add(FetchProfile.Item.ENVELOPE); + downloadSmallMessages(remoteFolder, backendFolder, smallMessages, progress, newMessages, todo, fp, listener); + smallMessages.clear(); + /* + * Now do the large messages that require more round trips. + */ + fp = new FetchProfile(); + fp.add(FetchProfile.Item.STRUCTURE); + downloadLargeMessages(syncConfig, remoteFolder, backendFolder, largeMessages, progress, newMessages, todo, fp, listener); + largeMessages.clear(); + + Timber.d("SYNC: Synced remote messages for folder %s, %d new messages", folder, newMessages.get()); + + // If the oldest message seen on this sync is newer than the oldest message seen on the previous sync, then + // we want to move our high-water mark forward. + Date oldestMessageTime = backendFolder.getOldestMessageDate(); + if (oldestMessageTime != null) { + if (oldestMessageTime.before(downloadStarted) && + oldestMessageTime.after(getLatestOldMessageSeenTime(backendFolder))) { + setLatestOldMessageSeenTime(backendFolder, oldestMessageTime); + } + } + + return newMessages.get(); + } + + private Date getLatestOldMessageSeenTime(BackendFolder backendFolder) { + Long latestOldMessageSeenTime = backendFolder.getFolderExtraNumber(EXTRA_LATEST_OLD_MESSAGE_SEEN_TIME); + long timestamp = latestOldMessageSeenTime != null ? latestOldMessageSeenTime : 0L; + return new Date(timestamp); + } + + private void setLatestOldMessageSeenTime(BackendFolder backendFolder, Date oldestMessageTime) { + backendFolder.setFolderExtraNumber(EXTRA_LATEST_OLD_MESSAGE_SEEN_TIME, oldestMessageTime.getTime()); + } + + private void evaluateMessageForDownload( + final Pop3Message message, + final String folder, + final BackendFolder backendFolder, + final List<Pop3Message> unsyncedMessages, + final List<Pop3Message> syncFlagMessages, + SyncListener listener) { + + String messageServerId = message.getUid(); + if (message.isSet(Flag.DELETED)) { + Timber.v("Message with uid %s is marked as deleted", messageServerId); + + syncFlagMessages.add(message); + return; + } + + boolean messagePresentLocally = backendFolder.isMessagePresent(messageServerId); + + if (!messagePresentLocally) { + if (!message.isSet(Flag.X_DOWNLOADED_FULL) && !message.isSet(Flag.X_DOWNLOADED_PARTIAL)) { + Timber.v("Message with uid %s has not yet been downloaded", messageServerId); + + unsyncedMessages.add(message); + } else { + Timber.v("Message with uid %s is partially or fully downloaded", messageServerId); + + // Store the updated message locally + boolean completeMessage = message.isSet(Flag.X_DOWNLOADED_FULL); + if (completeMessage) { + backendFolder.saveMessage(message, MessageDownloadState.FULL); + } else { + backendFolder.saveMessage(message, MessageDownloadState.PARTIAL); + } + + boolean isOldMessage = isOldMessage(backendFolder, message); + listener.syncNewMessage(folder, messageServerId, isOldMessage); + } + return; + } + + Set<Flag> messageFlags = backendFolder.getMessageFlags(messageServerId); + if (!messageFlags.contains(Flag.DELETED)) { + Timber.v("Message with uid %s is present in the local store", messageServerId); + + if (!messageFlags.contains(Flag.X_DOWNLOADED_FULL) && !messageFlags.contains(Flag.X_DOWNLOADED_PARTIAL)) { + Timber.v("Message with uid %s is not downloaded, even partially; trying again", messageServerId); + + unsyncedMessages.add(message); + } else { + syncFlagMessages.add(message); + } + } else { + Timber.v("Local copy of message with uid %s is marked as deleted", messageServerId); + } + } + + private void fetchUnsyncedMessages(final SyncConfig syncConfig, final Pop3Folder remoteFolder, + List<Pop3Message> unsyncedMessages, + final List<Pop3Message> smallMessages, + final List<Pop3Message> largeMessages, + final AtomicInteger progress, + final int todo, + FetchProfile fp, + final SyncListener listener) throws MessagingException { + final String folder = remoteFolder.getServerId(); + + final Date earliestDate = syncConfig.getEarliestPollDate(); + remoteFolder.fetch(unsyncedMessages, fp, + new MessageRetrievalListener<Pop3Message>() { + @Override + public void messageFinished(Pop3Message message) { + try { + if (message.isSet(Flag.DELETED) || message.olderThan(earliestDate)) { + if (message.isSet(Flag.DELETED)) { + Timber.v("Newly downloaded message %s:%s:%s was marked deleted on server, " + + "skipping", accountName, folder, message.getUid()); + } else { + Timber.d("Newly downloaded message %s is older than %s, skipping", + message.getUid(), earliestDate); + } + + progress.incrementAndGet(); + + //TODO: This might be the source of poll count errors in the UI. Is todo always the same as ofTotal + listener.syncProgress(folder, progress.get(), todo); + return; + } + + if (syncConfig.getMaximumAutoDownloadMessageSize() > 0 && + message.getSize() > syncConfig.getMaximumAutoDownloadMessageSize()) { + largeMessages.add(message); + } else { + smallMessages.add(message); + } + } catch (Exception e) { + Timber.e(e, "Error while storing downloaded message."); + } + } + }, + syncConfig.getMaximumAutoDownloadMessageSize()); + } + + private void downloadSmallMessages( + final Pop3Folder remoteFolder, + final BackendFolder backendFolder, + List<Pop3Message> smallMessages, + final AtomicInteger progress, + final AtomicInteger newMessages, + final int todo, + FetchProfile fp, + final SyncListener listener) throws MessagingException { + final String folder = remoteFolder.getServerId(); + + Timber.d("SYNC: Fetching %d small messages for folder %s", smallMessages.size(), folder); + + remoteFolder.fetch(smallMessages, + fp, new MessageRetrievalListener<Pop3Message>() { + @Override + public void messageFinished(final Pop3Message message) { + try { + + // Store the updated message locally + backendFolder.saveMessage(message, MessageDownloadState.FULL); + progress.incrementAndGet(); + + // Increment the number of "new messages" if the newly downloaded message is + // not marked as read. + if (!message.isSet(Flag.SEEN)) { + newMessages.incrementAndGet(); + } + + String messageServerId = message.getUid(); + Timber.v("About to notify listeners that we got a new small message %s:%s:%s", + accountName, folder, messageServerId); + + // Update the listener with what we've found + listener.syncProgress(folder, progress.get(), todo); + + boolean isOldMessage = isOldMessage(backendFolder, message); + listener.syncNewMessage(folder, messageServerId, isOldMessage); + } catch (Exception e) { + Timber.e(e, "SYNC: fetch small messages"); + } + } + }, + -1); + + Timber.d("SYNC: Done fetching small messages for folder %s", folder); + } + + private boolean isOldMessage(BackendFolder backendFolder, Pop3Message message) { + return message.olderThan(getLatestOldMessageSeenTime(backendFolder)); + } + + private void downloadLargeMessages( + final SyncConfig syncConfig, + final Pop3Folder remoteFolder, + final BackendFolder backendFolder, + List<Pop3Message> largeMessages, + final AtomicInteger progress, + final AtomicInteger newMessages, + final int todo, + FetchProfile fp, + SyncListener listener) throws MessagingException { + final String folder = remoteFolder.getServerId(); + + Timber.d("SYNC: Fetching large messages for folder %s", folder); + + int maxDownloadSize = syncConfig.getMaximumAutoDownloadMessageSize(); + remoteFolder.fetch(largeMessages, fp, null, maxDownloadSize); + for (Pop3Message message : largeMessages) { + + downloadSaneBody(syncConfig, remoteFolder, backendFolder, message); + + String messageServerId = message.getUid(); + Timber.v("About to notify listeners that we got a new large message %s:%s:%s", + accountName, folder, messageServerId); + + // Update the listener with what we've found + progress.incrementAndGet(); + + // TODO do we need to re-fetch this here? + Set<Flag> flags = backendFolder.getMessageFlags(messageServerId); + // Increment the number of "new messages" if the newly downloaded message is + // not marked as read. + if (!flags.contains(Flag.SEEN)) { + newMessages.incrementAndGet(); + } + + listener.syncProgress(folder, progress.get(), todo); + + boolean isOldMessage = isOldMessage(backendFolder, message); + listener.syncNewMessage(folder, messageServerId, isOldMessage); + } + + Timber.d("SYNC: Done fetching large messages for folder %s", folder); + } + + private void downloadSaneBody(SyncConfig syncConfig, Pop3Folder remoteFolder, BackendFolder backendFolder, + Pop3Message message) throws MessagingException { + /* + * The provider was unable to get the structure of the message, so + * we'll download a reasonable portion of the message and mark it as + * incomplete so the entire thing can be downloaded later if the user + * wishes to download it. + */ + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.BODY_SANE); + /* + * TODO a good optimization here would be to make sure that all Stores set + * the proper size after this fetch and compare the before and after size. If + * they equal we can mark this SYNCHRONIZED instead of PARTIALLY_SYNCHRONIZED + */ + + int maxDownloadSize = syncConfig.getMaximumAutoDownloadMessageSize(); + remoteFolder.fetch(Collections.singletonList(message), fp, null, maxDownloadSize); + + boolean completeMessage = false; + // Certain (POP3) servers give you the whole message even when you ask for only the first x Kb + if (!message.isSet(Flag.X_DOWNLOADED_FULL)) { + /* + * Mark the message as fully downloaded if the message size is smaller than + * the account's autodownload size limit, otherwise mark as only a partial + * download. This will prevent the system from downloading the same message + * twice. + * + * If there is no limit on autodownload size, that's the same as the message + * being smaller than the max size + */ + if (syncConfig.getMaximumAutoDownloadMessageSize() == 0 + || message.getSize() < syncConfig.getMaximumAutoDownloadMessageSize()) { + completeMessage = true; + } + } + + // Store the updated message locally + if (completeMessage) { + backendFolder.saveMessage(message, MessageDownloadState.FULL); + } else { + backendFolder.saveMessage(message, MessageDownloadState.PARTIAL); + } + } +} |
