You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

792 lines
24 KiB

/*
** CWIMAPFolder.m
**
** Copyright (c) 2001-2007
**
** Author: Ludovic Marcotte <ludovic@Sophos.ca>
**
** This library is free software; you can redistribute it and/or
** modify it under the terms of the GNU Lesser General Public
** License as published by the Free Software Foundation; either
** version 2.1 of the License, or (at your option) any later version.
**
** This library is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
** Lesser General Public License for more details.
**
** You should have received a copy of the GNU Lesser General Public
** License along with this library; if not, write to the Free Software
** Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#import "CWIMAPFolder+CWProtected.h"
#import "CWConnection.h"
#import "CWConstants.h"
#import "CWFlags.h"
#import "CWIMAPStore+Protected.h"
#import "CWIMAPMessage.h"
#import <pEpIOSToolbox/PEPLogger.h>
#import "NSData+Extensions.h"
#import "Pantomime/NSString+Extensions.h"
#import "NSDate+StringRepresentation.h"
@interface CWIMAPFolder ()
@property NSMutableDictionary *uidToMsnMap;
@property NSMutableDictionary *msnToUidMap;
@property BOOL isUpdatingMessageNumber;
@end
//
// Private methods
//
@interface CWIMAPFolder (Private)
- (BOOL) _isInFetchedRange:(NSUInteger)uid;
- (BOOL) _wouldCreatedUpperFetchedRangeWithFrom:(NSUInteger)fromUid to:(NSUInteger)toUid;
- (BOOL) _wouldCreatedLowerFetchedRangeWithFrom:(NSUInteger)fromUid to:(NSUInteger)toUid;
- (BOOL) _isNegativeUid:(NSInteger)uid;
- (BOOL) _uidRangeAllreadyFetchedWithFrom:(NSUInteger)fromUid to:(NSUInteger)toUid;
- (BOOL) _uidRangeOutOfExistsRangeWithFrom:(NSUInteger)fromUid to:(NSUInteger)toUid;
- (NSData *) _removeInvalidHeadersFromMessage: (NSData *) theMessage;
@end
//
//
//
@implementation CWIMAPFolder
- (id) initWithName: (NSString *) theName
{
self = [super initWithName: theName];
if (self) {
self.uidToMsnMap = [NSMutableDictionary new];
self.msnToUidMap = [NSMutableDictionary new];
[self setSelected: NO];
}
return self;
}
//
//
//
- (id) initWithName: (NSString *) theName
mode: (PantomimeFolderMode) theMode
{
self = [self initWithName: theName];
if (self) {
_mode = theMode;
}
return self;
}
//
//
//
- (void) appendMessageFromRawSource: (NSData *) theData
flags: (CWFlags *) theFlags
{
[self appendMessageFromRawSource: theData
flags: theFlags
internalDate: nil];
}
- (void)appendMessageFromRawSource:(NSData *)rawSource
flags:(CWFlags * _Nullable)flags
internalDate:(NSDate * _Nullable)date;
{
NSString *flagsAsString = @"";
if (flags) {
flagsAsString = [flags asString];
}
// We remove any invalid headers from our message
NSData *dataToAppend = [self _removeInvalidHeadersFromMessage: rawSource];
NSDictionary *aDictionary;
if (flags) {
aDictionary = @{@"NSDataToAppend":dataToAppend,
@"NSData":rawSource,
@"Folder":self,
PantomimeFlagsKey:flags};
} else {
aDictionary = @{@"NSDataToAppend":dataToAppend,
@"NSData":rawSource,
@"Folder":self};
}
if (!date) {
date = [NSDate new];
}
NSAssert(date, @"Must not be nil");
[_store sendCommand: IMAP_APPEND
info: aDictionary
arguments: @"APPEND \"%@\" (%@) \"%@\" {%d}", // IMAP command
[_name modifiedUTF7String], // folder name
flagsAsString, // flags
[date dateTimeString], // Internal date
[dataToAppend length]]; // length of the data to write
}
#pragma mark - UID COPY
// Implementation of UID COPY
// (see https://tools.ietf.org/html/rfc3501#section-6.4.8)
- (void)copyMessageWithUid:(NSUInteger)uid toFolderNamed:(NSString *)targetFolderName;
{
NSParameterAssert(uid > 0);
[_store sendCommand: IMAP_UID_COPY
info: nil
arguments: @"UID COPY %u \"%@\"", uid, [targetFolderName modifiedUTF7String]];
}
- (void) copyMessages: (NSArray *) theMessages
toFolder: (NSString *) theFolder
{
NSMutableString *aMutableString;
NSUInteger i, count;
// We create our message's UID set
aMutableString = [[NSMutableString alloc] init];
count = [theMessages count];
for (i = 0; i < count; i++)
{
if (i == count-1)
{
[aMutableString appendFormat: @"%lu",
(unsigned long)[[theMessages objectAtIndex: i] UID]];
}
else
{
[aMutableString appendFormat: @"%lu,",
(unsigned long)[[theMessages objectAtIndex: i] UID]];
}
}
// We send our IMAP command
[_store sendCommand: IMAP_UID_COPY
info: [NSDictionary dictionaryWithObjectsAndKeys: theMessages,
PantomimeMessagesKey, theFolder, @"Name", self, @"Folder", nil]
arguments: @"UID COPY %@ \"%@\"",
aMutableString,
[theFolder modifiedUTF7String]];
RELEASE(aMutableString);
}
#pragma mark - UID MOVE
// Basic implementation of the UID MOVE extension(see RFC-6851)
- (void)moveMessageWithUid:(NSUInteger)uid toFolderNamed:(NSString *)targetFolderName;
{
NSParameterAssert(uid > 0);
[_store sendCommand: IMAP_UID_MOVE
info: nil
arguments: @"UID MOVE %u \"%@\"", uid, [targetFolderName modifiedUTF7String]];
}
#pragma mark - Fetching
// Fetches fetchMaxMails number of (yet unfetched) older messages by MSN.
// Fetching by UID did not work. Here is why:
//
// |<----------------------------- Existing messages on server (self.existsCount == 20) ---------------------------------->|
// Sequence numbers:
// | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
// UIDs:
// | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 108 | 109 | 110 | 11 | 112 | 313 | 314 | 415 | 416 | 417 | 418 | 519 | 520 |
// |<---- allready fetched ----->|
// |<------ fetchedRange ------->|
// ^ ^
// | |
// firstUid lastUid
// We want this: |<--- fetchMaxMails --->|
// |
// UID gap 11 - 107
//
// The Problem is that the UIDs are not sequential.
// uidGap: 108 - 10 - 1 == 97
// Handling the UID gap by making multiple calls can cause many, many calls (num calls ~= uidGap / fetchMaxMails)
// which might even be punished by the provider by denying access due to assumed DOS attempt. Temporarly or forever.
// Thus we are fetching by MSNs.
- (void) fetchOlder
{
if ([self isFirstCallToFetchOlder]) {
[self fetchOlderProtected];
}
}
// We want to always have a closed fetchedRange.
// In other words, we do not want to have multible fetchedRanges.
// Thus we adjust fromUid and toUid accordingly, if required.
//
// Example:
//
// |<----------------------------- Existing messages on server (self.existsCount == 20) ---------------------------------->|
// Sequence numbers:
// | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
// UIDs:
// | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 108 | 109 | 110 | 11 | 112 | 313 | 314 | 415 | 416 | 417 | 418 | 519 | 520 |
// |<---- allready fetched ----->|
// |<------ fetchedRange ------->|
// ^ ^
// | |
// firstUid lastUid
//---------------------------------------------------------------------------------------------------------------------------
// case 1:
// | |
// fromUid toUid
// Range already fetched. Do nothing.
//---------------------------------------------------------------------------------------------------------------------------
// case 2:
// before:
// | |
// fromUid toUid
// Would result in a second fetchedRange. Move "fromUid" down.
// after:
// |<--- |
// fromUid toUid
//---------------------------------------------------------------------------------------------------------------------------
// case 3:
// before:
// | |
// fromUid toUid
// Would result in a second fetchedRange. Move "toUid" up.
// after:
// | ---->|
// fromUid toUid
//---------------------------------------------------------------------------------------------------------------------------
// case 4:
// before:
// | |
// fromUid toUid
// "fromUid" is in fetchedRange. Move it up.
// after:
// --->| |
// fromUid toUid
//---------------------------------------------------------------------------------------------------------------------------
// case 5:
// before:
// | |
// fromUid toUid
// "toUid" is in fetchedRange. Move it down.
// after:
// | |<---------
// fromUid toUid
//---------------------------------------------------------------------------------------------------------------------------
// case 6:
// before:
// | |
// fromUid toUid
// fetchedRange is included in fromUid-toUid range.
// We ignore this fact and fetch the messaged in fetchedRange again.
//---------------------------------------------------------------------------------------------------------------------------
// case 7:
// Nothing has been fetched yet. We get the last fetchMaxMails numbers of *sequence* numbers.
// |<----------------------------------- fetchMaxMails --------------------------------------->|
// fromSequenceNum toSequenceNum
//---------------------------------------------------------------------------------------------------------------------------
#define UNLIMITED NSIntegerMin
- (void) fetchFrom:(NSUInteger)fromUid to:(NSInteger)toUid
{
// Invalid input. Do nothing.
if (fromUid == 0 || fromUid > toUid) {
LogWarn(@"Invalid input.");
// Inform the client
[_store signalFolderFetchCompleted];
return;
}
// No messages on server (EXISTS count is 0)
if (![self messagesExistOnServer]) {
[_store signalFolderFetchCompleted];
return;
}
// case 1
if ([self _uidRangeAllreadyFetchedWithFrom:fromUid to:toUid]) {
// No reason to fetch, inform the client
[_store signalFolderFetchCompleted];
return;
}
NSInteger from = fromUid;
NSInteger to = toUid;
// case 4
from = [self _isInFetchedRange:from] ? [self lastUID] + 1 : from;
// case 5
to = [self _isInFetchedRange:to] ? [self firstUID] - 1 : to;
if ([self _wouldCreatedUpperFetchedRangeWithFrom:from to:to]) {
// case 2
from = [self lastUID] + 1;
} else if ([self _wouldCreatedLowerFetchedRangeWithFrom:from to:to]) {
// case 3
to = [self firstUID] - 1;
}
NSString *toString = (toUid == UNLIMITED) ? @"*" : [NSString stringWithFormat:@"%ld", (long)to];
[_store sendCommand: IMAP_UID_FETCH_RFC822 info: nil
arguments: @"UID FETCH %u:%@ (UID FLAGS BODY.PEEK[])", from, toString];
}
//
//
//
- (void) fetch
{
// Maximum number of mails to fetch
NSInteger fetchMaxMails = [self maximumNumberOfMessagesToFetch];
if ([self lastUID] > 0) {
// We already fetched mails before, so lets fetch all newer ones by UID
NSInteger fromUid = [self lastUID] + 1;
fromUid = fromUid <= 0 ? 1 : fromUid;
[self fetchFrom:fromUid to:UNLIMITED];
} else {
LogInfo(@"no messages, fetching from scratch");
// case 7
// Local cache seems to be empty. Fetch a maximum of fetchMaxMails newest mails
// with a simple FETCH by sequnce numbers
NSInteger upperMessageSequenceNumber = [self existsCount];
LogInfo(@"existsCount %ld", (long) upperMessageSequenceNumber);
if (upperMessageSequenceNumber == 0) {
// nothing to fetch
[_store signalFolderFetchCompleted];
return;
} else {
NSInteger lowerMessageSequenceNumber = upperMessageSequenceNumber - fetchMaxMails + 1;
lowerMessageSequenceNumber = MAX(1, lowerMessageSequenceNumber);
[_store sendCommand: IMAP_UID_FETCH_RFC822 info: nil
arguments: @"FETCH %u:%u (UID FLAGS BODY.PEEK[])",
lowerMessageSequenceNumber,
upperMessageSequenceNumber];
}
}
}
- (void)fetchUidsForNewMails;
{
NSInteger lastUid = [self lastUID] ? [self lastUID] : 0;
NSInteger from = lastUid + 1;
[_store sendCommand: IMAP_UID_FETCH_UIDS info:nil arguments:@"UID FETCH %u:* (UID)", from];
}
#pragma mark -
- (void)syncExistingFirstUID:(NSUInteger)firstUID lastUID:(NSUInteger)lastUID
{
if (firstUID <= lastUID && firstUID > 0) {
LogInfo(@"sync existing %lu:%lu", (unsigned long) firstUID, (unsigned long) lastUID);
[_store sendCommand: IMAP_UID_FETCH_FLAGS info: nil
arguments: @"UID FETCH %u:%u (FLAGS)", firstUID, lastUID];
} else {
LogError(@"UID FETCH %lu:%lu (FLAGS)", (unsigned long) firstUID, (unsigned long) lastUID);
[_store signalFolderSyncError];
}
}
//
// This method simply close the selected mailbox (ie. folder)
//
- (void)close
{
IMAPCommand theCommand;
if (![self selected])
{
[_store removeFolderFromOpenFolders: self];
return;
}
// If we are opening a mailbox but -close was called before we
// finished opening it, we close the connection immediately.
theCommand = [[self store] lastCommand];
if (theCommand == IMAP_SELECT || theCommand == IMAP_UID_SEARCH || theCommand == IMAP_UID_SEARCH_ANSWERED ||
theCommand == IMAP_UID_SEARCH_FLAGGED || theCommand == IMAP_UID_SEARCH_UNSEEN)
{
[_store removeFolderFromOpenFolders: self];
[[self store] cancelRequest];
[[self store] reconnect];
return;
}
if (_cacheManager)
{
[_cacheManager synchronize];
}
// We set the _folder ivar to nil for all messages. This is required in case
// an IMAPMessage instance was retained and we invoke -setFlags: on it, which
// will try to access the _folder ivar in order to communicate with the IMAP server.
[self.allMessages makeObjectsPerformSelector: @selector(setFolder:) withObject: nil];
// We avoid to call IMAP_CLOSE and call SELECT for a non-existing mailbox (aka. folder).
// See: RFC4549-4.2.5
if ([_store isConnected] && ![self showDeleted])
{
[_store sendCommand: IMAP_SELECT info: nil arguments: @"SELECT \"%@\"",
[PantomimeFolderNameToIgnore modifiedUTF7String]];
}
else
{
PERFORM_SELECTOR_2([_store delegate], @selector(folderCloseCompleted:), PantomimeFolderCloseCompleted, self, @"Folder");
}
[_store removeFolderFromOpenFolders: self];
}
//
// This method returns all messages that have the flag PantomimeFlagDeleted.
//
- (void) expunge
{
//
// We send our EXPUNGE command. The responses will be processed in IMAPStore and
// the MSN will be updated in IMAPStore: -_parseExpunge.
//
[_store sendCommand: IMAP_EXPUNGE info: nil arguments: @"EXPUNGE"];
}
//
//
//
- (NSUInteger) UIDValidity
{
return _uid_validity;
}
//
//
//
- (void) setUIDValidity: (NSUInteger) theUIDValidity
{
_uid_validity = theUIDValidity;
if (_cacheManager)
{
if ([_cacheManager UIDValidity] == 0 || [_cacheManager UIDValidity] != _uid_validity)
{
[_cacheManager invalidate];
[_cacheManager setUIDValidity: _uid_validity];
}
}
}
//
//
//
- (BOOL) selected
{
return _selected;
}
//
//
//
- (void) setSelected: (BOOL) theBOOL
{
_selected = theBOOL;
}
//
//
//
- (void) setFlags: (CWFlags *) theFlags
messages: (NSArray *) theMessages
{
NSMutableString *aMutableString, *aSequenceSet;
CWIMAPMessage *aMessage;
if ([theMessages count] == 1)
{
aMessage = [theMessages lastObject];
// We set the flags right away, just in case someone asks for them
// just after invoking this method. Nevertheless, they WILL be set
// in IMAPStore: -_parseOK:.
// We do the same below, when the count > 1
[[aMessage flags] replaceWithFlags: theFlags];
aSequenceSet = [NSMutableString stringWithFormat: @"%lu:%lu",
(unsigned long)[aMessage UID], (unsigned long)[aMessage UID]];
}
else
{
NSUInteger i, count;
aSequenceSet = AUTORELEASE([[NSMutableString alloc] init]);
count = [theMessages count];
for (i = 0; i < count; i++)
{
aMessage = [theMessages objectAtIndex: i];
[[aMessage flags] replaceWithFlags: theFlags];
if (aMessage == [theMessages lastObject])
{
[aSequenceSet appendFormat: @"%lu", (unsigned long)[aMessage UID]];
}
else
{
[aSequenceSet appendFormat: @"%lu,", (unsigned long)[aMessage UID]];
}
}
}
aMutableString = [[NSMutableString alloc] init];
//
// If we're removing all flags, we rather send a STORE -FLAGS (<current flags>)
// than a STORE FLAGS (<new flags>) since some broken servers might not
// support it (like Cyrus v1.5.19 and v1.6.24).
//
if (theFlags->flags == 0 && aMessage)
{
[aMutableString appendFormat: @"UID STORE %@ -FLAGS.SILENT (", aSequenceSet];
[aMutableString appendString: [[aMessage flags] asString]];
[aMutableString appendString: @")"];
}
else
{
[aMutableString appendFormat: @"UID STORE %@ FLAGS.SILENT (", aSequenceSet];
[aMutableString appendString: [theFlags asString]];
[aMutableString appendString: @")"];
}
[_store sendCommand: IMAP_UID_STORE
info: [NSDictionary dictionaryWithObjectsAndKeys: theMessages,
PantomimeMessagesKey, theFlags, PantomimeFlagsKey, nil]
arguments: aMutableString];
RELEASE(aMutableString);
}
//
// Using IMAP, we ignore most parameters.
//
- (void) search: (NSString *) theString
mask: (PantomimeSearchMask) theMask
options: (PantomimeSearchOption) theOptions
{
NSString *aString;
switch (theMask)
{
case PantomimeFrom:
aString = [NSString stringWithFormat: @"UID SEARCH ALL FROM \"%@\"", theString];
break;
case PantomimeTo:
aString = [NSString stringWithFormat: @"UID SEARCH ALL TO \"%@\"", theString];
break;
case PantomimeContent:
aString = [NSString stringWithFormat: @"UID SEARCH ALL BODY \"%@\"", theString];
break;
case PantomimeSubject:
default:
aString = [NSString stringWithFormat: @"UID SEARCH ALL SUBJECT \"%@\"", theString];
}
// We send our SEARCH command. Store->searchResponse will have the result.
[_store sendCommand: IMAP_UID_SEARCH_ALL info: [NSDictionary dictionaryWithObject: self forKey: @"Folder"] arguments: aString];
}
- (NSUInteger) lastMSN
{
return self.allMessages.count;
}
- (NSUInteger) firstUID
{
return [[self allMessages] firstObject] ? [[[self allMessages] firstObject] UID] : 0;
}
- (NSUInteger) lastUID
{
return [[self allMessages] lastObject] ? [[[self allMessages] lastObject] UID] : 0;
}
- (CWIMAPMessage * _Nullable)messageByUID:(NSUInteger)uid
{
return nil;
}
- (void)matchUID:(NSUInteger)uid withMSN:(NSUInteger)msn
{
[self.msnToUidMap setObject:[NSNumber numberWithUnsignedInteger:uid]
forKey:[NSNumber numberWithUnsignedInteger:msn]];
[self.uidToMsnMap setObject:[NSNumber numberWithUnsignedInteger:msn]
forKey:[NSNumber numberWithUnsignedInteger:uid]];
}
- (NSUInteger)uidForMSN:(NSUInteger)msn
{
return [[self.msnToUidMap objectForKey:[NSNumber numberWithUnsignedInteger:msn]]
unsignedIntegerValue];
}
- (NSUInteger)msnForUID:(NSUInteger)uid
{
return [[self.uidToMsnMap objectForKey:[NSNumber numberWithUnsignedInteger:uid]]
unsignedIntegerValue];
}
- (BOOL)existsUID:(NSUInteger)uid
{
return [self.uidToMsnMap objectForKey:[NSNumber numberWithUnsignedInteger:uid]] != nil;
}
- (NSSet * _Nonnull)existingUIDs
{
return [NSSet setWithArray:self.uidToMsnMap.allKeys];
}
- (void)resetMatchedUIDs
{
[self.uidToMsnMap removeAllObjects];
[self.msnToUidMap removeAllObjects];
}
- (void)expungeMSN:(NSUInteger)msn
{
NSArray<NSNumber *> *keysAsc = [self.uidToMsnMap
keysSortedByValueUsingSelector:@selector(unsignedIntegerValue)];
if (keysAsc.count) {
NSUInteger lowest = [[keysAsc firstObject] unsignedIntegerValue];
NSNumber *highestKey = [keysAsc lastObject];
if (msn >= lowest) {
NSArray<NSNumber *> *keysDesc = [[keysAsc reverseObjectEnumerator] allObjects];
NSArray<NSNumber *> *toRework = [keysDesc
filteredArrayUsingPredicate:
[NSPredicate
predicateWithFormat:@"integerValue > %d", msn]];
for (NSNumber *num in toRework) {
NSNumber *value = [self.uidToMsnMap objectForKey:num];
[self.uidToMsnMap setObject:value forKey:[NSNumber numberWithInt:num.intValue - 1]];
}
[self.uidToMsnMap removeObjectForKey:highestKey];
}
}
}
@end
//
// Private methods
//
@implementation CWIMAPFolder (Private)
//
//
//
- (BOOL) _isInFetchedRange:(NSUInteger)uid
{
return [self firstUID] <= uid && uid <= [self lastUID];
}
//
//
//
- (BOOL) _wouldCreatedUpperFetchedRangeWithFrom:(NSUInteger)fromUid to:(NSUInteger)toUid
{
return [self previouslyFetchedMessagesExist] && fromUid > [self lastUID] + 1;
}
//
//
//
- (BOOL) _wouldCreatedLowerFetchedRangeWithFrom:(NSUInteger)fromUid to:(NSUInteger)toUid
{
return [self previouslyFetchedMessagesExist] && toUid < [self firstUID] - 1;
}
//
//
//
- (BOOL) _isNegativeUid:(NSInteger)uid
{
return (UNLIMITED < uid && uid < 0);
}
//
//
//
- (BOOL) _uidRangeAllreadyFetchedWithFrom:(NSUInteger)fromUid to:(NSUInteger)toUid
{
return [self _isInFetchedRange:fromUid] && [self _isInFetchedRange:toUid];
}
//
//
//
- (BOOL) _uidRangeOutOfExistsRangeWithFrom:(NSUInteger)fromUid to:(NSUInteger)toUid
{
return self.existsCount == 0 || fromUid == 0 || toUid > self.existsCount;
}
//
//
//
- (NSData *) _removeInvalidHeadersFromMessage: (NSData *) theMessage
{
// We allocate our mutable data object
NSMutableData *aMutableData = [[NSMutableData alloc] initWithCapacity: [theMessage length]];
// We now replace all \n by \r\n
[theMessage componentsSeparatedByCString:"\n" block:^(NSData *aLine,
NSUInteger count,
BOOL isLast) {
// We skip dumb headers
if ([aLine hasCPrefix: "From "]) {
return;
}
[aMutableData appendData: aLine];
[aMutableData appendCString: "\r\n"];
}];
return AUTORELEASE(aMutableData);
}
@end