2 Copyright (C) 2000-2005 SKYRIX Software AG
4 This file is part of SOPE.
6 SOPE 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 SOPE 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 SOPE; see the file COPYING. If not, write to the
18 Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
22 #include "NGImap4Message.h"
23 #include "NGImap4Folder.h"
24 #include "NGImap4Context.h"
25 #include "NGImap4Functions.h"
26 #include "NGImap4Client.h"
27 #include "NGImap4MessageGlobalID.h"
28 #include "NGImap4FolderMailRegistry.h"
29 #include <NGExtensions/NGFileManager.h>
32 #include "NGImap4Message+BodyStructure.h"
34 @class NSNotification;
36 @interface NGImap4Message(Internals)
38 - (void)initializeMessage;
41 - (void)generateBodyStructure;
42 - (NSString *)_addFlagNotificationName;
43 - (NSString *)_removeFlagNotificationName;
44 - (void)_removeFlag:(NSNotification *)_obj;
45 - (void)_addFlag:(NSNotification *)_obj;
46 - (void)setIsRead:(BOOL)_isRead;
48 @end /* NGImap4Message(Internals) */
50 #define USE_OWN_GLOBAL_ID 1
52 @implementation NGImap4Message
54 static Class NumClass = Nil;
55 static NSNumber *YesNumber = nil;
56 static NSNumber *NoNumber = nil;
57 static NGMimeHeaderNames *Fields = NULL;
58 static NSArray *CoreMsgAttrNames = nil;
59 static NSArray *bodyNameArray = nil;
60 static NSArray *rfc822NameArray = nil;
61 static BOOL debugFlags = NO;
62 static BOOL ImapDebugEnabled = NO;
69 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
70 static BOOL didInit = NO;
74 NumClass = [NSNumber class];
75 YesNumber = [[NumClass numberWithBool:YES] retain];
76 NoNumber = [[NumClass numberWithBool:NO] retain];
77 Fields = (NGMimeHeaderNames *)[NGMimePartParser headerFieldNames];
79 CoreMsgAttrNames = [[NSArray alloc] initWithObjects:@"rfc822.header",
80 @"rfc822.size", @"flags", nil];
81 bodyNameArray = [[NSArray alloc] initWithObjects:@"body", nil];
82 rfc822NameArray = [[NSArray alloc] initWithObjects:@"rfc822", nil];
84 ImapDebugEnabled = [ud boolForKey:@"ImapDebugEnabled"];
87 - (id)initWithUid:(unsigned)_uid folder:(NGImap4Folder *)_folder
88 context:(NGImap4Context *)_ctx
90 return [self initWithUid:_uid headers:nil size:-1 flags:nil
91 folder:_folder context:_ctx];
94 - (id)initWithUid:(unsigned)_uid headers:(NGHashMap *)_headers
95 size:(unsigned)_size flags:(NSArray *)_flags folder:(NGImap4Folder *)_folder
96 context:(NGImap4Context *)_ctx
98 if ((self = [super init])) {
101 self->folder = _folder;
102 self->headers = [_headers retain];
103 self->flags = [_flags retain];
104 self->context = [_ctx retain];
108 // Note: we can safely retain the registry since it doesn't retain the
110 self->mailRegistry = [[self->folder mailRegistry] retain];
111 [self->mailRegistry registerObject:self];
114 [self logWithFormat:@"WARNING(-init): not attached to a folder!"];
121 if (self->mailRegistry) {
122 [self->mailRegistry forgetObject:self];
123 [self->mailRegistry release];
126 [self logWithFormat:@"WARNING(-dealloc): not attached to a registry!"];
128 [self->headers release];
129 [self->flags release];
130 [self->context release];
131 [self->rawData release];
132 [self->message release];
133 [self->bodyStructure release];
135 [self->bodyStructureContent release];
136 [self->globalID release];
137 [self->removeFlagNotificationName release];
138 [self->addFlagNotificationName release];
143 /* internal methods */
145 - (void)_setHeaders:(NGHashMap *)_headers
147 flags:(NSArray *)_flags
149 ASSIGN(self->headers, _headers);
150 ASSIGN(self->flags, _flags);
155 return (self->headers != nil) && (self->flags != nil);
160 - (NSException *)lastException {
161 return [self->context lastException];
163 - (void)resetLastException {
164 [self->context resetLastException];
171 - (NGHashMap *)headers {
172 if (self->headers == nil)
173 [self initializeMessage];
174 return self->headers;
178 if (self->size == -1)
179 [self initializeMessage];
184 if (self->flags == nil)
185 [self initializeMessage];
189 - (NSData *)rawData {
190 if (self->rawData == nil)
192 return self->rawData;
195 - (NSData *)contentsOfPart:(NSString *)_part {
201 /* apparently caches the data for a part ID like "1.2.3" */
202 if (self->bodyStructureContent == nil) {
203 self->bodyStructureContent =
204 [[NSMutableDictionary alloc] initWithCapacity:8];
207 if ((result = [self->bodyStructureContent objectForKey:_part]) == nil) {
208 if ((result = [self->folder blobForUid:self->uid part:_part]) != nil)
209 [self->bodyStructureContent setObject:result forKey:_part];
214 - (id<NGMimePart>)bodyStructure {
215 if (self->bodyStructure == nil)
216 [self generateBodyStructure];
218 return self->bodyStructure;
221 - (id<NGMimePart>)message {
222 if (self->message == nil)
224 return self->message;
227 - (NGImap4Folder *)folder {
231 - (NGImap4Context *)context {
232 return self->context;
236 if ((self->flags == nil) && (self->isRead != -1))
237 return (self->isRead == 1) ? YES : NO;
239 return [[self flags] containsObject:@"seen"];
244 [self addFlag:@"seen"];
246 [self removeFlag:@"recent"];
251 [self removeFlag:@"seen"];
255 return [[self flags] containsObject:@"flagged"];
258 - (void)markFlagged {
259 if (![self isFlagged])
260 [self addFlag:@"flagged"];
263 - (void)markUnFlagged {
264 if ([self isFlagged]) {
265 [self removeFlag:@"flagged"];
270 return [[self flags] containsObject:@"answered"];
273 - (void)markAnswered {
274 if (![self isAnswered])
275 [self addFlag:@"answered"];
278 - (void)markNotAnswered {
279 if ([self isAnswered])
280 [self removeFlag:@"answered"];
283 - (void)addFlag:(NSString *)_flag {
289 if (self->mailRegistry)
290 [self->mailRegistry postFlagAdded:_flag inMessage:self];
292 [self logWithFormat:@"WARNING(-addFlag:): no folder attached to message!"];
294 if (![[self->folder messageFlags] containsObject:_flag])
297 if (![self->context registerAsSelectedFolder:self->folder])
299 res = [[self->context client] storeUid:self->uid add:YesNumber
300 flags:[NSArray arrayWithObject:_flag]];
301 if (!_checkResult(self->context, res, __PRETTY_FUNCTION__))
304 [self->folder resetStatus];
307 - (void)removeFlag:(NSString *)_flag {
310 if (_flag == nil) return;
312 if (self->mailRegistry)
313 [self->mailRegistry postFlagRemoved:_flag inMessage:self];
315 [self logWithFormat:@"WARNING(-remFlag:): no folder attached to message!"];
317 if (![[self->folder messageFlags] containsObject:_flag])
320 if (![self->context registerAsSelectedFolder:self->folder])
323 res = [[self->context client] storeUid:self->uid add:NoNumber
324 flags:[NSArray arrayWithObject:_flag]];
325 if (!_checkResult(self->context, res, __PRETTY_FUNCTION__))
328 [self->folder resetStatus];
333 - (BOOL)isEqual:(id)_obj {
336 if ([_obj isKindOfClass:[NGImap4Message class]])
337 return [self isEqualToNGImap4Message:_obj];
341 - (BOOL)isEqualToNGImap4Message:(NGImap4Message *)_messages {
342 if ([_messages uid] != self->uid)
344 if (![[_messages context] isEqual:self->context])
346 if (![[_messages folder] isEqual:self->folder])
356 - (EOGlobalID *)globalID {
357 #if USE_OWN_GLOBAL_ID
361 return self->globalID;
363 if ((fgid = [[self folder] globalID]) == nil) {
364 [self logWithFormat:@"WARNING(-globalID): got no globalID for folder: %@",
368 self->globalID = [[NGImap4MessageGlobalID alloc] initWithFolderGlobalID:fgid
370 return self->globalID;
375 return self->globalID;
377 // TODO: this needs to be invalidated, if a folder is moved!
379 keys[0] = [NumClass numberWithUnsignedInt:[self uid]];
380 keys[1] = [[self folder] absoluteName];
381 keys[2] = [[self context] host];
382 keys[3] = [[self context] login];
384 globalID = [[EOKeyGlobalID globalIDWithEntityName:@"NGImap4Message"
391 /* key-value coding */
393 - (id)valueForKey:(NSString *)_key {
394 // TODO: might want to add some more caching
398 if ((len = [_key length]) == 0)
401 if ([_key characterAtIndex:0] == 'N') {
404 else if ([_key isEqualToString:@"NSFileIdentifier"])
405 v = [[self headers] objectForKey:Fields->messageID];
406 else if ([_key isEqualToString:NSFileSize])
407 v = [NumClass numberWithInt:[self size]];
408 else if ([_key isEqualToString:NSFileModificationDate])
409 v = [[self headers] objectForKey:Fields->date];
410 else if ([_key isEqualToString:NSFileType])
411 v = NSFileTypeRegular;
412 else if ([_key isEqualToString:NSFileOwnerAccountName])
413 v = [self->context login];
414 else if ([_key isEqualToString:@"NGFileSubject"])
415 v = [[self headers] objectForKey:Fields->subject];
416 else if ([_key isEqualToString:@"NSFileSubject"])
417 v = [[self headers] objectForKey:Fields->subject];
418 else if ([_key isEqualToString:@"NGFilePath"])
419 v = [NSString stringWithFormat:@"%@/%d",
420 [self->folder absoluteName],
422 else if ([_key isEqualToString:@"url"])
428 if ((v = [[self headers] objectForKey:_key]))
431 return [super valueForKey:_key];
436 - (NSString *)description {
440 ms = [NSMutableString stringWithCapacity:128];
441 [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])];
442 [ms appendFormat:@" uid=%d size=%d", self->uid, self->size];
445 tmp = [[self headers] objectForKey:@"subject"];
446 if ([tmp length] > 0)
447 [ms appendFormat:@" subject='%@'", tmp];
449 [ms appendString:@" no-subject"];
452 [ms appendString:@" [header not fetched]"];
454 tmp = [self->folder absoluteName];
456 [ms appendFormat:@" folder='%@'", tmp];
462 [ms appendFormat:@"flags: (%@)", [tmp componentsJoinedByString:@", "]];
464 [ms appendString:@"no-flags"];
467 [ms appendString:@" [flags not fetched]"];
470 [ms appendString:@">"];
479 if (self->url) return self->url;
481 base = [self->folder url];
483 sprintf(buf, "%d", [self uid]);
484 s = [[NSString alloc] initWithCString:buf];
485 path = [[base path] stringByAppendingPathComponent:s];
488 self->url = [[NSURL alloc] initWithScheme:[base scheme]
496 - (void)initializeMessage {
499 NSAutoreleasePool *pool;
501 pool = [[NSAutoreleasePool alloc] init];
503 if (![self->context registerAsSelectedFolder:self->folder])
506 [self resetLastException];
508 dict = [[self->context client]
510 parts:CoreMsgAttrNames];
512 if (!_checkResult(self->context, dict, __PRETTY_FUNCTION__))
515 fetch = [[[dict objectForKey:@"fetch"] objectEnumerator] nextObject];
518 NSLog(@"WARNING[%s] : couldn`t fetch message with id %d",
519 __PRETTY_FUNCTION__, self->uid);
524 NGMimeMessageParser *parser;
525 NGDataStream *stream;
527 h = [fetch objectForKey:@"header"];
528 f = [fetch objectForKey:@"flags"];
529 s = [fetch objectForKey:@"size"];
531 if ((h == nil) || (f == nil) || (s == nil)) {
532 NSLog(@"WARNING[%s]: got no header, flags, size for %@",
533 __PRETTY_FUNCTION__, fetch);
536 parser = [[[NGMimeMessageParser alloc] init] autorelease];
537 stream = [[[NGDataStream alloc] initWithData:h] autorelease];
539 [parser prepareForParsingFromStream:stream];
541 ASSIGN(self->headers, [parser parseHeader]);
543 self->size = [s intValue];
545 if (([f containsObject:@"recent"]) && ([f containsObject:@"seen"])) {
546 f = [[f mutableCopy] autorelease];
547 [f removeObject:@"recent"];
549 ASSIGNCOPY(self->flags, f);
551 [pool release]; pool = nil;
554 - (void)fetchMessage {
555 NSDictionary *dict, *fetch;
557 if (![self->context registerAsSelectedFolder:self->folder])
560 [self resetLastException];
562 dict = [[self->context client] fetchUid:self->uid parts:rfc822NameArray];
563 if (!_checkResult(self->context, dict, __PRETTY_FUNCTION__))
566 fetch = [[[dict objectForKey:@"fetch"] objectEnumerator] nextObject];
569 NSLog(@"WARNING[%s]: couldn`t fetch message with id %d",
570 __PRETTY_FUNCTION__, self->uid);
573 ASSIGN(self->rawData, [fetch objectForKey:@"message"]);
576 - (void)_processBodyStructureEncoding:(NSDictionary *)bStruct {
580 orgH = [self headers];
582 if ([[orgH objectForKey:@"encoding"] length] != 0)
585 h = [[self headers] mutableCopy];
587 [h setObject:[[bStruct objectForKey:@"encoding"] lowercaseString]
588 forKey:@"content-transfer-encoding"];
590 ASSIGNCOPY(self->headers, h);
593 - (void)generateBodyStructure {
594 NSDictionary *dict, *bStruct;
595 NSArray *fetchResponses;
597 [self->bodyStructure release]; self->bodyStructure = nil;
599 [self resetLastException];
601 if (![self->context registerAsSelectedFolder:self->folder])
604 dict = [[self->context client] fetchUid:self->uid parts:bodyNameArray];
606 if (!(_checkResult(self->context, dict, __PRETTY_FUNCTION__)))
610 TODO: the following seems to fail with Courier, see OGo bug #800:
612 C[0x8b4e754]: 27 uid fetch 635 (body)
613 S[0x8c8b4e4]: * 627 FETCH (UID 635 BODY
614 ("text" "plain" ("charset" "iso-8859-1" "format" "flowed")
615 NIL NIL "8bit" 2474 51))
616 S[0x8c8b4e4]: * 627 FETCH (FLAGS (\Seen))
617 S[0x8c8b4e4]: 27 OK FETCH completed.
618 Jun 26 18:38:15 OpenGroupware [30904]: <0x08A73DCC[NGImap4Message]>
619 WARNING[-[NGImap4Message generateBodyStructure]]: could not fetch body of
623 fetchResponses = [dict objectForKey:@"fetch"];
624 if ([fetchResponses count] == 1) {
625 /* like with Cyrus, "old" behaviour */
627 [(NSDictionary *)[fetchResponses lastObject] objectForKey:@"body"];
629 else if ([fetchResponses count] == 0) {
634 /* need to scan for the 'body' response, Courier 'behaviour' */
636 NSDictionary *response;
639 e = [fetchResponses objectEnumerator];
640 while ((response = [e nextObject])) {
641 if ((bStruct = [response objectForKey:@"body"]) != nil)
646 if (bStruct == nil) {
648 @"WARNING[%s]: could not fetch body of message with id %d.",
649 __PRETTY_FUNCTION__, self->uid];
650 if (ImapDebugEnabled) {
651 [self logWithFormat:@" raw: %@", dict];
652 [self logWithFormat:@" fetch: %@", [dict objectForKey:@"fetch"]];
653 [self logWithFormat:@" last: %@",
654 [[dict objectForKey:@"fetch"] lastObject]];
660 if ([[bStruct objectForKey:@"encoding"] length] > 0)
661 [self _processBodyStructureEncoding:bStruct];
663 self->bodyStructure = [[NGMimeMessage alloc] initWithHeader:[self headers]];
664 [self->bodyStructure setBody:
665 _buildMimeMessageBody(self, [self url], bStruct, self->bodyStructure)];
668 - (void)parseMessage {
669 NGMimeMessageParser *parser;
671 parser = [[NGMimeMessageParser alloc] init];
672 ASSIGN(self->message, [parser parsePartFromData:[self rawData]]);
676 /* flag notifications */
678 - (NSString *)_addFlagNotificationName {
679 if (self->addFlagNotificationName)
680 return self->addFlagNotificationName;
682 self->addFlagNotificationName =
684 initWithFormat:@"NGImap4MessageAddFlag_%@_%d",
685 [[self folder] absoluteName], self->uid];
687 return self->addFlagNotificationName;
690 - (NSString *)_removeFlagNotificationName {
691 if (self->removeFlagNotificationName)
692 return self->removeFlagNotificationName;
694 self->removeFlagNotificationName =
696 initWithFormat:@"NGImap4MessageRemoveFlag_%@_%d",
697 [[self folder] absoluteName], self->uid];
698 return self->removeFlagNotificationName;
701 - (void)_removeFlag:(NSNotification *)_notification {
705 flag = [[_notification userInfo] objectForKey:@"flag"];
706 if (debugFlags) [self logWithFormat:@"_del flag: %@", flag];
708 if (![self->flags containsObject:flag]) {
709 if (debugFlags) [self logWithFormat:@" not set."];
713 tmp = [self->flags mutableCopy];
714 [tmp removeObject:flag];
716 ASSIGNCOPY(self->flags, tmp);
719 - (void)_addFlag:(NSNotification *)_notification {
723 flag = [[_notification userInfo] objectForKey:@"flag"];
724 if (debugFlags) [self logWithFormat:@"_add flag: %@", flag];
726 if ([self->flags containsObject:flag]) {
727 if (debugFlags) [self logWithFormat:@" already set."];
732 self->flags = [[self->flags arrayByAddingObject:flag] copy];
736 - (void)setIsRead:(BOOL)_isRead {
737 self->isRead = (_isRead)?1:0;
740 @end /* NGImap4Message */