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 @interface NGImap4Message(Internals)
36 - (void)initializeMessage;
39 - (void)generateBodyStructure;
40 - (NSString *)_addFlagNotificationName;
41 - (NSString *)_removeFlagNotificationName;
42 - (void)_removeFlag:(id)_obj;
43 - (void)_addFlag:(id)_obj;
44 - (void)setIsRead:(BOOL)_isRead;
46 @end /* NGImap4Message(Internals) */
48 #define USE_OWN_GLOBAL_ID 1
50 @implementation NGImap4Message
52 static Class NumClass = Nil;
53 static NSNumber *YesNumber = nil;
54 static NSNumber *NoNumber = nil;
55 static NGMimeHeaderNames *Fields = NULL;
56 static NSArray *CoreMsgAttrNames = nil;
57 static NSArray *bodyNameArray = nil;
58 static NSArray *rfc822NameArray = nil;
59 static BOOL debugFlags = NO;
60 static BOOL ImapDebugEnabled = NO;
67 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
68 static BOOL didInit = NO;
72 NumClass = [NSNumber class];
73 YesNumber = [[NumClass numberWithBool:YES] retain];
74 NoNumber = [[NumClass numberWithBool:NO] retain];
75 Fields = (NGMimeHeaderNames *)[NGMimePartParser headerFieldNames];
77 CoreMsgAttrNames = [[NSArray alloc] initWithObjects:@"rfc822.header",
78 @"rfc822.size", @"flags", nil];
79 bodyNameArray = [[NSArray alloc] initWithObjects:@"body", nil];
80 rfc822NameArray = [[NSArray alloc] initWithObjects:@"rfc822", nil];
82 ImapDebugEnabled = [ud boolForKey:@"ImapDebugEnabled"];
85 - (id)initWithUid:(unsigned)_uid folder:(NGImap4Folder *)_folder
86 context:(NGImap4Context *)_ctx
88 return [self initWithUid:_uid headers:nil size:-1 flags:nil
89 folder:_folder context:_ctx];
92 - (id)initWithUid:(unsigned)_uid headers:(NGHashMap *)_headers
93 size:(unsigned)_size flags:(NSArray *)_flags folder:(NGImap4Folder *)_folder
94 context:(NGImap4Context *)_ctx
96 if ((self = [super init])) {
99 self->folder = _folder;
100 self->headers = [_headers retain];
101 self->flags = [_flags retain];
102 self->context = [_ctx retain];
106 // Note: we can safely retain the registry since it doesn't retain the
108 self->mailRegistry = [[self->folder mailRegistry] retain];
109 [self->mailRegistry registerObject:self];
112 [self logWithFormat:@"WARNING(-init): not attached to a folder!"];
119 if (self->mailRegistry) {
120 [self->mailRegistry forgetObject:self];
121 [self->mailRegistry release];
124 [self logWithFormat:@"WARNING(-dealloc): not attached to a registry!"];
126 [self->headers release];
127 [self->flags release];
128 [self->context release];
129 [self->rawData release];
130 [self->message release];
131 [self->bodyStructure release];
133 [self->bodyStructureContent release];
134 [self->globalID release];
135 [self->removeFlagNotificationName release];
136 [self->addFlagNotificationName release];
141 /* internal methods */
143 - (void)_setHeaders:(NGHashMap *)_headers
145 flags:(NSArray *)_flags
147 ASSIGN(self->headers, _headers);
148 ASSIGN(self->flags, _flags);
153 return (self->headers != nil) && (self->flags != nil);
158 - (NSException *)lastException {
159 return [self->context lastException];
161 - (void)resetLastException {
162 [self->context resetLastException];
169 - (NGHashMap *)headers {
170 if (self->headers == nil)
171 [self initializeMessage];
172 return self->headers;
176 if (self->size == -1)
177 [self initializeMessage];
182 if (self->flags == nil)
183 [self initializeMessage];
187 - (NSData *)rawData {
188 if (self->rawData == nil)
190 return self->rawData;
193 - (NSData *)contentsOfPart:(NSString *)_part {
199 /* apparently caches the data for a part ID like "1.2.3" */
200 if (self->bodyStructureContent == nil) {
201 self->bodyStructureContent =
202 [[NSMutableDictionary alloc] initWithCapacity:8];
205 if ((result = [self->bodyStructureContent objectForKey:_part]) == nil) {
206 if ((result = [self->folder blobForUid:self->uid part:_part]) != nil)
207 [self->bodyStructureContent setObject:result forKey:_part];
212 - (id<NGMimePart>)bodyStructure {
213 if (self->bodyStructure == nil)
214 [self generateBodyStructure];
216 return self->bodyStructure;
219 - (id<NGMimePart>)message {
220 if (self->message == nil)
222 return self->message;
225 - (NGImap4Folder *)folder {
229 - (NGImap4Context *)context {
230 return self->context;
234 if ((self->flags == nil) && (self->isRead != -1))
235 return (self->isRead == 1) ? YES : NO;
237 return [[self flags] containsObject:@"seen"];
242 [self addFlag:@"seen"];
244 [self removeFlag:@"recent"];
249 [self removeFlag:@"seen"];
253 return [[self flags] containsObject:@"flagged"];
256 - (void)markFlagged {
257 if (![self isFlagged])
258 [self addFlag:@"flagged"];
261 - (void)markUnFlagged {
262 if ([self isFlagged]) {
263 [self removeFlag:@"flagged"];
268 return [[self flags] containsObject:@"answered"];
271 - (void)markAnswered {
272 if (![self isAnswered])
273 [self addFlag:@"answered"];
276 - (void)markNotAnswered {
277 if ([self isAnswered])
278 [self removeFlag:@"answered"];
281 - (void)addFlag:(NSString *)_flag {
287 if (self->mailRegistry)
288 [self->mailRegistry postFlagAdded:_flag inMessage:self];
290 [self logWithFormat:@"WARNING(-addFlag:): no folder attached to message!"];
292 if (![[self->folder messageFlags] containsObject:_flag])
295 if (![self->context registerAsSelectedFolder:self->folder])
297 res = [[self->context client] storeUid:self->uid add:YesNumber
298 flags:[NSArray arrayWithObject:_flag]];
299 if (!_checkResult(self->context, res, __PRETTY_FUNCTION__))
302 [self->folder resetStatus];
305 - (void)removeFlag:(NSString *)_flag {
308 if (_flag == nil) return;
310 if (self->mailRegistry)
311 [self->mailRegistry postFlagRemoved:_flag inMessage:self];
313 [self logWithFormat:@"WARNING(-remFlag:): no folder attached to message!"];
315 if (![[self->folder messageFlags] containsObject:_flag])
318 if (![self->context registerAsSelectedFolder:self->folder])
321 res = [[self->context client] storeUid:self->uid add:NoNumber
322 flags:[NSArray arrayWithObject:_flag]];
323 if (!_checkResult(self->context, res, __PRETTY_FUNCTION__))
326 [self->folder resetStatus];
331 - (BOOL)isEqual:(id)_obj {
334 if ([_obj isKindOfClass:[NGImap4Message class]])
335 return [self isEqualToNGImap4Message:_obj];
339 - (BOOL)isEqualToNGImap4Message:(NGImap4Message *)_messages {
340 if ([_messages uid] != self->uid)
342 if (![[_messages context] isEqual:self->context])
344 if (![[_messages folder] isEqual:self->folder])
354 - (EOGlobalID *)globalID {
355 #if USE_OWN_GLOBAL_ID
359 return self->globalID;
361 if ((fgid = [[self folder] globalID]) == nil) {
362 [self logWithFormat:@"WARNING(-globalID): got no globalID for folder: %@",
366 self->globalID = [[NGImap4MessageGlobalID alloc] initWithFolderGlobalID:fgid
368 return self->globalID;
373 return self->globalID;
375 // TODO: this needs to be invalidated, if a folder is moved!
377 keys[0] = [NumClass numberWithUnsignedInt:[self uid]];
378 keys[1] = [[self folder] absoluteName];
379 keys[2] = [[self context] host];
380 keys[3] = [[self context] login];
382 globalID = [[EOKeyGlobalID globalIDWithEntityName:@"NGImap4Message"
389 /* key-value coding */
391 - (id)valueForKey:(NSString *)_key {
392 // TODO: might want to add some more caching
396 if ((len = [_key length]) == 0)
399 if ([_key characterAtIndex:0] == 'N') {
402 else if ([_key isEqualToString:@"NSFileIdentifier"])
403 v = [[self headers] objectForKey:Fields->messageID];
404 else if ([_key isEqualToString:NSFileSize])
405 v = [NumClass numberWithInt:[self size]];
406 else if ([_key isEqualToString:NSFileModificationDate])
407 v = [[self headers] objectForKey:Fields->date];
408 else if ([_key isEqualToString:NSFileType])
409 v = NSFileTypeRegular;
410 else if ([_key isEqualToString:NSFileOwnerAccountName])
411 v = [self->context login];
412 else if ([_key isEqualToString:@"NGFileSubject"])
413 v = [[self headers] objectForKey:Fields->subject];
414 else if ([_key isEqualToString:@"NSFileSubject"])
415 v = [[self headers] objectForKey:Fields->subject];
416 else if ([_key isEqualToString:@"NGFilePath"])
417 v = [NSString stringWithFormat:@"%@/%d",
418 [self->folder absoluteName],
420 else if ([_key isEqualToString:@"url"])
426 if ((v = [[self headers] objectForKey:_key]))
429 return [super valueForKey:_key];
434 - (NSString *)description {
438 ms = [NSMutableString stringWithCapacity:128];
439 [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])];
440 [ms appendFormat:@" uid=%d size=%d", self->uid, self->size];
443 tmp = [[self headers] objectForKey:@"subject"];
444 if ([tmp length] > 0)
445 [ms appendFormat:@" subject='%@'", tmp];
447 [ms appendString:@" no-subject"];
450 [ms appendString:@" [header not fetched]"];
452 tmp = [self->folder absoluteName];
454 [ms appendFormat:@" folder='%@'", tmp];
460 [ms appendFormat:@"flags: (%@)", [tmp componentsJoinedByString:@", "]];
462 [ms appendString:@"no-flags"];
465 [ms appendString:@" [flags not fetched]"];
468 [ms appendString:@">"];
477 if (self->url) return self->url;
479 base = [self->folder url];
481 sprintf(buf, "%d", [self uid]);
482 s = [[NSString alloc] initWithCString:buf];
483 path = [[base path] stringByAppendingPathComponent:s];
486 self->url = [[NSURL alloc] initWithScheme:[base scheme]
494 - (void)initializeMessage {
497 NSAutoreleasePool *pool;
499 pool = [[NSAutoreleasePool alloc] init];
501 if (![self->context registerAsSelectedFolder:self->folder])
504 [self resetLastException];
506 dict = [[self->context client]
508 parts:CoreMsgAttrNames];
510 if (!_checkResult(self->context, dict, __PRETTY_FUNCTION__))
513 fetch = [[[dict objectForKey:@"fetch"] objectEnumerator] nextObject];
516 NSLog(@"WARNING[%s] : couldn`t fetch message with id %d",
517 __PRETTY_FUNCTION__, self->uid);
522 NGMimeMessageParser *parser;
523 NGDataStream *stream;
525 h = [fetch objectForKey:@"header"];
526 f = [fetch objectForKey:@"flags"];
527 s = [fetch objectForKey:@"size"];
529 if ((h == nil) || (f == nil) || (s == nil)) {
530 NSLog(@"WARNING[%s]: got no header, flags, size for %@",
531 __PRETTY_FUNCTION__, fetch);
534 parser = [[[NGMimeMessageParser alloc] init] autorelease];
535 stream = [[[NGDataStream alloc] initWithData:h] autorelease];
537 [parser prepareForParsingFromStream:stream];
539 ASSIGN(self->headers, [parser parseHeader]);
541 self->size = [s intValue];
543 if (([f containsObject:@"recent"]) && ([f containsObject:@"seen"])) {
544 f = [[f mutableCopy] autorelease];
545 [f removeObject:@"recent"];
547 ASSIGNCOPY(self->flags, f);
549 [pool release]; pool = nil;
552 - (void)fetchMessage {
553 NSDictionary *dict, *fetch;
555 if (![self->context registerAsSelectedFolder:self->folder])
558 [self resetLastException];
560 dict = [[self->context client] fetchUid:self->uid parts:rfc822NameArray];
561 if (!_checkResult(self->context, dict, __PRETTY_FUNCTION__))
564 fetch = [[[dict objectForKey:@"fetch"] objectEnumerator] nextObject];
567 NSLog(@"WARNING[%s]: couldn`t fetch message with id %d",
568 __PRETTY_FUNCTION__, self->uid);
571 ASSIGN(self->rawData, [fetch objectForKey:@"message"]);
574 - (void)_processBodyStructureEncoding:(NSDictionary *)bStruct {
578 orgH = [self headers];
580 if ([[orgH objectForKey:@"encoding"] length] != 0)
583 h = [[self headers] mutableCopy];
585 [h setObject:[[bStruct objectForKey:@"encoding"] lowercaseString]
586 forKey:@"content-transfer-encoding"];
588 ASSIGNCOPY(self->headers, h);
591 - (void)generateBodyStructure {
592 NSDictionary *dict, *bStruct;
593 NSArray *fetchResponses;
595 [self->bodyStructure release]; self->bodyStructure = nil;
597 [self resetLastException];
599 if (![self->context registerAsSelectedFolder:self->folder])
602 dict = [[self->context client] fetchUid:self->uid parts:bodyNameArray];
604 if (!(_checkResult(self->context, dict, __PRETTY_FUNCTION__)))
608 TODO: the following seems to fail with Courier, see OGo bug #800:
610 C[0x8b4e754]: 27 uid fetch 635 (body)
611 S[0x8c8b4e4]: * 627 FETCH (UID 635 BODY
612 ("text" "plain" ("charset" "iso-8859-1" "format" "flowed")
613 NIL NIL "8bit" 2474 51))
614 S[0x8c8b4e4]: * 627 FETCH (FLAGS (\Seen))
615 S[0x8c8b4e4]: 27 OK FETCH completed.
616 Jun 26 18:38:15 OpenGroupware [30904]: <0x08A73DCC[NGImap4Message]>
617 WARNING[-[NGImap4Message generateBodyStructure]]: could not fetch body of
621 fetchResponses = [dict objectForKey:@"fetch"];
622 if ([fetchResponses count] == 1) {
623 /* like with Cyrus, "old" behaviour */
625 [(NSDictionary *)[fetchResponses lastObject] objectForKey:@"body"];
627 else if ([fetchResponses count] == 0) {
632 /* need to scan for the 'body' response, Courier 'behaviour' */
634 NSDictionary *response;
637 e = [fetchResponses objectEnumerator];
638 while ((response = [e nextObject])) {
639 if ((bStruct = [response objectForKey:@"body"]) != nil)
644 if (bStruct == nil) {
646 @"WARNING[%s]: could not fetch body of message with id %d.",
647 __PRETTY_FUNCTION__, self->uid];
648 if (ImapDebugEnabled) {
649 [self logWithFormat:@" raw: %@", dict];
650 [self logWithFormat:@" fetch: %@", [dict objectForKey:@"fetch"]];
651 [self logWithFormat:@" last: %@",
652 [[dict objectForKey:@"fetch"] lastObject]];
658 if ([[bStruct objectForKey:@"encoding"] length] > 0)
659 [self _processBodyStructureEncoding:bStruct];
661 self->bodyStructure = [[NGMimeMessage alloc] initWithHeader:[self headers]];
662 [self->bodyStructure setBody:
663 _buildMimeMessageBody(self, [self url], bStruct, self->bodyStructure)];
666 - (void)parseMessage {
667 NGMimeMessageParser *parser;
669 parser = [[NGMimeMessageParser alloc] init];
670 ASSIGN(self->message, [parser parsePartFromData:[self rawData]]);
674 /* flag notifications */
676 - (NSString *)_addFlagNotificationName {
677 if (self->addFlagNotificationName)
678 return self->addFlagNotificationName;
680 self->addFlagNotificationName =
682 initWithFormat:@"NGImap4MessageAddFlag_%@_%d",
683 [[self folder] absoluteName], self->uid];
685 return self->addFlagNotificationName;
688 - (NSString *)_removeFlagNotificationName {
689 if (self->removeFlagNotificationName)
690 return self->removeFlagNotificationName;
692 self->removeFlagNotificationName =
694 initWithFormat:@"NGImap4MessageRemoveFlag_%@_%d",
695 [[self folder] absoluteName], self->uid];
696 return self->removeFlagNotificationName;
699 - (void)_removeFlag:(NSNotification *)_notification {
703 flag = [[_notification userInfo] objectForKey:@"flag"];
704 if (debugFlags) [self logWithFormat:@"_del flag: %@", flag];
706 if (![self->flags containsObject:flag]) {
707 if (debugFlags) [self logWithFormat:@" not set."];
711 tmp = [self->flags mutableCopy];
712 [tmp removeObject:flag];
714 ASSIGNCOPY(self->flags, tmp);
717 - (void)_addFlag:(NSNotification *)_notification {
721 flag = [[_notification userInfo] objectForKey:@"flag"];
722 if (debugFlags) [self logWithFormat:@"_add flag: %@", flag];
724 if ([self->flags containsObject:flag]) {
725 if (debugFlags) [self logWithFormat:@" already set."];
730 self->flags = [[self->flags arrayByAddingObject:flag] copy];
734 - (void)setIsRead:(BOOL)_isRead {
735 self->isRead = (_isRead)?1:0;
738 @end /* NGImap4Message */