Check-in [e0c73936fc]

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Merge dotnet into dotnet/timers.
Timelines: family | ancestors | descendants | both | dotnet/timers
Files: files | file ages | folders
SHA1: e0c73936fc67f8d66da91a13d4e1323e31dc56a1
User & Date: tinus 2019-09-10 19:09:25
Wiki:dotnet/timers
Context
2019-09-13
18:58
Merge dotnet into dotnet/timers. check-in: e57149797c user: tinus tags: dotnet/timers
2019-09-10
19:09
Merge dotnet into dotnet/timers. check-in: e0c73936fc user: tinus tags: dotnet/timers
19:08
Updated TODO.md. check-in: a0bfc52b64 user: tinus tags: dotnet
2019-09-07
06:38
Only report times when done, not for each folder. check-in: 43bfe70cba user: tinus tags: dotnet/timers
Changes

Changes to MailSynchronizer.cs.

650
651
652
653
654
655
656
657
658





659
660
661
662
663

664
665
666
667
668
669












670
671
672
673
674
675
676
...
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763



764

765
766
767
768
769
770
771
            GatherParticipants(ref participantList, sourceMsg.ResentTo, ParticipantField.ResentTo);
            GatherParticipants(ref participantList, sourceMsg.ResentCc, ParticipantField.ResentCc);
            GatherParticipants(ref participantList, sourceMsg.ResentBcc, ParticipantField.ResentBcc);
            GatherParticipants(ref participantList, sourceMsg.ResentReplyTo, ParticipantField.ResentReplyTo);
            GatherParticipants(ref participantList, sourceMsg.ResentSender, ParticipantField.ResentSender);

            int order = 0;
            foreach (var (field, address, name) in participantList)
            {





                var msgParticipant = new MessageParticipant
                {
                    Message = targetMsg,
                    Field = field,
                    Order = ++order,

                };

                var key = address ?? name;
                if (_participants.TryGetValue(key, out long participantID))
                {
                    msgParticipant.ParticipantID = participantID;












                }
                else
                {
                    var participant = new Participant
                    {
                        Address = address,
                        Name = name,
................................................................................
            var sourceReferences = new List<string>(sourceMsg.References);
            if (!string.IsNullOrWhiteSpace(sourceMsg.InReplyTo) && !sourceReferences.Contains(sourceMsg.InReplyTo))
                sourceReferences.Insert(0, sourceMsg.InReplyTo);

            var dbMessageRefs = await work.DB.MessageReferences
                .Where(mr => mr.Message == targetMsg)
                .ToDictionaryAsync(mr => mr.ReferencedRfcMessageID, _cancellationToken);
            // TODO: figure out how to handle multiple messages with identical RfcMessageID
            var dbReferencedMessageIDs = await work.Messages
                .GetDictionaryAsync(m => sourceReferences.Contains(m.RfcMessageID),
                                    m => m.RfcMessageID,
                                    m => m?.ID);
            int index = 0;
            foreach (string mailRef in sourceReferences)
            {
                index++;
                MessageReference dbReference = dbMessageRefs.GetValueOrDefault(mailRef, null);



                long? dbMessageID = dbReferencedMessageIDs.GetValueOrDefault(mailRef, null);

                if (dbReference == null)
                {
                    await work.DB.MessageReferences.AddAsync(new MessageReference
                    {
                        Message = targetMsg,
                        ReferencedRfcMessageID = mailRef,
                        InReplyTo = mailRef == sourceMsg.InReplyTo,







|

>
>
>
>
>





>






>
>
>
>
>
>
>
>
>
>
>
>







 







<
|
|
|
|





>
>
>
|
>







650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
...
765
766
767
768
769
770
771

772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
            GatherParticipants(ref participantList, sourceMsg.ResentTo, ParticipantField.ResentTo);
            GatherParticipants(ref participantList, sourceMsg.ResentCc, ParticipantField.ResentCc);
            GatherParticipants(ref participantList, sourceMsg.ResentBcc, ParticipantField.ResentBcc);
            GatherParticipants(ref participantList, sourceMsg.ResentReplyTo, ParticipantField.ResentReplyTo);
            GatherParticipants(ref participantList, sourceMsg.ResentSender, ParticipantField.ResentSender);

            int order = 0;
            foreach (var (field, mailAddress, mailName) in participantList)
            {
                string address = mailAddress == "" ? null : mailAddress;
                string name = mailName == "" ? null : mailName;
                if (name.Trim(' ', '\'', '"', '<', '>', '(', ')') == address)
                    name = null;

                var msgParticipant = new MessageParticipant
                {
                    Message = targetMsg,
                    Field = field,
                    Order = ++order,
                    Name = name,
                };

                var key = address ?? name;
                if (_participants.TryGetValue(key, out long participantID))
                {
                    msgParticipant.ParticipantID = participantID;
                    /* TODO: when cleaning up the database? Put the most used name for this address in the participant.
                    UPDATE Participants p
                       SET Name = (SELECT mp.Name
                                     FROM MessageParticipants mp
                                    WHERE ParticipantID = p.ID
                                      AND mp.Name IS NOT NULL
                                 GROUP BY mp.Name
                                 ORDER BY count(*) DESC
                                 ,        ID ASC
                                    LIMIT 1)
                     WHERE p.ID IN ({string.Join(', ', participantIDs)});
                    */
                }
                else
                {
                    var participant = new Participant
                    {
                        Address = address,
                        Name = name,
................................................................................
            var sourceReferences = new List<string>(sourceMsg.References);
            if (!string.IsNullOrWhiteSpace(sourceMsg.InReplyTo) && !sourceReferences.Contains(sourceMsg.InReplyTo))
                sourceReferences.Insert(0, sourceMsg.InReplyTo);

            var dbMessageRefs = await work.DB.MessageReferences
                .Where(mr => mr.Message == targetMsg)
                .ToDictionaryAsync(mr => mr.ReferencedRfcMessageID, _cancellationToken);

            var dbReferencedMessageIDs = work.Messages
                .GetLookup(m => sourceReferences.Contains(m.RfcMessageID),
                           m => m.RfcMessageID,
                           m => m?.ID);
            int index = 0;
            foreach (string mailRef in sourceReferences)
            {
                index++;
                MessageReference dbReference = dbMessageRefs.GetValueOrDefault(mailRef, null);
                long? dbMessageID = null;
                if (dbReferencedMessageIDs.Contains(mailRef))
                {
                    dbMessageID = dbReferencedMessageIDs[mailRef].FirstOrDefault(id => id.HasValue);
                }
                if (dbReference == null)
                {
                    await work.DB.MessageReferences.AddAsync(new MessageReference
                    {
                        Message = targetMsg,
                        ReferencedRfcMessageID = mailRef,
                        InReplyTo = mailRef == sourceMsg.InReplyTo,

Changes to Models/MessageParticipant.cs.

28
29
30
31
32
33
34

35
36
        public Message Message { get; set; }

        public long ParticipantID { get; set; }
        public Participant Participant { get; set; }

        public ParticipantField Field { get; set; }
        public int Order { get; set; }

    }
}







>


28
29
30
31
32
33
34
35
36
37
        public Message Message { get; set; }

        public long ParticipantID { get; set; }
        public Participant Participant { get; set; }

        public ParticipantField Field { get; set; }
        public int Order { get; set; }
        public string Name { get; set; }
    }
}

Changes to Repositories/FoldersRepository.cs.

81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106

107
108
109
110
111
112
113
114
115
        public Task<IEnumerable<string>> GetFullNamesAsync(IEnumerable<Folder> folders)
        {
            return GetFullNamesAsync(folders.Select(f => f.ID));
        }
        public async Task<IEnumerable<string>> GetFullNamesAsync(IEnumerable<long> ids)
        {
            string sql = $@"
            WITH RECURSIVE FullName(ID, Name, ParentID) AS (
                SELECT ID
                ,      Name
                ,      ParentFolderID
                    FROM Folders
                    WHERE ID IN ({string.Join(", ", ids)})
                UNION
                SELECT fn.ID
                ,      pf.Name || '/' || fn.Name
                ,      pf.ParentFolderID
                    FROM FullName fn
                        LEFT JOIN Folders pf
                                ON fn.ParentID = pf.ID
                    WHERE pf.ID IS NOT NULL
            )
                SELECT ID
                ,      Name
                FROM FullName
                WHERE ParentID IS NULL

            ;";
#pragma warning disable EF1000
            var fullNameById = await _set.FromSql<dynamic>(sql)
                .ToDictionaryAsync(row => (long)row.ID, row => (string)row.Name);
#pragma warning restore EF1000
            return ids.Select(id => fullNameById.GetValueOrDefault(id));
        }
    }
}







|



|
|




|
|
|
|

|
<
|
|
>

|
|
<

<



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103

104
105
106
107
108
109

110

111
112
113
        public Task<IEnumerable<string>> GetFullNamesAsync(IEnumerable<Folder> folders)
        {
            return GetFullNamesAsync(folders.Select(f => f.ID));
        }
        public async Task<IEnumerable<string>> GetFullNamesAsync(IEnumerable<long> ids)
        {
            string sql = $@"
            WITH RECURSIVE FullNames(ID, Name, ParentID) AS (
                SELECT ID
                ,      Name
                ,      ParentFolderID
                  FROM Folders
                 WHERE ID IN ({string.Join(", ", ids)})
                UNION
                SELECT fn.ID
                ,      pf.Name || '/' || fn.Name
                ,      pf.ParentFolderID
                  FROM FullNames fn
                       LEFT JOIN Folders pf
                              ON fn.ParentID = pf.ID
                 WHERE fn.ParentID IS NOT NULL
            )
              SELECT Name

                FROM FullNames
               WHERE ParentID IS NULL
            ORDER BY CASE ID {string.Join("\n", ids.Select((id, index) => $"WHEN {id} THEN {index}"))} END
            ;";
#pragma warning disable EF1000 // There's no risk of SQL injection with this query.
            return await _set.FromSql(sql).Select(f => f.Name).ToListAsync();

#pragma warning restore EF1000

        }
    }
}

Changes to Repositories/Repository.cs.

58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
..
76
77
78
79
80
81
82
83
84
85
86
87


































88
89
90
91
92
93
94
...
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
        public Task<Dictionary<TKey, T>> GetDictionaryAsync<TKey>(Expression<Func<T, bool>> where,
                                                                  Func<T, TKey> keySelector,
                                                                  IEqualityComparer<TKey> keyComparer = null)
        {
            IQueryable<T> result = _set;
            if (where != null)
                result = result.Where(where);
            return result.ToDictionaryAsync(keySelector, 
                                            keyComparer ?? EqualityComparer<TKey>.Default, 
                                            _cancellationToken);
        }
        public Task<Dictionary<TKey,TValue>> GetDictionaryAsync<TKey, TValue>(Func<T, TKey> keySelector,
                                                                              Func<T, TValue> valueSelector,
                                                                              IEqualityComparer<TKey> keyComparer = null)
        {
            return GetDictionaryAsync(null, keySelector, valueSelector, keyComparer);
................................................................................
                                                                              Func<T, TKey> keySelector,
                                                                              Func<T, TValue> valueSelector,
                                                                              IEqualityComparer<TKey> keyComparer = null)
        {
            IQueryable<T> result = _set;
            if (where != null)
                result = result.Where(where);
            return result.ToDictionaryAsync<T, TKey, TValue>(keySelector, 
                                                             valueSelector, 
                                                             keyComparer ?? EqualityComparer<TKey>.Default, 
                                                             _cancellationToken);
        }



































        public IEnumerable<TResult> Select<TResult>(Expression<Func<T, bool>> where,
                                                    Expression<Func<T, TResult>> valueExpression,
                                                    Expression<Func<TResult, bool>> valueFilter = null,
                                                    IComparer<TResult> valueSorter = null)
        {
            return Select(valueExpression, where, valueFilter, valueSorter);
................................................................................
                if (keyProperties.Count == 1)
                {
                    IProperty key = keyProperties.First();
                    if (typeof(long).IsAssignableFrom(key.ClrType))
                    {
                        // Ensure we've got Int64s, and nothing else
                        var numericIDs = ids.Select(id => Convert.ToInt64(id));
                        
                        // Prepare the SQL statement
                        string tableName = entityType.Relational().TableName;
                        string keyColumn = key.Relational().ColumnName;
                        string sql = "DELETE FROM [" + tableName + "] WHERE [" + keyColumn + "] IN (" + string.Join(", ", numericIDs) + ")";
                        #pragma warning disable EF1000 // SQL command has been checked for SQL injection vectors, and found safe.
                        int numDeleted = _db.Database.ExecuteSqlCommand(sql);
                        #pragma warning restore EF1000
                        
                        // Detach all corresponding entries
                        var entries = _db.ChangeTracker.Entries()
                            .Where(e => e.Metadata == entityType)
                            .Where(e => numericIDs.Contains((long)e.Property(key.Name).CurrentValue));
                        foreach (var entry in entries)
                        {
                            entry.State = EntityState.Detached;







|
|







 







|
|
|


>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







 







|







|







58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
..
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
...
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
        public Task<Dictionary<TKey, T>> GetDictionaryAsync<TKey>(Expression<Func<T, bool>> where,
                                                                  Func<T, TKey> keySelector,
                                                                  IEqualityComparer<TKey> keyComparer = null)
        {
            IQueryable<T> result = _set;
            if (where != null)
                result = result.Where(where);
            return result.ToDictionaryAsync(keySelector,
                                            keyComparer ?? EqualityComparer<TKey>.Default,
                                            _cancellationToken);
        }
        public Task<Dictionary<TKey,TValue>> GetDictionaryAsync<TKey, TValue>(Func<T, TKey> keySelector,
                                                                              Func<T, TValue> valueSelector,
                                                                              IEqualityComparer<TKey> keyComparer = null)
        {
            return GetDictionaryAsync(null, keySelector, valueSelector, keyComparer);
................................................................................
                                                                              Func<T, TKey> keySelector,
                                                                              Func<T, TValue> valueSelector,
                                                                              IEqualityComparer<TKey> keyComparer = null)
        {
            IQueryable<T> result = _set;
            if (where != null)
                result = result.Where(where);
            return result.ToDictionaryAsync<T, TKey, TValue>(keySelector,
                                                             valueSelector,
                                                             keyComparer ?? EqualityComparer<TKey>.Default,
                                                             _cancellationToken);
        }

        public ILookup<TKey, T> GetLookup<TKey>(Func<T, TKey> keySelector,
                                                IEqualityComparer<TKey> keyComparer = null)
        {
            return GetLookup((Expression<Func<T, bool>>)null, keySelector, keyComparer);
        }
        public ILookup<TKey, T> GetLookup<TKey>(Expression<Func<T, bool>> where,
                                                Func<T, TKey> keySelector,
                                                IEqualityComparer<TKey> keyComparer = null)
        {
            IQueryable<T> result = _set;
            if (where != null)
                result = result.Where(where);
            return result.ToLookup(keySelector,
                                   keyComparer ?? EqualityComparer<TKey>.Default);
        }
        public ILookup<TKey, TValue> GetLookup<TKey, TValue>(Func<T, TKey> keySelector,
                                                             Func<T, TValue> valueSelector,
                                                             IEqualityComparer<TKey> keyComparer = null)
        {
            return GetLookup(null, keySelector, valueSelector, keyComparer);
        }
        public ILookup<TKey, TValue> GetLookup<TKey, TValue>(Expression<Func<T, bool>> where,
                                                             Func<T, TKey> keySelector,
                                                             Func<T, TValue> valueSelector,
                                                             IEqualityComparer<TKey> keyComparer = null)
        {
            IQueryable<T> result = _set;
            if (where != null)
                result = result.Where(where);
            return result.ToLookup(keySelector,
                                   valueSelector,
                                   keyComparer ?? EqualityComparer<TKey>.Default);
        }

        public IEnumerable<TResult> Select<TResult>(Expression<Func<T, bool>> where,
                                                    Expression<Func<T, TResult>> valueExpression,
                                                    Expression<Func<TResult, bool>> valueFilter = null,
                                                    IComparer<TResult> valueSorter = null)
        {
            return Select(valueExpression, where, valueFilter, valueSorter);
................................................................................
                if (keyProperties.Count == 1)
                {
                    IProperty key = keyProperties.First();
                    if (typeof(long).IsAssignableFrom(key.ClrType))
                    {
                        // Ensure we've got Int64s, and nothing else
                        var numericIDs = ids.Select(id => Convert.ToInt64(id));

                        // Prepare the SQL statement
                        string tableName = entityType.Relational().TableName;
                        string keyColumn = key.Relational().ColumnName;
                        string sql = "DELETE FROM [" + tableName + "] WHERE [" + keyColumn + "] IN (" + string.Join(", ", numericIDs) + ")";
                        #pragma warning disable EF1000 // SQL command has been checked for SQL injection vectors, and found safe.
                        int numDeleted = _db.Database.ExecuteSqlCommand(sql);
                        #pragma warning restore EF1000

                        // Detach all corresponding entries
                        var entries = _db.ChangeTracker.Entries()
                            .Where(e => e.Metadata == entityType)
                            .Where(e => numericIDs.Contains((long)e.Property(key.Name).CurrentValue));
                        foreach (var entry in entries)
                        {
                            entry.State = EntityState.Detached;

Name change from Scripts/Participants with message counts.sql to Scripts/MessageParticipants.sql.

1
2
3
4
5
6
7
8
9
  SELECT p.Address
  ,      p.Name
  ,      count(*) AS MessageCount
    FROM Participants p
         LEFT JOIN MessageParticipants mp
                ON p.ID = mp.ParticipantID
GROUP BY p.ID
ORDER BY MessageCount DESC
;
|
|
|






1
2
3
4
5
6
7
8
9
  SELECT count(mp.MessageID) AS MessageCount
  ,      p.Address
  ,      p.Name
    FROM Participants p
         LEFT JOIN MessageParticipants mp
                ON p.ID = mp.ParticipantID
GROUP BY p.ID
ORDER BY MessageCount DESC
;

Added Scripts/MessageParticipantsDated.sql.



























>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
  SELECT count(mp.MessageID) AS MessageCount
  ,      p.Address
  ,      p.Name
  ,      min(m.Date) AS First
  ,      max(m.Date) AS Last
    FROM Participants p
         LEFT JOIN MessageParticipants mp
                ON p.ID = mp.ParticipantID
         LEFT JOIN Messages m
                ON mp.MessageID = m.ID
GROUP BY p.ID
ORDER BY MessageCount DESC
;

Changes to TODO.md.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# TO DO

## Fix bugs

* ✔ ~~Correct the folder filter, so that it _actually_ skips the trash, spam, and all mail folders.~~
* ✔ ~~Try fixing the message with unparseable headers:~~

      System.FormatException: Failed to parse message headers.
         at MimeKit.MimeParser.ParseMessageAsync(CancellationToken cancellationToken)
         at MailKit.Net.Imap.ImapEngine.ParseMessageAsync(Stream stream, Boolean persistent, Boolean doAsync, CancellationToken cancellationToken)
         at MailKit.Net.Imap.ImapFolder.ParseMessageAsync(Stream stream, Boolean doAsync, CancellationToken cancellationToken)
         at MailKit.Net.Imap.ImapFolder.GetMessageAsync(UniqueId uid, Boolean doAsync, CancellationToken cancellationToken, ITransferProgress progress)
         at MailJanitor.MailSynchronizer.SynchronizeMessage(IMessageSummary summary, IMailFolder mailFolder, Int64 folderID, Nullable`1 folderMsgID, Nullable`1 messageID) in /home/tinus/code/mailjanitor/MailSynchronizer.cs:line 312

  - ~~Download the message's stream, save that to file, then load it as string, and while the first line starts with a spacing character, remove that first line.~~
  - ~~Then, try parsing the message again. If successful, continue processing as normal.~~

* ✔ ~~Better handling of impossible dates for Zip entry:~~

      System.ArgumentOutOfRangeException: The DateTimeOffset specified cannot be converted into a Zip file timestamp.
      Parameter name: value
         at System.IO.Compression.ZipArchiveEntry.set_LastWriteTime(DateTimeOffset value)
         at MailJanitor.MailSynchronizer.PopulateMessageAsync(UnitOfWork work, Message targetMsg, MimeMessage sourceMsg, IEnumerable`1 keywords, IEnumerable`1 labels) in /home/tinus/code/mailjanitor/MailSynchronizer.cs:line 422
         at MailJanitor.MailSynchronizer.SynchronizeMessage(IMessageSummary summary, IMailFolder mailFolder, Int64 folderID, Nullable`1 folderMsgID, Nullable`1 messageID) in /home/tinus/code/mailjanitor/MailSynchronizer.cs:line 340

* Better handling of duplicate RfcMessageIDs:

      System.ArgumentException: An item with the same key has already been added. Key: 1949688653-1463792126-1170105986@boing.topica.com
         at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
         at System.Linq.AsyncEnumerable.<>c__DisplayClass452_0`3.<ToDictionary>b__0(Dictionary`2 d, TSource x) in D:\a\1\s\Ix.NET\Source\System.Interactive.Async\ToCollection.cs:line 143
         at System.Linq.AsyncEnumerable.Aggregate_[TSource,TAccumulate,TResult](IAsyncEnumerable`1 source, TAccumulate seed, Func`3 accumulator, Func`2 resultSelector, CancellationToken cancellationToken) in D:\a\1\s\Ix.NET\Source\System.Interactive.Async\Aggregate.cs:line 123
         at MailJanitor.MailSynchronizer.PopulateReferencesAsync(UnitOfWork work, Message targetMsg, MimeMessage sourceMsg) in /home/tinus/code/mailjanitor/MailSynchronizer.cs:line 682
         at MailJanitor.MailSynchronizer.PopulateMessageAsync(UnitOfWork work, Message targetMsg, MimeMessage sourceMsg, IEnumerable`1 keywords, IEnumerable`1 labels) in /home/tinus/code/mailjanitor/MailSynchronizer.cs:line 439
         at MailJanitor.MailSynchronizer.SynchronizeMessage(IMessageSummary summary, IMailFolder mailFolder, Int64 folderID, Nullable`1 folderMsgID, Nullable`1 messageID) in /home/tinus/code/mailjanitor/MailSynchronizer.cs:line 340

## Feature: improve robustness

* When a message cannot be downloaded or processed, we should record this, and try again next time (even if the folder's UIDNext is unchanged).
  - Add a nullable `error` text field to `FolderMessage`, where we store the full exception text.
  - When selecting the UIDs to download, make sure we pick up the ones with a non-null `error` field.
  - When the folder would be skipped based on UIDNext, check if there are no errored messages; if so, process the folder anyway.



<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







1
2

































3
4
5
6
7
8
9
# TO DO


































## Feature: improve robustness

* When a message cannot be downloaded or processed, we should record this, and try again next time (even if the folder's UIDNext is unchanged).
  - Add a nullable `error` text field to `FolderMessage`, where we store the full exception text.
  - When selecting the UIDs to download, make sure we pick up the ones with a non-null `error` field.
  - When the folder would be skipped based on UIDNext, check if there are no errored messages; if so, process the folder anyway.