2 Copyright (C) 2000-2004 SKYRIX Software AG
4 This file is part of OpenGroupware.org.
6 OGo is free software; you can redistribute it and/or modify it under
7 the terms of the GNU Lesser General Public License as published by the
8 Free Software Foundation; either version 2, or (at your option) any
11 OGo is distributed in the hope that it will be useful, but WITHOUT ANY
12 WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
14 License for more details.
16 You should have received a copy of the GNU Lesser General Public
17 License along with OGo; see the file COPYING. If not, write to the
18 Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
23 #include "NGImap4Folder.h"
24 #include "NGImap4Context.h"
25 #include "NGImap4Client.h"
26 #include "NGImap4Message.h"
27 #include "NGImap4Functions.h"
28 #include "NGImap4FolderGlobalID.h"
29 #include "NGImap4FolderMailRegistry.h"
30 #include "NGImap4FolderFlags.h"
33 @interface NGImap4Message(Private)
35 - (void)_setHeaders:(NGHashMap *)_headers
37 flags:(NSArray *)_flags;
40 - (void)setIsRead:(BOOL)_isRead;
42 @end /* NGImap4Message(Private) */
45 @interface NGImap4Context(Private)
46 - (void)setLastException:(NSException *)_exception;
47 - (void)setSelectedFolder:(NGImap4Folder *)_folder;
48 @end /* NGImap4Context(Private) */
50 @interface NGImap4Folder(Private)
53 - (void)_resetSubFolder;
55 - (NSArray *)initializeMessagesFrom:(unsigned)_from to:(unsigned)_to;
56 - (NSArray *)initializeMessages;
57 - (void)initializeSubFolders;
58 - (void)addSubFolder:(NGImap4Folder *)_folder;
59 - (BOOL)flag:(NSString*)_doof toMessages:(NSArray*)_msg add:(NSNumber*)_n;
60 - (BOOL)flagToAllMessages:(NSString *)_flag add:(NSNumber *)_add;
61 - (NSArray *)fetchMessagesFrom:(unsigned)_from to:(unsigned)_to;
63 - (void)resetQualifierCache;
65 - (void)setRecent:(NSNumber *)_rec exists:(NSNumber *)_exists;
66 - (void)clearParentFolder;
68 - (BOOL)_testMessages:(NSArray *)_msgs operation:(NSString *)_op;
69 - (NSArray *)_getMsnRanges:(NSArray *)_msgs;
70 - (NSArray *)_calculateSequences:(NSMutableArray *)_numbers count:(int)_cnt;
72 - (NSNotificationCenter *)notificationCenter;
73 - (void)_registerForNotifications;
75 @end /* NGImap4Folder(Private) */
78 @implementation NGImap4Folder
80 static NSNumber *YesNumber = nil;
81 static NSNumber *NoNumber = nil;
82 static NSArray *StatusFlags = nil;
83 static NSArray *UnseenFlag = nil;
84 static BOOL ImapDebugEnabled = NO;
86 static int ShowNonExistentFolder = -1;
87 static int IgnoreHasNoChildrenFlag = -1;
88 static int FetchNewUnseenMessagesInSubFoldersOnDemand = -1;
91 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
92 static BOOL didInit = NO;
96 YesNumber = [[NSNumber numberWithBool:YES] retain];
97 NoNumber = [[NSNumber numberWithBool:NO] retain];
99 StatusFlags = [[NSArray alloc]
100 initWithObjects:@"messages", @"recent", @"unseen", nil];
101 UnseenFlag = [[NSArray alloc] initWithObjects:@"unseen", nil];
103 ShowNonExistentFolder = [ud boolForKey:@"ShowNonExistentFolder"] ? 1 : 0;
104 IgnoreHasNoChildrenFlag = [ud boolForKey:@"IgnoreHasNoChildrenFlag"] ? 1 : 0;
105 ImapDebugEnabled = [ud boolForKey:@"ImapDebugEnabled"];
107 FetchNewUnseenMessagesInSubFoldersOnDemand =
108 [ud boolForKey:@"FetchNewUnseenMessagesInSubFoldersOnDemand"] ? 1 : 0;
113 [self logWithFormat:@"ERROR: cannot init NGImap4Folder with -init!"];
114 [self doesNotRecognizeSelector:_cmd];
118 - (void)_setupMessageCache {
119 #if USE_MESSAGE_CACHE
121 self->qualifierCache =
122 [[NSMutableArray alloc] initWithCapacity:MAX_QUALIFIER_CACHE];
123 self->messagesCache =
124 [[NSMutableArray alloc] initWithCapacity:MAX_QUALIFIER_CACHE];
128 - (id)initWithContext:_context
129 name:(NSString *)_name
130 flags:(NSArray *)_flags
131 parentFolder:(id<NGImap4Folder>)_folder
133 if ((self = [super init])) {
134 self->context = [_context retain];
135 self->flags = [[NGImap4FolderFlags alloc] initWithFlagArray:_flags];
136 self->name = [_name copy];
137 self->parentFolder = _folder;
138 self->mailRegistry = [[NGImap4FolderMailRegistry alloc] init];
140 /* mark as 'to be fetched' */
144 self->usedSpace = -1;
146 self->overQuota = -1;
148 self->failedFlags.status = NO;
149 self->failedFlags.select = NO;
150 self->failedFlags.quota = NO;
152 // TODO: this also looks pretty weird!
153 if ([[self->name lowercaseString] isEqualToString:@"/inbox"] &&
154 [self->flags doNotSelectFolder]) {
157 [self resetLastException];
159 res = [[self->context client] subscribe:[self absoluteName]];
161 if ([self lastException] != nil) {
166 if ([[res objectForKey:@"result"] boolValue])
167 [self->flags allowFolderSelect];
170 [self _registerForNotifications];
171 [self _setupMessageCache];
173 #if NGIMAP_FOLDER_DEBUG
174 return [self->context registerFolder:self];
181 [[self notificationCenter] removeObserver:self];
182 [self->context removeSelectedFolder:self];
183 [self->subFolders makeObjectsPerformSelector:@selector(clearParentFolder)];
185 [self->mailRegistry release];
186 [self->msn2UidCache release];
187 [self->context release];
188 [self->flags release];
189 [self->name release];
190 [self->subFolders release];
191 [self->messageFlags release];
193 #if USE_MESSAGE_CACHE
194 [self->messages release];
195 [self->qualifierCache release];
196 [self->messagesCache release];
198 self->isReadOnly = nil;
199 self->parentFolder = nil;
204 - (BOOL)isEqual:(id)_obj {
207 if ([_obj isKindOfClass:[NGImap4Folder class]])
208 return [self isEqualToImap4Folder:_obj];
212 - (BOOL)isEqualToImap4Folder:(NGImap4Folder *)_folder {
215 if (([[_folder absoluteName] isEqualToString:self->name]) &&
216 [_folder context] == self->context) {
224 - (NSException *)lastException {
225 return [self->context lastException];
227 - (void)resetLastException {
228 [self->context resetLastException];
231 - (NGImap4Context *)context {
232 return self->context;
236 return [[self absoluteName] lastPathComponent];
239 - (NSString *)absoluteName {
244 return [self->flags flagArray];
247 - (NSArray *)messages {
248 return [self initializeMessages];
251 - (BOOL)_checkResult:(NSDictionary *)_dict cmd:(const char *)_command {
252 return _checkResult(self->context, _dict, _command);
255 - (NSArray *)messagesForQualifier:(EOQualifier *)_qualifier
258 // TODO: split up method
259 NSMutableArray *mes = nil;
260 NSMutableArray *msn = nil;
261 NSDictionary *dict = nil;
262 NSAutoreleasePool *pool = nil;
264 if ([self->flags doNotSelectFolder] || self->failedFlags.select)
267 if (![self->context registerAsSelectedFolder:self])
270 pool = [[NSAutoreleasePool alloc] init];
272 #if USE_MESSAGE_CACHE
273 if (self->cacheIdx > 0) {
274 NSEnumerator *qualifierEnum = nil;
275 EOQualifier *qual = nil;
278 qualifierEnum = [self->qualifierCache objectEnumerator];
280 while ((qual = [qualifierEnum nextObject])) {
281 if ([qual isEqual:_qualifier]) {
284 m = [[self->messagesCache objectAtIndex:cnt] retain];
286 return [m autorelease];
292 [self resetLastException];
294 dict = [[self->context client] searchWithQualifier:_qualifier];
296 if (![self _checkResult:dict cmd:__PRETTY_FUNCTION__])
299 msn = [[dict objectForKey:@"search"] mutableCopy];
301 if ((msn == nil) || ([msn count] == 0)) {
302 mes = [NSArray array];
305 NSEnumerator *seq = nil;
306 NSDictionary *obj = nil;
308 mes = [NSMutableArray arrayWithCapacity:512];
309 seq = [[self _calculateSequences:msn count:_cnt] objectEnumerator];
310 while ((obj = [seq nextObject])) {
313 a = [self fetchMessagesFrom:
314 [[obj objectForKey:@"start"] unsignedIntValue]
315 to:[[obj objectForKey:@"end"] unsignedIntValue]];
317 if ([self lastException] != nil)
321 [mes addObjectsFromArray:a];
323 mes = [[mes copy] autorelease];
327 #if USE_MESSAGE_CACHE
328 if (self->cacheIdx == 5)
331 if ([self->qualifierCache count] == self->cacheIdx)
332 [self->qualifierCache addObject:_qualifier];
334 [self->qualifierCache replaceObjectAtIndex:self->cacheIdx
335 withObject:_qualifier];
336 if ([self->messagesCache count] == self->cacheIdx)
337 [self->messagesCache addObject:mes];
339 [self->messagesCache replaceObjectAtIndex:self->cacheIdx
348 if ([self lastException]) {
352 return [mes autorelease];
355 - (NSArray *)messagesForQualifier:(EOQualifier *)_qualifier {
356 return [self messagesForQualifier:_qualifier maxCount:-1];
359 - (NSArray *)messageFlags {
360 if (self->messageFlags == nil)
361 [self->context registerAsSelectedFolder:self];
362 return self->messageFlags;
365 - (NSArray *)subFolders {
366 if (self->subFolders == nil)
367 [self initializeSubFolders];
368 return self->subFolders;
371 - (NGImap4Folder *)subFolderWithName:(NSString *)_name
372 caseInsensitive:(BOOL)_caseIns {
373 return _subFolderWithName(self, _name, _caseIns);
376 - (id<NGImap4Folder>)parentFolder {
377 return self->parentFolder;
381 if (self->isReadOnly == nil)
382 [self->context registerAsSelectedFolder:self];
383 return (self->isReadOnly == YesNumber) ? YES : NO;
389 return [self->flags doNotSelectFolder];
391 - (BOOL)noinferiors {
392 return [self->flags doesNotSupportSubfolders];
394 - (BOOL)nonexistent {
395 return [self->flags doesNotExist];
397 - (BOOL)haschildren {
398 return [self->flags hasSubfolders];
400 - (BOOL)hasnochildren {
401 return [self->flags hasNoSubfolders];
404 return [self->flags isMarked];
407 return [self->flags isUnmarked];
411 if (self->exists == -1) {
418 if (self->recent == -1)
425 if (self->unseen == -1)
431 - (BOOL)isOverQuota {
432 if (self->overQuota == -1)
433 [self->context registerAsSelectedFolder:self];
435 return (self->overQuota == 1)? YES : NO;
439 if (self->usedSpace == -1)
442 return self->usedSpace;
446 if (self->maxQuota == -1)
449 return self->maxQuota;
452 - (BOOL)hasNewMessagesSearchRecursiv:(BOOL)_rec fetchOnDemand:(BOOL)_fetch {
454 if (([self recent] > 0) && ([self unseen] > 0))
458 if ((self->recent > 0) && (self->unseen > 0))
463 return _hasNewMessagesInSubFolder(self, _fetch);
468 - (BOOL)hasNewMessagesSearchRecursiv:(BOOL)_recursiv {
469 if (([self recent] > 0) && ([self unseen] > 0))
473 return _hasNewMessagesInSubFolder(self,
474 FetchNewUnseenMessagesInSubFoldersOnDemand);
479 - (BOOL)hasUnseenMessagesSearchRecursiv:(BOOL)_rec fetchOnDemand:(BOOL)_fetch {
481 if ([self unseen] > 0)
485 if (self->unseen > 0)
489 return _hasUnseenMessagesInSubFolder(self, _fetch);
494 - (BOOL)hasUnseenMessagesSearchRecursiv:(BOOL)_recursiv {
495 if ([self unseen] > 0)
500 _hasUnseenMessagesInSubFolder(self,
501 FetchNewUnseenMessagesInSubFoldersOnDemand);
505 /* notifications (fix that junk!) */
507 - (NSNotificationCenter *)notificationCenter {
508 static NSNotificationCenter *nc = nil;
510 nc = [[NSNotificationCenter defaultCenter] retain];
514 - (NSString *)resetFolderNotificationName {
515 return [@"NGImap4FolderReset_" stringByAppendingString:[self absoluteName]];
517 - (NSString *)resetSubfolderNotificationName {
518 return [@"NGImap4SubFolderReset__"
519 stringByAppendingString:[self absoluteName]];
522 - (void)_registerForNotifications {
523 NSNotificationCenter *nc;
526 nc = [self notificationCenter];
527 n = [self absoluteName];
529 // TODO: fix that junk!
530 if ([n length] > 0) {
531 [nc addObserver:self selector:@selector(_resetFolder)
532 name:[self resetFolderNotificationName]
534 [nc addObserver:self selector:@selector(_resetSubFolder)
535 name:[self resetSubfolderNotificationName]
540 - (void)_postResetFolderNotification {
541 [[self notificationCenter] postNotificationName:
542 [self resetFolderNotificationName]
545 - (void)_postResetSubfolderNotification {
546 [[self notificationCenter] postNotificationName:
547 [self resetSubfolderNotificationName]
551 /* private methods */
553 - (NSArray *)initializeMessages {
554 return [self initializeMessagesFrom:0 to:[self exists]];
557 - (NSArray *)initializeMessagesFrom:(unsigned)_from to:(unsigned)_to {
558 #if USE_MESSAGE_CACHE
559 if (self->messages == nil) {
560 self->messages = [[NSMutableArray alloc] initWithCapacity:_to];
562 [self->messages addObjectsFromArray:[self fetchMessagesFrom:_from to:_to]];
563 return self->messages;
565 return [self fetchMessagesFrom:_from to:_to];
569 - (NGImap4Message *)createMessageForUid:(unsigned)_uid
570 headers:(id)_headers size:(unsigned)_size flags:(NSArray *)_flags
572 return [[NGImap4Message alloc] initWithUid:_uid
573 headers:_headers size:_size flags:_flags
574 folder:self context:self->context];
577 - (NSArray *)_buildMessagesFromFetch:(NSDictionary *)_fetch
578 usingMessages:(NSDictionary *)_messages
582 NGMimeMessageParser *parser;
584 NSAutoreleasePool *pool;
586 pool = [[NSAutoreleasePool alloc] init];
587 mEnum = [[_fetch objectForKey:@"fetch"] objectEnumerator];
590 if (_messages == nil)
591 mes = [[NSMutableArray alloc] initWithCapacity:512];
593 parser = [[[NGMimeMessageParser alloc] init] autorelease];
594 // TODO: should we disable parsing of some headers? but which?
595 // is this method only for parsing headers?!
597 while ((m = [mEnum nextObject])) {
598 NGDataStream *stream = nil;
600 id headers, uid, f, size;
602 headerData = [m objectForKey:@"header"];
603 uid = [m objectForKey:@"uid"];
604 f = [m objectForKey:@"flags"];
605 size = [m objectForKey:@"size"];
607 if (headerData == nil || uid == nil || f == nil || size == nil) {
608 [self logWithFormat:@"WARNING[%s]: got no header, uid, flags, size "
609 @"for %@", __PRETTY_FUNCTION__, m];
612 if (([f containsObject:@"recent"]) && ([f containsObject:@"seen"])) {
614 [f removeObject:@"recent"];
619 stream = [[NGDataStream alloc] initWithData:headerData
620 mode:NGStreamMode_readOnly];
621 [parser prepareForParsingFromStream:stream];
622 [stream release]; stream = nil;
625 headers = [parser parseHeader];
630 if ((msg = [_messages objectForKey:uid]) == nil) {
631 [self logWithFormat:@"WARNING[%s]: missing message for uid %@ from "
632 @"fetch %@ in dict %@", __PRETTY_FUNCTION__,
633 uid, _fetch, _messages];
636 [msg _setHeaders:headers size:[size intValue] flags:f];
641 m = [self createMessageForUid:[uid unsignedIntValue]
642 headers:headers size:[size unsignedIntValue] flags:f];
643 if (m) [mes addObject:m];
648 [mes release]; mes = nil;
651 return [m autorelease];;
654 - (NSArray *)_buildMessagesFromFetch:(NSDictionary *)_fetch {
655 return [self _buildMessagesFromFetch:_fetch usingMessages:nil];
658 - (NSArray *)_messageIds:(NSArray *)_so onlyUnseen:(BOOL)_unseen {
659 NSAutoreleasePool *pool;
662 static EOQualifier *UnseenQual = nil;
666 /* hack for sorting for unseen/seen */
667 if (UnseenQual == nil) {
668 UnseenQual = [[EOKeyValueQualifier alloc]
671 EOQualifierOperatorEqual
675 pool = [[NSAutoreleasePool alloc] init];
677 if ([_so count] == 1) {
680 so = [_so lastObject];
682 if ([[so key] isEqualToString:@"unseen"]) {
683 static NSArray *DateSo = nil;
684 static EOQualifier *SeenQual = nil;
686 NSMutableArray *muids;
687 EOQualifier *qual1, *qual2;
690 DateSo = [[NSArray alloc] initWithObjects:
691 [EOSortOrdering sortOrderingWithKey:@"date"
692 selector:[so selector]], nil];
694 if (SeenQual == nil) {
695 SeenQual = [[EOKeyValueQualifier alloc]
698 EOQualifierOperatorEqual
701 muids = [[NSMutableArray alloc] initWithCapacity:255];
703 if (sel_eq([so selector], EOCompareAscending) ||
704 sel_eq([so selector], EOCompareCaseInsensitiveAscending)) {
721 dict = [[self->context client] sort:DateSo qualifier:qual1];
723 if (![[dict objectForKey:@"result"] boolValue]) {
724 [self logWithFormat:@"ERROR[%s](1): sort failed (sortOrderings %@, "
725 @"qual1 %@)", __PRETTY_FUNCTION__, DateSo, qual1];
728 [muids addObjectsFromArray:[dict objectForKey:@"sort"]];
731 dict = [[self->context client] sort:DateSo qualifier:qual2];
733 if (![[dict objectForKey:@"result"] boolValue]) {
734 [self logWithFormat:@"ERROR[%s](2): sort failed (sortOrderings %@, "
735 @"qual2 %@ ", __PRETTY_FUNCTION__, DateSo, qual2];
738 [muids addObjectsFromArray:[dict objectForKey:@"sort"]];
741 [muids release]; muids = nil;
748 static NSArray *ArrivalSO = nil;
750 if (ArrivalSO == nil) {
754 [EOSortOrdering sortOrderingWithKey:@"arrival"
755 selector:EOCompareAscending], nil];
765 [self resetLastException];
767 dict = [[self->context client] sort:_so qualifier:qual];
769 if (![self _checkResult:dict cmd:__PRETTY_FUNCTION__])
772 uids = [[dict objectForKey:@"sort"] retain];
774 [pool release]; pool = nil;
776 return [uids autorelease];
779 - (NSData *)blobForUid:(unsigned)_mUid part:(NSString *)_part {
780 NSDictionary *result;
781 NSArray *fetchResults;
783 NSArray *uids, *parts;
785 if (![self->context registerAsSelectedFolder:self])
788 bodyKey = [NSString stringWithFormat:@"body[%@]", _part?_part:@""];
789 uids = [NSArray arrayWithObject:[NSNumber numberWithUnsignedInt:_mUid]];
790 parts = [NSArray arrayWithObject:bodyKey];
792 result = [[self->context client] fetchUids:uids parts:parts];
793 if (![self _checkResult:result cmd:__PRETTY_FUNCTION__]) {
794 [self debugWithFormat:@"Note: _checkResult: rejected result %d/%@: %@",
795 _mUid, _part, result];
798 else if (result == nil) {
799 [self debugWithFormat:@"Note: got no result for %d/%@", _mUid, _part];
803 fetchResults = [result objectForKey:@"fetch"];
804 if ([fetchResults count] == 0)
805 [self debugWithFormat:@"found no fetch result"];
807 // TODO: using 'lastObject' is certainly wrong? need to search for body
808 result = [fetchResults lastObject];
810 if ((result = [result objectForKey:@"body"]) == nil)
811 [self debugWithFormat:@"found no body in fetch results: %@", fetchResults];
813 return [result objectForKey:@"data"];
816 - (NGImap4Message *)messageForUid:(unsigned)_mUid
817 sortOrderings:(NSArray *)_so
818 onlyUnread:(BOOL)_unread
819 nextMessage:(BOOL)_next
821 NSArray *uids, *allSortUids;
822 NSEnumerator *enumerator;
823 NSNumber *uid, *eUid, *lastUid;
825 if ([self->flags doNotSelectFolder] || self->failedFlags.select)
828 uid = [NSNumber numberWithUnsignedInt:_mUid];
829 allSortUids = [self _messageIds:_so onlyUnseen:NO];
830 uids = _unread ? [self _messageIds:_so onlyUnseen:_unread] : nil;
831 enumerator = [allSortUids objectEnumerator];
834 while ((eUid = [enumerator nextObject])) {
835 if ([uid isEqual:eUid])
839 if ([uids containsObject:eUid])
846 [self logWithFormat:@"WARNING[%s]: Couldn`t found next/prev message "
847 @"(missing orig. message %d for sortordering %@",
848 __PRETTY_FUNCTION__, _mUid, _so];
853 while ((uid = [enumerator nextObject])) {
854 if ([uids containsObject:uid])
859 uid = [enumerator nextObject];
867 return [[[NGImap4Message alloc] initWithUid:[uid unsignedIntValue]
868 folder:self context:self->context]
873 build NGImap4Messages with sorted uids
876 - (NSArray *)fetchSortedMessages:(NSArray *)_so {
877 NSArray *uids, *array;
878 NSMutableArray *marray;
879 NSAutoreleasePool *pool;
880 NSEnumerator *enumerator;
883 if ([self->flags doNotSelectFolder] || self->failedFlags.select)
886 if (![self->context registerAsSelectedFolder:self])
889 pool = [[NSAutoreleasePool alloc] init];
892 return [self messages];
895 if (!(uids = [self _messageIds:_so onlyUnseen:NO]))
896 return [self messages];
898 enumerator = [uids objectEnumerator];
899 marray = [[NSMutableArray alloc] initWithCapacity:[uids count]];
901 while ((uid = [enumerator nextObject])) {
904 m = [[NGImap4Message alloc] initWithUid:[uid intValue]
905 folder:self context:self->context];
906 if (m) [marray addObject:m];
909 array = [marray shallowCopy];
911 [marray release]; marray = nil;
912 [pool release]; pool = nil;
914 return [array autorelease];
918 fetch headers for _array in range (for use with fetchSortedMessages)
921 - (void)bulkFetchHeadersFor:(NSArray *)_array inRange:(NSRange)_aRange
922 withAllUnread:(BOOL)_unread
924 NSArray *messages, *uids;
925 NSAutoreleasePool *pool;
926 NSEnumerator *enumerator;
927 NGImap4Message *message;
928 NSMutableDictionary *messageMapping;
932 if ([self->flags doNotSelectFolder])
935 if (_aRange.length == 0)
938 if (![self->context registerAsSelectedFolder:self])
941 pool = [[NSAutoreleasePool alloc] init];
943 if (_aRange.location >= [_array count]) {
946 if (_aRange.location + _aRange.length > [_array count]) {
947 _aRange.length = [_array count] - _aRange.location;
949 messages = [_array subarrayWithRange:_aRange];
956 q = [EOQualifier qualifierWithQualifierFormat:@"flags = \"unseen\""];
957 d = [[self->context client] searchWithQualifier:q];
959 if ([[d objectForKey:@"result"] boolValue])
960 unreadUids = [d objectForKey:@"search"];
962 enumerator = [messages objectEnumerator];
963 messageMapping = [NSMutableDictionary dictionaryWithCapacity:
965 while ((message = [enumerator nextObject])) {
966 if (![message isComplete])
967 [messageMapping setObject:message
968 forKey:[NSNumber numberWithUnsignedInt:[message uid]]];
970 if ([ unreadUids count]) {
971 enumerator = [_array objectEnumerator];
972 while ((message = [enumerator nextObject])) {
975 number = [NSNumber numberWithUnsignedInt:[message uid]];
977 if ([unreadUids containsObject:number])
978 [messageMapping setObject:message forKey:number];
981 if ([messageMapping count] > 0) {
982 static NSArray *sortKeys = nil;
984 uids = [messageMapping allKeys];
986 if (sortKeys == nil) {
987 sortKeys = [[NSArray alloc] initWithObjects:@"uid",
989 @"rfc822.size", @"flags",
992 dict = [[self->context client] fetchUids:uids parts:sortKeys];
993 if ([self _checkResult:dict cmd:__PRETTY_FUNCTION__]) {
994 [self _buildMessagesFromFetch:dict usingMessages:messageMapping];
996 if (_unread) { /* set unfetched messeges to unread */
997 NSEnumerator *enumerator;
1000 enumerator = [_array objectEnumerator];
1002 while ((m = [enumerator nextObject])) {
1005 n = [NSNumber numberWithUnsignedInt:[m uid]];
1007 if (![uids containsObject:n])
1013 [pool release]; pool = nil;
1017 - (void)bulkFetchHeadersFor:(NSArray *)_array inRange:(NSRange)_aRange {
1018 [self bulkFetchHeadersFor:_array inRange:_aRange withAllUnread:NO];
1022 fetch only sorted messages in range
1025 - (NSArray *)fetchSortedMessages:(NSRange)_aRange
1026 sortOrderings:(NSArray *)_so
1028 static NSArray *sortKeys = nil;
1031 NSAutoreleasePool *pool;
1033 if ([self->flags doNotSelectFolder] || self->failedFlags.select)
1036 if (_aRange.length == 0)
1037 return [NSArray array];
1039 if (![self->context registerAsSelectedFolder:self])
1042 pool = [[NSAutoreleasePool alloc] init];
1044 if ((uids = [self _messageIds:_so onlyUnseen:NO]) == nil)
1047 if (_aRange.location + _aRange.length > [uids count])
1048 _aRange.length = [uids count] - _aRange.location;
1050 uids = [uids subarrayWithRange:_aRange];
1052 if (sortKeys == nil) {
1053 sortKeys = [[NSArray alloc] initWithObjects:@"uid",
1055 @"rfc822.size", @"flags",
1058 dict = [[self->context client] fetchUids:uids parts:sortKeys];
1060 if (![self _checkResult:dict cmd:__PRETTY_FUNCTION__])
1063 m = [[self _buildMessagesFromFetch:dict] retain];
1065 return [m autorelease];
1068 - (NSArray *)fetchMessagesFrom:(unsigned)_from to:(unsigned)_to {
1069 static NSArray *sortKeys = nil;
1070 NSAutoreleasePool *pool;
1074 if ([self->flags doNotSelectFolder])
1080 if (![self->context registerAsSelectedFolder:self])
1084 return [NSArray array];
1086 pool = [[NSAutoreleasePool alloc] init];
1088 [self resetLastException];
1090 /* TODO: normalize sort-key arrays? */
1091 if (sortKeys == nil) {
1092 sortKeys = [[NSArray alloc] initWithObjects:@"uid",
1094 @"rfc822.size", @"flags",
1097 dict = [[self->context client] fetchFrom:_from to:_to parts:sortKeys];
1098 if (![self _checkResult:dict cmd:__PRETTY_FUNCTION__])
1101 m = [[self _buildMessagesFromFetch:dict] retain];
1104 return [m autorelease];
1107 - (void)initializeSubFolders {
1109 NSEnumerator *folders;
1113 BOOL showSubsrcFolders;
1116 if ([self->flags doesNotSupportSubfolders])
1119 if (!IgnoreHasNoChildrenFlag && [self->flags hasNoSubfolders])
1122 if (self->subFolders)
1123 [self resetSubFolders];
1125 [self resetLastException];
1127 showSubsrcFolders = [self->context showOnlySubscribedInSubFolders];
1129 pattern = [[self absoluteName] stringByAppendingString:@"/%"];
1130 res = (showSubsrcFolders)
1131 ? [[self->context client] lsub:@"" pattern:pattern]
1132 : [[self->context client] list:@"" pattern:pattern];
1134 if (![self _checkResult:res cmd:__PRETTY_FUNCTION__])
1137 res = [res objectForKey:@"list"];
1139 objs = calloc([res count] + 2, sizeof(id));
1142 names = [res allKeys];
1143 names = [names sortedArrayUsingSelector:
1144 @selector(caseInsensitiveCompare:)];
1145 folders = [names objectEnumerator];
1149 if (showSubsrcFolders) {
1150 n = [self absoluteName];
1154 while ((folder = [folders nextObject])) {
1155 NGImap4Folder *newFolder;
1158 f = [res objectForKey:folder];
1160 if (!ShowNonExistentFolder) {
1161 if ([f containsObject:@"nonexistent"])
1164 newFolder = [NGImap4Folder alloc]; /* to keep gcc happy */
1165 objs[cnt] = [[newFolder initWithContext:self->context
1166 name:folder flags:f parentFolder:self]
1168 if (objs[cnt] == nil)
1173 self->subFolders = [[NSArray alloc] initWithObjects:objs count:cnt];
1175 if (objs) free(objs);
1179 return [self selectImmediately:NO];
1182 - (BOOL)selectImmediately:(BOOL)_imm {
1185 if ([self->flags doNotSelectFolder]) {
1186 [self logWithFormat:@"WARNING[%s]: try to select folder with noselect "
1187 @"flag <%@>", __PRETTY_FUNCTION__, self];
1190 if (self->failedFlags.select)
1194 if ([[self context] isInSyncMode] && self->selectSyncState)
1197 [self resetLastException];
1199 dict = [[self->context client] select:[self absoluteName]];
1200 if (![self _checkResult:dict cmd:__PRETTY_FUNCTION__]) {
1201 self->failedFlags.select = YES;
1204 [self->context setSelectedFolder:self];
1206 ASSIGN(self->messageFlags, [dict objectForKey:@"flags"]);
1209 [[dict objectForKey:@"access"] isEqualToString:@"READ-WRITE"]
1210 ? NoNumber : YesNumber;
1212 // TODO: isn't there a better way to check for overquota?
1213 if ([[dict objectForKey:@"alert"] isEqualToString:@"Mailbox is over quota"])
1214 self->overQuota = 1;
1216 self->overQuota = 0;
1218 [self setRecent:[dict objectForKey:@"recent"]
1219 exists:[dict objectForKey:@"exists"]];
1221 self->maxQuota = -1;
1222 self->usedSpace = -1;
1223 self->failedFlags.quota = NO;
1224 self->selectSyncState = YES;
1232 if ([self->flags doNotSelectFolder])
1235 if (self->failedFlags.status)
1238 [self->context resetLastException];
1240 dict = [[self->context client] status:[self absoluteName] flags:StatusFlags];
1242 if (![self _checkResult:dict cmd:__PRETTY_FUNCTION__]) {
1246 self->maxQuota = -1;
1247 self->usedSpace = -1;
1248 self->overQuota = -1;
1249 self->failedFlags.status = YES;
1250 self->failedFlags.quota = NO;
1254 [self setRecent:[dict objectForKey:@"recent"]
1255 exists:[dict objectForKey:@"messages"]];
1256 self->unseen = [[dict objectForKey:@"unseen"] intValue];
1263 - (void)resetFolder {
1264 // TODO: shouldn't the post happen after the expunge?
1265 [self _postResetFolderNotification];
1269 - (void)_resetFolder {
1270 #if USE_MESSAGE_CACHE
1271 [self resetQualifierCache];
1273 [self->msn2UidCache release]; self->msn2UidCache = nil;
1274 [self->flags release]; self->flags = nil;
1275 [self->messageFlags release]; self->messageFlags = nil;
1276 #if USE_MESSAGE_CACHE
1277 [self->messages release]; self->messages = nil;
1279 self->maxQuota = -1;
1280 self->usedSpace = -1;
1281 self->overQuota = -1;
1282 self->failedFlags.select = NO;
1283 self->failedFlags.quota = NO;
1284 self->isReadOnly = nil;
1288 - (void)resetStatus {
1292 self->failedFlags.status = NO;
1295 - (void)_resetSubFolder {
1298 ctx = [self context];
1300 if ((self->parentFolder == nil) || (self == [ctx inboxFolder]))
1301 [ctx resetSpecialFolders];
1303 [self->subFolders release]; self->subFolders = nil;
1306 - (void)resetSubFolders {
1307 // TODO: explain in detail what this does
1310 n = [self absoluteName];
1312 [self _postResetSubfolderNotification];
1314 [self _resetSubFolder];
1317 - (BOOL)renameTo:(NSString *)_name {
1321 if ([self isReadOnly])
1324 if ((_name == nil) || ([_name length] == 0))
1327 n = [[self->name stringByDeletingLastPathComponent]
1328 stringByAppendingPathComponent:_name];
1330 [self resetLastException];
1332 dict = [[self->context client] rename:[self absoluteName] to:n];
1334 if (![self _checkResult:dict cmd:__PRETTY_FUNCTION__])
1337 ASSIGNCOPY(self->name, n);
1338 [self->globalID release]; self->globalID = nil;
1340 [self resetSubFolders];
1342 dict = [[self->context client] subscribe:self->name];
1344 if (![self _checkResult:dict cmd:__PRETTY_FUNCTION__])
1352 - (BOOL)deleteSubFolder:(NGImap4Folder *)_folder {
1353 if ([self isReadOnly])
1355 return _deleteSubFolder(self, _folder);;
1358 - (BOOL)copySubFolder:(NGImap4Folder *)_f to:(NGImap4Folder *)_folder {
1359 return _copySubFolder(self, _f, _folder);
1362 - (BOOL)moveSubFolder:(NGImap4Folder *)_f to:(NGImap4Folder *)_folder {
1363 if ([self isReadOnly])
1365 return _moveSubFolder(self, _f, _folder);
1368 - (BOOL)createSubFolderWithName:(NSString *)_name {
1369 if ([self isReadOnly])
1371 return _createSubFolderWithName(self, _name, YES);
1377 if ([self->flags doNotSelectFolder] || self->failedFlags.select)
1380 if ([self isReadOnly])
1382 if (![self->context registerAsSelectedFolder:self])
1385 dict = [[self->context client] expunge];
1386 if (![self _checkResult:dict cmd:__PRETTY_FUNCTION__])
1389 [self setRecent:[dict objectForKey:@"recent"]
1390 exists:[dict objectForKey:@"exists"]];
1393 - (BOOL)addFlag:(NSString *)_flag toMessages:(NSArray *)_messages {
1394 return [self flag:_flag toMessages:_messages add:YesNumber];
1397 - (BOOL)removeFlag:(NSString *)_flag fromMessages:(NSArray *)_messages {
1398 return [self flag:_flag toMessages:_messages add:NoNumber];
1401 - (BOOL)flag:(NSString *)_flag toMessages:(NSArray *)_messages
1402 add:(NSNumber *)_add
1404 NSEnumerator *enumerator;
1405 NGImap4Message *message;
1410 add = [_add boolValue];
1412 if ([self->flags doNotSelectFolder])
1416 [self logWithFormat:@"WARNING[%s]: try to set an empty flag",
1417 __PRETTY_FUNCTION__];
1420 if ([self isReadOnly])
1423 if (![self->context registerAsSelectedFolder:self])
1426 if (![self _testMessages:_messages operation:@"store"])
1429 [self resetLastException];
1431 enumerator = [[self _getMsnRanges:_messages] objectEnumerator];
1432 if (enumerator == nil) {
1434 [self->context removeSelectedFolder:self];
1438 flagArray = [_flag isNotNull] ? [NSArray arrayWithObject:_flag] : nil;
1439 while ((obj = [enumerator nextObject])) {
1441 int objEnd, objStart;
1443 if ((objEnd = [[obj objectForKey:@"end"] unsignedIntValue]) <= 0)
1446 objStart = [[obj objectForKey:@"start"] unsignedIntValue];
1447 res = [[self->context client]
1448 storeFrom:objStart to:objEnd
1449 add:_add flags:flagArray];
1451 if (![self _checkResult:res cmd:__PRETTY_FUNCTION__])
1457 enumerator = [_messages objectEnumerator];
1458 while ((message = [enumerator nextObject])) {
1460 [message addFlag:_flag];
1462 [message removeFlag:_flag];
1468 - (BOOL)flagToAllMessages:(NSString *)_flag add:(NSNumber *)_add {
1469 #if USE_MESSAGE_CACHE
1470 BOOL add = [_add boolValue];
1471 NSEnumerator *enumerator = nil;
1472 NGImap4Message *m = nil;
1475 if ([self->flags doNotSelectFolder])
1481 if ([self isReadOnly])
1484 if (![self->context registerAsSelectedFolder:self])
1488 if ([self exists] > 0) {
1491 [self resetLastException];
1493 res = [[self->context client] storeFrom:0 to:[self exists]
1495 flags:[NSArray arrayWithObject:_flag]];
1497 if (![self _checkResult:res cmd:__PRETTY_FUNCTION__])
1500 #if USE_MESSAGE_CACHE
1501 enumerator = [self->messages objectEnumerator];
1502 while ((m = [enumerator nextObject])) {
1505 : [m removeFlag:_flag];
1512 - (BOOL)deleteAllMessages {
1513 if ([self isReadOnly])
1516 if ([self flagToAllMessages:@"Deleted" add:YesNumber]) {
1523 - (BOOL)deleteMessages:(NSArray *)_messages {
1524 if ([self isReadOnly])
1527 if ([self addFlag:@"Deleted" toMessages:_messages]) {
1534 - (BOOL)moveMessages:(NSArray *)_messages toFolder:(NGImap4Folder *)_folder {
1535 if ([self isReadOnly])
1538 if (_folder != nil) {
1539 if ([self copyMessages:_messages toFolder:_folder]) {
1540 return [self deleteMessages:_messages];
1546 - (BOOL)copyMessages:(NSArray *)_messages toFolder:(NGImap4Folder *)_folder {
1547 NSEnumerator *enumerator;
1549 NSString *folderName;
1551 if ([self->flags doNotSelectFolder])
1554 folderName = [_folder absoluteName];
1556 if (_folder == nil) {
1557 [self logWithFormat:@"WARNING[%s]: try to copy to nil folder",
1558 __PRETTY_FUNCTION__];
1561 [self resetLastException];
1563 if ([_folder isReadOnly]) {
1564 NGImap4ResponseException *exc;
1566 [self logWithFormat:@"WARNING[%s]: try to copy to readonly folder %@",
1567 __PRETTY_FUNCTION__, _folder];
1569 exc = [[NGImap4ResponseException alloc] initWithFormat:
1570 @"copy to read only folder"];
1571 [self->context setLastException:exc];
1572 [exc release]; exc = nil;
1575 if (![self->context registerAsSelectedFolder:self])
1578 if (![self _testMessages:_messages operation:@"copy"])
1581 enumerator = [[self _getMsnRanges:_messages] objectEnumerator];
1582 if (enumerator == nil) {
1584 [self->context removeSelectedFolder:self];
1588 [self resetLastException];
1589 if (![self->context registerAsSelectedFolder:self])
1592 while ((obj = [enumerator nextObject])) {
1595 objEnd = [[obj objectForKey:@"end"] unsignedIntValue];
1601 start = [[obj objectForKey:@"start"] unsignedIntValue];
1602 res = [[self->context client]
1603 copyFrom:start to:objEnd toFolder:folderName];
1604 if (![self _checkResult:res cmd:__PRETTY_FUNCTION__])
1608 [_folder resetFolder];
1609 return (obj == nil) ? YES : NO;
1612 - (BOOL)appendMessage:(NSData *)_msg {
1613 if ([self isReadOnly])
1619 dict = [[self->context client]
1620 append:_msg toFolder:[self absoluteName]
1621 withFlags:[NSArray arrayWithObject:@"seen"]];
1623 if ([self _checkResult:dict cmd:__PRETTY_FUNCTION__]) {
1631 #if USE_MESSAGE_CACHE
1632 - (void)resetQualifierCache {
1634 [self->qualifierCache removeAllObjects];
1640 - (void)setRecent:(NSNumber *)_rec exists:(NSNumber *)_exists {
1641 #if USE_MESSAGE_CACHE
1642 BOOL resetQualifier = NO;
1644 int tmp = [_rec intValue];
1645 if (self->recent != tmp) {
1647 resetQualifier = YES;
1650 if (_exists != nil) {
1651 int tmp = [_exists intValue];
1652 if (self->exists != tmp) {
1654 resetQualifier = YES;
1658 [self resetQualifierCache];
1661 if (_exists != nil) {
1664 e = [_exists intValue];
1674 r = [_rec intValue];
1676 if ((e != self->exists) || (r != self->recent)) {
1687 - (void)processResponse:(NSDictionary *)_dict {
1688 #if USE_MESSAGE_CACHE
1689 id exp = [_dict objectForKey:@"expunge"];
1692 [self setRecent:[_dict objectForKey:@"recent"]
1693 exists:[_dict objectForKey:@"exists"]];
1695 #if USE_MESSAGE_CACHE
1696 if ((exp != nil) && ([exp count] > 0) && ([self->messages count] > 0)) {
1697 NSEnumerator *enumerator;
1700 enumerator = [exp objectEnumerator];
1701 while ((obj = [enumerator nextObject])) {
1702 int n = [obj intValue] - 1;
1704 [self->messages removeObjectAtIndex:n];
1706 [self resetQualifierCache];
1712 id<NGImap4Folder> f, trash;
1714 trash = [self->context trashFolder];
1717 [self logWithFormat:@"WARNING[%s]: No trash folder was set",
1718 __PRETTY_FUNCTION__];
1722 for (f = self; f; f = [f parentFolder]) {
1723 if ([f isEqual:trash])
1730 NSEnumerator *enumerator;
1733 self->selectSyncState = NO;
1735 if (self->subFolders == nil)
1738 enumerator = [[self subFolders] objectEnumerator];
1739 while ((folder = [enumerator nextObject]))
1749 base = [self->context url];
1751 self->url = [[NSURL alloc]
1752 initWithScheme:[base scheme]
1754 path:[self absoluteName]];
1758 - (EOGlobalID *)serverGlobalID {
1759 return [self->context serverGlobalID];
1761 - (EOGlobalID *)globalID {
1763 return self->globalID;
1765 self->globalID = [[NGImap4FolderGlobalID alloc] initWithServerGlobalID:
1766 [self serverGlobalID]
1768 [self absoluteName]];
1769 return self->globalID;
1772 /* quota information */
1776 NSDictionary *quota;
1778 if (self->failedFlags.quota)
1781 if (![self->context canQuota]) {
1782 [self logWithFormat:@"WARNING[%s] call quota but capability contains"
1783 @" no quota string", __PRETTY_FUNCTION__];
1786 n = [self absoluteName];
1787 [self resetLastException];
1789 if ([self->flags doNotSelectFolder])
1792 quota = [[self->context client] getQuotaRoot:n];
1794 if (![self _checkResult:quota cmd:__PRETTY_FUNCTION__]) {
1795 self->failedFlags.quota = YES;
1799 quota = [quota objectForKey:@"quotas"];
1800 quota = [quota objectForKey:n];
1802 self->maxQuota = [[quota objectForKey:@"maxQuota"] intValue];
1803 self->usedSpace = [[quota objectForKey:@"usedSpace"] intValue];
1807 - (BOOL)_testMessages:(NSArray *)_messages operation:(NSString *)_operation {
1808 NSEnumerator *enumerator;
1811 enumerator = [_messages objectEnumerator];
1812 while ((obj = [enumerator nextObject])) {
1813 if ([obj folder] != self) {
1814 [self logWithFormat:@"ERROR: try to %@ mails in folder who didn`t own"
1815 @" this mail \nFolder %@\nMail %@ allMessages %@",
1816 _operation, self, obj, _messages];
1823 - (NSArray *)_getMsnRanges:(NSArray *)_messages {
1824 // TODO: might split up? document!
1825 static NSArray *UidKey = nil;
1828 NSMutableDictionary *map;
1829 NSMutableArray *msn;
1830 NSEnumerator *enumerator;
1832 NSAutoreleasePool *pool;
1833 NGImap4Message *message;
1835 if ([self exists] == 0)
1836 return [NSArray array];
1838 pool = [[NSAutoreleasePool alloc] init];
1840 if (UidKey == nil) {
1844 UidKey = [[NSArray alloc] initWithObjects:&objs count:1];
1847 [self resetLastException];
1849 if (![self->context registerAsSelectedFolder:self])
1852 if ([_messages count] > [self->msn2UidCache count]) {
1853 [self->msn2UidCache release];
1854 self->msn2UidCache = nil;
1857 if (!self->msn2UidCache) {
1860 res = [[self->context client] fetchFrom:1 to:[self exists] parts:UidKey];
1862 if (![self _checkResult:res cmd:__PRETTY_FUNCTION__])
1865 self->msn2UidCache = [[res objectForKey:@"fetch"] retain];
1867 map = [[NSMutableDictionary alloc] initWithCapacity:
1868 [self->msn2UidCache count]];
1869 enumerator = [self->msn2UidCache objectEnumerator];
1871 while ((obj = [enumerator nextObject]))
1872 [map setObject:[obj objectForKey:@"msn"] forKey:[obj objectForKey:@"uid"]];
1874 msn = [[NSMutableArray alloc] initWithCapacity:[_messages count]];
1875 enumerator = [_messages objectEnumerator];
1876 while ((message = [enumerator nextObject])) {
1879 m = [map objectForKey:[NSNumber numberWithUnsignedInt:[message uid]]];
1882 [self logWithFormat:@"WARNING[%s]: Couldn`t map a message sequence "
1883 @"number to message %@ numbers %@ messages %@ "
1884 @"self->msn2UidCache %@",
1885 __PRETTY_FUNCTION__, message, map, _messages, self->msn2UidCache];
1893 [map release]; map = nil;
1895 result = [self _calculateSequences:msn count:-1];
1897 result = [result retain];
1898 [msn release]; msn = nil;
1900 return [result autorelease];
1903 - (NSArray *)_calculateSequences:(NSMutableArray *)_numbers count:(int)_cnt {
1904 // TODO: might split up? document! This looks pretty weird
1905 NSAutoreleasePool *pool;
1906 NSEnumerator *enumerator;
1907 NSMutableDictionary *range;
1908 NSMutableArray *ranges;
1912 pool = [[NSAutoreleasePool alloc] init];
1915 _cnt = [_numbers count];
1917 [_numbers sortUsingSelector:@selector(compare:)];
1919 ranges = [NSMutableArray arrayWithCapacity:[_numbers count]];
1920 enumerator = [_numbers objectEnumerator];
1921 buffer = [NSNumber numberWithInt:0];
1924 while (((obj = [enumerator nextObject])) && (cntMsgs < _cnt)) {
1927 range = [NSMutableDictionary dictionaryWithCapacity:2];
1928 [range setObject:buffer forKey:@"start"];
1931 if ([obj intValue] != [buffer intValue] + 1) {
1934 [range setObject:buffer forKey:@"end"];
1936 [ranges addObject:ir];
1943 [range setObject:buffer forKey:@"end"];
1944 [ranges addObject:range];
1949 d = [[NSDictionary alloc] initWithObjectsAndKeys:
1950 buffer, @"start", buffer, @"end", nil];
1951 [ranges addObject:d];
1954 range = [ranges objectAtIndex:0];
1955 if ([[range objectForKey:@"end"] intValue] == 0)
1956 [ranges removeObjectAtIndex:0];
1958 obj = [ranges copy];
1960 return [obj autorelease];
1963 - (void)clearParentFolder {
1964 self->parentFolder = nil;
1967 /* message factory */
1969 - (id)messageWithUid:(unsigned int)_uid {
1970 return [[[NGImap4Message alloc]
1971 initWithUid:_uid folder:self context:[self context]]
1975 /* message registry */
1977 - (NGImap4FolderMailRegistry *)mailRegistry {
1978 return self->mailRegistry;
1983 - (BOOL)isDebuggingEnabled {
1984 return ImapDebugEnabled;
1989 - (NSString *)description {
1990 NSMutableString *ms;
1993 ms = [NSMutableString stringWithCapacity:64];
1995 [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])];
1997 if ((tmp = [self name]))
1998 [ms appendFormat:@" name=%@", tmp];
1999 if ((tmp = [self absoluteName]))
2000 [ms appendFormat:@" absolute=%@", tmp];
2001 if ((tmp = [[self flags] componentsJoinedByString:@","]))
2002 [ms appendFormat:@" flags=%@", tmp];
2004 [ms appendString:@">"];
2009 @end /* NGImap4Folder */
2012 @implementation NSData(xxxx)
2013 - (NSString *)description {
2014 return [NSString stringWithFormat:@"NSData len: %d",