private async void SaveClick(object sender, EventArgs e) { // Filter out special labels that don't match mailbox names. List <string> labelsBefore = GmailExtensions.GetNonSpecialLabels(Conversation.Labels); List <string> labelsAfter = LabelList.SelectedItems.Cast <LabelInfo>() .Select(info => info.Name).ToList(); Account account = App.AccountManager.GetCurrentAccount(); List <string> addTo = new List <string>(); List <string> removeFrom = new List <string>(); // Diff before and after. For new labels, add that label. // For old labels, remove that label. SyncUtilities.CompareLists(labelsBefore, labelsAfter, input => input, (before, after) => { }, // Match, do nothing (before) => removeFrom.Add(before), (after) => addTo.Add(after) // Added ); // Do additions before removals. Additions will fail if you first remove the current label because you'll // have deleted the message from the current mailbox. foreach (string label in addTo) { await account.AddLabelAsync(Conversation.Messages, label); // Added } foreach (string label in removeFrom) { await account.RemoveLabelAsync(Conversation.Messages, label); // Removed } NavigationService.GoBack(); }
// Removing labels for the non-active mailboxes is a bit more work. We must: // - Switch to that mailbox // - Find the message mailbox UID for that message // -- Search storage first, then online // - Delete the message from the mailbox & storage // - Switch back to the original mailbox private async Task RemoveOtherLabelAsync(List <MailMessage> messages, string labelName) { List <MailMessage> changedMessages = new List <MailMessage>(); // Remove the label from the messages and store the changes. // TODO: Remove message from disk if the message is no longer referenced by any sync'd labels. foreach (var message in messages) { if (message.RemoveLabel(labelName)) { changedMessages.Add(message); if (MailStorage.HasMessageLables(message.GetThreadId(), message.GetMessageId())) { // Update on disk await MailStorage.StoreMessageLabelsAsync(message); } } } LabelInfo labelInfo = Labels.Where(info => info.Name.Equals(labelName)).FirstOrDefault() ?? new LabelInfo() { Name = labelName }; // Look up UIDs. If they're not here, we may need to check online. List <string> localMessageIds = new List <string>(); // Ids for messages we have referenced from a locally sync'd label. List <string> nonlocalMessageIds = new List <string>(); // Ids for messages we'll have to lookup online. List <MessageIdInfo> labelMessageIds = (labelInfo.StoreMessages ? await MailStorage.GetLabelMessageListAsync(labelName) : null) ?? new List <MessageIdInfo>(); SyncUtilities.CompareLists(changedMessages.Select(message => message.GetMessageId()), labelMessageIds.Select(ids => ids.MessageId), id => id, (searchId, localId) => localMessageIds.Add(localId), (searchId) => nonlocalMessageIds.Add(searchId), (localId) => { } // Only in storage, ignore. ); // Remove from that labelList List <MessageIdInfo> updatedLabelMessageIds = labelMessageIds.Where(messageIds => !localMessageIds.Contains(messageIds.MessageId)).ToList(); if (labelInfo.StoreMessages) { await MailStorage.StoreLabelMessageListAsync(labelName, updatedLabelMessageIds); } List <string> uidsToRemove = labelMessageIds.Where(messageIds => localMessageIds.Contains(messageIds.MessageId)).Select(ids => ids.Uid).ToList(); // TODO: Queue up this action for later if (nonlocalMessageIds.Count > 0) { List <string> remoteUids = await GmailImap.GetUidsFromMessageIds(labelName, nonlocalMessageIds); uidsToRemove.AddRange(remoteUids); } if (uidsToRemove.Count > 0) { await GmailImap.RemoveOtherLabelAsync(labelName, uidsToRemove); } }
// Determine oldest date: Now - Range // Query server for ids of messages in folder since oldest date (async while loading local list?) // Load stored message list (Ids only) // For each new item in the remote list: // - Download & save headers, flags, & labels, add to local list, save (TODO: What if another label already had this message?) // - Anything that is unread counts as new mail. It wasn't downloaded last time we opened the app, so the message date is irrelevant. // - Queue body and attachments for download later. // For each item that was already in the local list // - query for updated flags and labels // - Anything that is unread since the last time we opened the app counts as new mail. // - Queue body and attachments for download later (if they weren't downloaded on a previous sync) public async Task <int> SyncMessageHeadersAsync(CancellationToken cancellationToken) { DateTime syncMailSince = DateTime.Now - Info.Range; Task <IList <GmailMessageInfo> > remoteMessageInfoTask = GmailImap.GetCurrentMessageIdsAsync(syncMailSince); List <MessageIdInfo> localIds = await MailStorage.GetLabelMessageListAsync(ActiveLabel.Info.Name) ?? new List <MessageIdInfo>(); IList <GmailMessageInfo> remoteMessageInfos = await remoteMessageInfoTask; IEnumerable <MessageIdInfo> remoteIds = remoteMessageInfos.Select(info => new MessageIdInfo() { Uid = info.Uid, MessageId = info.MessageId, ThreadId = info.ThreadId, }); DateTime lastAppActivation = AppSettings.LastAppActivationTime; int newMessages = remoteMessageInfos .Where(info => info.Date > lastAppActivation && !info.Flags.Contains(@"\Seen")).Count(); IList <MessageIdInfo> messagesInBothPlaces = new List <MessageIdInfo>(); IList <MessageIdInfo> messagesOnlyRemote = new List <MessageIdInfo>(); IList <MessageIdInfo> messagesOnlyLocal = new List <MessageIdInfo>(); IList <MessageIdInfo> messageHeadersToDownload = new List <MessageIdInfo>(); if (cancellationToken.IsCancellationRequested) { return(newMessages); } SyncUtilities.CompareLists(remoteIds, localIds, info => info.Uid, (remote, local) => messagesInBothPlaces.Add(local), remoteOnly => messagesOnlyRemote.Add(remoteOnly), localOnly => messagesOnlyLocal.Add(localOnly)); if (cancellationToken.IsCancellationRequested) { return(newMessages); } bool localMessageListModified = false; foreach (MessageIdInfo idInfo in messagesOnlyRemote) { // Check if the item is already on disk (from another label) if (MailStorage.HasMessageHeaders(idInfo.ThreadId, idInfo.MessageId)) { // If so, update the labels and flags. messagesInBothPlaces.Add(idInfo); } // If not, download the headers / message structure. else { messageHeadersToDownload.Add(idInfo); } // Add to labe's list. localIds.Add(idInfo); localMessageListModified = true; } if (messageHeadersToDownload.Count > 0) { // Bulk download headers await GmailImap.GetEnvelopeAndStructureAsync(messageHeadersToDownload.Select(ids => ids.Uid), async data => { // Find the matching Ids string messageId = data.GetMessageId(); GmailMessageInfo info = remoteMessageInfos.First(infos => infos.MessageId.Equals(messageId)); if (cancellationToken.IsCancellationRequested) { return; } // Save headers, labels, and flags to disk await MailStorage.StoreMessageFlagsAsync(info.ThreadId, info.MessageId, info.Flags); await MailStorage.StoreMessageLabelsAsync(info.ThreadId, info.MessageId, info.Labels); await MailStorage.StoreMessageHeadersAsync(data); }, cancellationToken); } if (cancellationToken.IsCancellationRequested) { return(newMessages); } if (localMessageListModified) { await MailStorage.StoreLabelMessageListAsync(ActiveLabel.Info.Name, localIds); } foreach (MessageIdInfo idInfo in messagesInBothPlaces) { if (cancellationToken.IsCancellationRequested) { return(newMessages); } // Find the matching Ids GmailMessageInfo info = remoteMessageInfos.First(infos => infos.MessageId.Equals(idInfo.MessageId)); // Update the labels and flags. await MailStorage.StoreMessageFlagsAsync(info.ThreadId, info.MessageId, info.Flags); await MailStorage.StoreMessageLabelsAsync(info.ThreadId, info.MessageId, info.Labels); } if (cancellationToken.IsCancellationRequested) { return(newMessages); } // Only remember our last sync time if we (mostly) finished. ActiveLabel.Info.LastSync = DateTime.Now; await SaveLabelSettingsAsync(); if (messagesOnlyLocal.Any()) { // Remove deleted mails from label list localIds = localIds.Except(messagesOnlyLocal).ToList(); await MailStorage.StoreLabelMessageListAsync(ActiveLabel.Info.Name, localIds); // TODO: GC message data. } return(newMessages); }