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