]> err.no Git - sope/blob - sope-mime/NGImap4/NGImap4Message.m
synced with latest additions and bumped framework versions
[sope] / sope-mime / NGImap4 / NGImap4Message.m
1 /*
2   Copyright (C) 2000-2005 SKYRIX Software AG
3
4   This file is part of SOPE.
5
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
9   later version.
10
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.
15
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
19   02111-1307, USA.
20 */
21
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>
30 #include "imCommon.h"
31
32 #include "NGImap4Message+BodyStructure.h"
33
34 @class NSNotification;
35
36 @interface NGImap4Message(Internals)
37
38 - (void)initializeMessage;
39 - (void)fetchMessage;
40 - (void)parseMessage;
41 - (void)generateBodyStructure;
42 - (NSString *)_addFlagNotificationName;
43 - (NSString *)_removeFlagNotificationName;
44 - (void)_removeFlag:(NSNotification *)_obj;
45 - (void)_addFlag:(NSNotification *)_obj;
46 - (void)setIsRead:(BOOL)_isRead;
47
48 @end /* NGImap4Message(Internals) */
49
50 #define USE_OWN_GLOBAL_ID 1
51
52 @implementation NGImap4Message
53
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;
63
64 + (int)version {
65   return 2;
66 }
67
68 + (void)initialize {
69   NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
70   static BOOL didInit = NO;
71   if (didInit) return;
72   didInit = YES;
73   
74   NumClass = [NSNumber class];
75   YesNumber = [[NumClass numberWithBool:YES] retain];
76   NoNumber  = [[NumClass numberWithBool:NO]  retain];  
77   Fields    = (NGMimeHeaderNames *)[NGMimePartParser headerFieldNames];
78   
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];
83   
84   ImapDebugEnabled = [ud boolForKey:@"ImapDebugEnabled"];
85 }
86
87 - (id)initWithUid:(unsigned)_uid folder:(NGImap4Folder *)_folder
88   context:(NGImap4Context *)_ctx
89 {
90   return [self initWithUid:_uid headers:nil size:-1 flags:nil
91                folder:_folder context:_ctx];
92 }
93
94 - (id)initWithUid:(unsigned)_uid headers:(NGHashMap *)_headers
95   size:(unsigned)_size flags:(NSArray *)_flags folder:(NGImap4Folder *)_folder
96   context:(NGImap4Context *)_ctx
97 {
98   if ((self = [super init])) {
99     self->uid     = _uid;
100     self->size    = _size;
101     self->folder  = _folder;
102     self->headers = [_headers retain];
103     self->flags   = [_flags   retain];
104     self->context = [_ctx     retain];
105     self->isRead  = -1;
106     
107     if (self->folder) {
108       // Note: we can safely retain the registry since it doesn't retain the
109       //       mails
110       self->mailRegistry = [[self->folder mailRegistry] retain];
111       [self->mailRegistry registerObject:self];
112     }
113     else
114       [self logWithFormat:@"WARNING(-init): not attached to a folder!"];
115     
116   }
117   return self;
118 }
119
120 - (void)dealloc {
121   if (self->mailRegistry) {
122     [self->mailRegistry forgetObject:self];
123     [self->mailRegistry release];
124   }
125   else
126     [self logWithFormat:@"WARNING(-dealloc): not attached to a registry!"];
127   
128   [self->headers              release];
129   [self->flags                release];
130   [self->context              release];
131   [self->rawData              release];
132   [self->message              release];
133   [self->bodyStructure        release];
134   [self->url                  release];
135   [self->bodyStructureContent release];
136   [self->globalID             release];
137   [self->removeFlagNotificationName release];
138   [self->addFlagNotificationName    release];
139   self->folder = nil;
140   [super dealloc];
141 }
142
143 /* internal methods */
144
145 - (void)_setHeaders:(NGHashMap *)_headers
146   size:(unsigned)_size
147   flags:(NSArray *)_flags
148 {
149   ASSIGN(self->headers, _headers);
150   ASSIGN(self->flags,   _flags);
151   self->size = _size;
152 }
153
154 - (BOOL)isComplete {
155   return (self->headers != nil) && (self->flags != nil);
156 }
157
158 /* accessors */
159
160 - (NSException *)lastException {
161   return [self->context lastException];
162 }
163 - (void)resetLastException {
164   [self->context resetLastException];
165 }
166
167 - (unsigned)uid {
168   return self->uid;
169 }
170
171 - (NGHashMap *)headers {
172   if (self->headers == nil)
173     [self initializeMessage];
174   return self->headers;
175 }
176
177 - (int)size {
178   if (self->size == -1)
179     [self initializeMessage];
180   return self->size;
181 }
182
183 - (NSArray *)flags {
184   if (self->flags == nil)
185     [self initializeMessage];
186   return self->flags;
187 }
188
189 - (NSData *)rawData {
190   if (self->rawData == nil)
191     [self fetchMessage];
192   return self->rawData;
193 }
194
195 - (NSData *)contentsOfPart:(NSString *)_part {
196   NSData *result;
197
198   if (_part == nil)
199     _part = @"";
200   
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];
205   }
206   
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];
210   }
211   return result;
212 }
213
214 - (id<NGMimePart>)bodyStructure {
215   if (self->bodyStructure == nil)
216     [self generateBodyStructure];
217   
218   return self->bodyStructure;
219 }
220
221 - (id<NGMimePart>)message {
222   if (self->message == nil) 
223     [self parseMessage];
224   return self->message;
225 }
226
227 - (NGImap4Folder *)folder {
228   return self->folder;
229 }
230
231 - (NGImap4Context *)context {
232   return self->context;
233 }
234
235 - (BOOL)isRead {
236   if ((self->flags == nil) && (self->isRead != -1))
237     return (self->isRead == 1) ? YES : NO;
238
239   return [[self flags] containsObject:@"seen"];
240 }
241
242 - (void)markRead {
243   if (![self isRead])
244     [self addFlag:@"seen"];
245
246   [self removeFlag:@"recent"];
247 }
248
249 - (void)markUnread {
250   if ([self isRead]);
251   [self removeFlag:@"seen"];
252 }
253
254 - (BOOL)isFlagged {
255   return [[self flags] containsObject:@"flagged"];
256 }
257
258 - (void)markFlagged {
259   if (![self isFlagged])
260     [self addFlag:@"flagged"];
261 }
262
263 - (void)markUnFlagged {
264   if ([self isFlagged]) {
265     [self removeFlag:@"flagged"];
266   }
267 }
268
269 - (BOOL)isAnswered {
270   return [[self flags] containsObject:@"answered"];
271 }
272
273 - (void)markAnswered {
274   if (![self isAnswered])
275     [self addFlag:@"answered"];
276 }
277
278 - (void)markNotAnswered {
279   if ([self isAnswered])
280     [self removeFlag:@"answered"];
281 }
282
283 - (void)addFlag:(NSString *)_flag {
284   NSDictionary *res;
285   
286   if (_flag == nil)
287     return;
288   
289   if (self->mailRegistry) 
290     [self->mailRegistry postFlagAdded:_flag inMessage:self];
291   else
292     [self logWithFormat:@"WARNING(-addFlag:): no folder attached to message!"];
293   
294   if (![[self->folder messageFlags] containsObject:_flag])
295     return;
296     
297   if (![self->context registerAsSelectedFolder:self->folder])
298     return;
299   res = [[self->context client] storeUid:self->uid add:YesNumber
300                                 flags:[NSArray arrayWithObject:_flag]];
301   if (!_checkResult(self->context, res, __PRETTY_FUNCTION__))
302     return;
303   
304   [self->folder resetStatus];
305 }
306
307 - (void)removeFlag:(NSString *)_flag {
308   NSDictionary *res;
309     
310   if (_flag == nil) return;
311   
312   if (self->mailRegistry) 
313     [self->mailRegistry postFlagRemoved:_flag inMessage:self];
314   else
315     [self logWithFormat:@"WARNING(-remFlag:): no folder attached to message!"];
316   
317   if (![[self->folder messageFlags] containsObject:_flag])
318     return;
319   
320   if (![self->context registerAsSelectedFolder:self->folder])
321     return;
322
323   res = [[self->context client] storeUid:self->uid add:NoNumber
324                                 flags:[NSArray arrayWithObject:_flag]];
325   if (!_checkResult(self->context, res, __PRETTY_FUNCTION__))
326     return;
327     
328   [self->folder resetStatus];
329 }
330
331 /* equality */
332
333 - (BOOL)isEqual:(id)_obj {
334   if (_obj == self)
335     return YES;
336   if ([_obj isKindOfClass:[NGImap4Message class]])
337     return [self isEqualToNGImap4Message:_obj];
338   return NO;
339 }
340
341 - (BOOL)isEqualToNGImap4Message:(NGImap4Message *)_messages {
342   if ([_messages uid] != self->uid)
343     return NO;
344   if (![[_messages context] isEqual:self->context])
345     return NO;
346   if (![[_messages folder] isEqual:self->folder])
347     return NO;
348   
349   return YES;
350 }
351
352 - (unsigned)hash {
353   return self->uid;
354 }
355
356 - (EOGlobalID *)globalID {
357 #if USE_OWN_GLOBAL_ID
358   EOGlobalID *fgid;
359   
360   if (self->globalID)
361     return self->globalID;
362
363   if ((fgid = [[self folder] globalID]) == nil) {
364     [self logWithFormat:@"WARNING(-globalID): got no globalID for folder: %@",
365             [self folder]];
366   }
367   
368   self->globalID = [[NGImap4MessageGlobalID alloc] initWithFolderGlobalID:fgid
369                                                    andUid:[self uid]];
370   return self->globalID;
371 #else
372   id keys[4];
373
374   if (self->globalID)
375     return self->globalID;
376   
377   // TODO: this needs to be invalidated, if a folder is moved!
378   
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];
383   
384   globalID = [[EOKeyGlobalID globalIDWithEntityName:@"NGImap4Message"
385                              keys:keys keyCount:4
386                              zone:NULL] retain];
387   return globalID;
388 #endif
389 }
390
391 /* key-value coding */
392
393 - (id)valueForKey:(NSString *)_key {
394   // TODO: might want to add some more caching
395   unsigned len;
396   id v = nil;
397   
398   if ((len = [_key length]) == 0)
399     return nil;
400   
401   if ([_key characterAtIndex:0] == 'N') {
402     if (len < 9)
403       ;
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],
421                       [self uid]];
422     else if ([_key isEqualToString:@"url"])
423       v = [self url];
424
425     if (v) return v;
426   }
427   
428   if ((v = [[self headers] objectForKey:_key]))
429     return v;
430   
431   return [super valueForKey:_key];
432 }
433
434 /* description */
435
436 - (NSString *)description {
437   NSMutableString *ms;
438   id tmp;
439   
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];
443
444   if (self->headers) {
445     tmp = [[self headers] objectForKey:@"subject"];
446     if ([tmp length] > 0)
447       [ms appendFormat:@" subject='%@'", tmp];
448     else
449       [ms appendString:@" no-subject"];
450   }
451   else {
452     [ms appendString:@" [header not fetched]"];
453   }
454   tmp = [self->folder absoluteName];
455   if (tmp)
456     [ms appendFormat:@" folder='%@'", tmp];
457
458   if (self->flags) {
459   tmp = [self flags];
460
461   if ([tmp count] > 0)
462     [ms appendFormat:@"flags: (%@)", [tmp componentsJoinedByString:@", "]];
463   else
464     [ms appendString:@"no-flags"];
465   }
466   else {
467     [ms appendString:@" [flags not fetched]"];
468   }
469   
470   [ms appendString:@">"];
471   return ms;
472 }
473
474 - (NSURL *)url {
475   NSURL    *base;
476   NSString *path, *s;
477   char buf[64];
478   
479   if (self->url) return self->url;
480
481   base = [self->folder url];
482   
483   sprintf(buf, "%d", [self uid]);
484   s = [[NSString alloc] initWithCString:buf];
485   path = [[base path] stringByAppendingPathComponent:s];
486   [s release];
487   
488   self->url = [[NSURL alloc] initWithScheme:[base scheme]
489                              host:[base host]
490                              path:path];
491   return self->url;
492 }
493
494 /* Internals */
495
496 - (void)initializeMessage {
497   NSDictionary        *dict;
498   NSDictionary        *fetch;
499   NSAutoreleasePool   *pool;
500
501   pool = [[NSAutoreleasePool alloc] init];
502
503   if (![self->context registerAsSelectedFolder:self->folder])
504     return;
505   
506   [self resetLastException];
507
508   dict = [[self->context client]
509                          fetchUid:self->uid
510                          parts:CoreMsgAttrNames];
511
512   if (!_checkResult(self->context, dict, __PRETTY_FUNCTION__))
513     return;
514   
515   fetch = [[[dict objectForKey:@"fetch"] objectEnumerator] nextObject];
516
517   if (fetch == nil) {
518     NSLog(@"WARNING[%s] : couldn`t fetch message with id %d",
519           __PRETTY_FUNCTION__, self->uid);
520     return;
521   }
522   {
523     id                  h, f, s;
524     NGMimeMessageParser *parser;
525     NGDataStream        *stream;  
526
527     h   = [fetch objectForKey:@"header"];
528     f   = [fetch objectForKey:@"flags"];
529     s   = [fetch objectForKey:@"size"];
530
531     if ((h == nil) || (f == nil) || (s == nil)) {
532       NSLog(@"WARNING[%s]: got no header, flags, size for %@",
533             __PRETTY_FUNCTION__, fetch);
534       return;
535     }
536     parser = [[[NGMimeMessageParser alloc] init] autorelease];
537     stream = [[[NGDataStream alloc] initWithData:h] autorelease];
538     
539     [parser prepareForParsingFromStream:stream];
540     
541     ASSIGN(self->headers, [parser parseHeader]);
542
543     self->size = [s intValue];
544
545     if (([f containsObject:@"recent"]) && ([f containsObject:@"seen"])) {
546       f = [[f mutableCopy] autorelease];
547       [f removeObject:@"recent"];
548     }
549     ASSIGNCOPY(self->flags, f);
550   }
551   [pool release]; pool = nil;
552 }
553
554 - (void)fetchMessage {
555   NSDictionary *dict, *fetch;
556
557   if (![self->context registerAsSelectedFolder:self->folder])
558     return;
559
560   [self resetLastException];
561   
562   dict = [[self->context client] fetchUid:self->uid parts:rfc822NameArray];
563   if (!_checkResult(self->context, dict, __PRETTY_FUNCTION__))
564       return;
565   
566   fetch = [[[dict objectForKey:@"fetch"] objectEnumerator] nextObject];
567
568   if (fetch == nil) {
569     NSLog(@"WARNING[%s]: couldn`t fetch message with id %d",
570           __PRETTY_FUNCTION__, self->uid);
571     return;
572   }
573   ASSIGN(self->rawData, [fetch objectForKey:@"message"]);
574 }
575
576 - (void)_processBodyStructureEncoding:(NSDictionary *)bStruct {
577   NGHashMap *orgH;
578   NGMutableHashMap *h;
579
580   orgH = [self headers];
581
582   if ([[orgH objectForKey:@"encoding"] length] != 0)
583     return;
584       
585   h = [[self headers] mutableCopy];
586   
587   [h setObject:[[bStruct objectForKey:@"encoding"] lowercaseString]
588      forKey:@"content-transfer-encoding"];
589   
590   ASSIGNCOPY(self->headers, h);
591   [h release];
592 }
593 - (void)generateBodyStructure {
594   NSDictionary *dict, *bStruct;
595   NSArray      *fetchResponses;
596
597   [self->bodyStructure release]; self->bodyStructure = nil;
598
599   [self resetLastException];
600
601   if (![self->context registerAsSelectedFolder:self->folder])
602     return;
603   
604   dict = [[self->context client] fetchUid:self->uid parts:bodyNameArray];
605   
606   if (!(_checkResult(self->context, dict, __PRETTY_FUNCTION__)))
607     return;
608
609   /*
610     TODO: the following seems to fail with Courier, see OGo bug #800:
611     ---snip---
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
620       message with id 635
621   */
622   
623   fetchResponses = [dict objectForKey:@"fetch"];
624   if ([fetchResponses count] == 1) {
625     /* like with Cyrus, "old" behaviour */
626     bStruct = 
627       [(NSDictionary *)[fetchResponses lastObject] objectForKey:@"body"];
628   }
629   else if ([fetchResponses count] == 0) {
630     /* no results */
631     bStruct = nil;
632   }
633   else {
634     /* need to scan for the 'body' response, Courier 'behaviour' */
635     NSEnumerator *e;
636     NSDictionary *response;
637     
638     bStruct = nil;
639     e = [fetchResponses objectEnumerator];
640     while ((response = [e nextObject])) {
641       if ((bStruct = [response objectForKey:@"body"]) != nil)
642         break;
643     }
644   }
645   
646   if (bStruct == nil) {
647     [self logWithFormat:
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]];
655     }
656     return;
657   }
658   
659   /* set encoding */
660   if ([[bStruct objectForKey:@"encoding"] length] > 0)
661     [self _processBodyStructureEncoding:bStruct];
662   
663   self->bodyStructure = [[NGMimeMessage alloc] initWithHeader:[self headers]];
664   [self->bodyStructure setBody:
665        _buildMimeMessageBody(self, [self url], bStruct, self->bodyStructure)];
666 }
667
668 - (void)parseMessage {
669   NGMimeMessageParser *parser;
670
671   parser = [[NGMimeMessageParser alloc] init];
672   ASSIGN(self->message, [parser parsePartFromData:[self rawData]]);
673   [parser release];
674 }
675
676 /* flag notifications */
677
678 - (NSString *)_addFlagNotificationName {
679   if (self->addFlagNotificationName)
680     return self->addFlagNotificationName;
681   
682   self->addFlagNotificationName =
683       [[NSString alloc]
684                  initWithFormat:@"NGImap4MessageAddFlag_%@_%d",
685                  [[self folder] absoluteName], self->uid];
686   
687   return self->addFlagNotificationName;
688 }
689
690 - (NSString *)_removeFlagNotificationName {
691   if (self->removeFlagNotificationName)
692     return self->removeFlagNotificationName;
693   
694   self->removeFlagNotificationName =
695       [[NSString alloc]
696                  initWithFormat:@"NGImap4MessageRemoveFlag_%@_%d",
697                  [[self folder] absoluteName], self->uid];
698   return self->removeFlagNotificationName;
699 }
700
701 - (void)_removeFlag:(NSNotification *)_notification {
702   NSMutableArray *tmp;
703   NSString *flag;
704
705   flag = [[_notification userInfo] objectForKey:@"flag"];
706   if (debugFlags) [self logWithFormat:@"_del flag: %@", flag];
707
708   if (![self->flags containsObject:flag]) {
709     if (debugFlags) [self logWithFormat:@"  not set."];
710     return;
711   }
712
713   tmp = [self->flags mutableCopy];
714   [tmp removeObject:flag];
715
716   ASSIGNCOPY(self->flags, tmp);
717   [tmp release];
718 }
719 - (void)_addFlag:(NSNotification *)_notification {
720   NSArray  *tmp;
721   NSString *flag;
722
723   flag = [[_notification userInfo] objectForKey:@"flag"];
724   if (debugFlags) [self logWithFormat:@"_add flag: %@", flag];
725   
726   if ([self->flags containsObject:flag]) {
727     if (debugFlags) [self logWithFormat:@"  already set."];
728     return;
729   }
730   
731   tmp = self->flags;
732   self->flags = [[self->flags arrayByAddingObject:flag] copy];
733   [tmp release];
734 }
735
736 - (void)setIsRead:(BOOL)_isRead {
737   self->isRead = (_isRead)?1:0;
738 }
739
740 @end /* NGImap4Message */