]> err.no Git - scalable-opengroupware.org/blob - SOGo/SoObjects/Mailer/SOGoMailObject.m
implemented trashing
[scalable-opengroupware.org] / SOGo / SoObjects / Mailer / SOGoMailObject.m
1 /*
2   Copyright (C) 2004-2005 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
22 #include "SOGoMailObject.h"
23 #include "SOGoMailFolder.h"
24 #include "SOGoMailAccount.h"
25 #include "SOGoMailManager.h"
26 #include "SOGoMailBodyPart.h"
27 #include <NGImap4/NGImap4Envelope.h>
28 #include <NGImap4/NGImap4EnvelopeAddress.h>
29 #include "common.h"
30
31 @implementation SOGoMailObject
32
33 static NSArray *coreInfoKeys = nil;
34 static BOOL heavyDebug = NO;
35 static BOOL debugOn = NO;
36 static BOOL debugBodyStructure = NO;
37
38 + (void)initialize {
39   /* Note: see SOGoMailManager.m for allowed IMAP4 keys */
40   /* Note: "BODY" actually returns the structure! */
41   coreInfoKeys = [[NSArray alloc] initWithObjects:
42                                     @"FLAGS", @"ENVELOPE", @"BODY",
43                                     @"RFC822.SIZE",
44                                     // not yet supported: @"INTERNALDATE",
45                                   nil];
46 }
47
48 - (void)dealloc {
49   [self->coreInfos release];
50   [super dealloc];
51 }
52
53 /* IMAP4 */
54
55 - (NSString *)relativeImap4Name {
56   return [[self nameInContainer] stringByDeletingPathExtension];
57 }
58
59 /* hierarchy */
60
61 - (SOGoMailObject *)mailObject {
62   return self;
63 }
64
65 /* part hierarchy */
66
67 - (NSString *)keyExtensionForPart:(id)_partInfo {
68   NSString *mt, *st;
69   
70   if (_partInfo == nil)
71     return nil;
72   
73   mt = [_partInfo valueForKey:@"type"];
74   st = [[_partInfo valueForKey:@"subtype"] lowercaseString];
75   if ([mt isEqualToString:@"text"]) {
76     if ([st isEqualToString:@"plain"])
77       return @".txt";
78     if ([st isEqualToString:@"html"])
79       return @".html";
80     if ([st isEqualToString:@"calendar"])
81       return @".ics";
82   }
83   else if ([mt isEqualToString:@"image"])
84     return [@"." stringByAppendingString:st];
85   else if ([mt isEqualToString:@"application"]) {
86     if ([st isEqualToString:@"pgp-signature"])
87       return @".asc";
88   }
89   
90   return nil;
91 }
92
93 - (NSArray *)relationshipKeysWithParts:(BOOL)_withParts {
94   /* should return non-multipart children */
95   NSMutableArray *ma;
96   NSArray *parts;
97   unsigned i, count;
98   
99   parts = [[self bodyStructure] valueForKey:@"parts"];
100   if (![parts isNotNull]) 
101     return nil;
102   if ((count = [parts count]) == 0)
103     return nil;
104   
105   for (i = 0, ma = nil; i < count; i++) {
106     NSString *key, *ext;
107     id   part;
108     BOOL hasParts;
109     
110     part     = [parts objectAtIndex:i];
111     hasParts = [part valueForKey:@"parts"] != nil ? YES:NO;
112     if ((hasParts && !_withParts) || (_withParts && !hasParts))
113       continue;
114
115     if (ma == nil)
116       ma = [NSMutableArray arrayWithCapacity:count - i];
117     
118     ext = [self keyExtensionForPart:part];
119     key = [[NSString alloc] initWithFormat:@"%d%@", i + 1, ext?ext:@""];
120     [ma addObject:key];
121     [key release];
122   }
123   return ma;
124 }
125
126 - (NSArray *)toOneRelationshipKeys {
127   return [self relationshipKeysWithParts:NO];
128 }
129 - (NSArray *)toManyRelationshipKeys {
130   return [self relationshipKeysWithParts:YES];
131 }
132
133 /* message */
134
135 - (id)fetchParts:(NSArray *)_parts {
136   return [[self mailManager] fetchURL:[self imap4URL] parts:_parts
137                              password:[self imap4Password]];
138 }
139
140 /* core infos */
141
142 - (id)fetchCoreInfos {
143   id msgs;
144   
145   if (self->coreInfos != nil)
146     return [self->coreInfos isNotNull] ? self->coreInfos : nil;
147
148   msgs = [[self clientObject] fetchParts:coreInfoKeys]; // returns dict
149   if (heavyDebug) [self logWithFormat:@"M: %@", msgs];
150   msgs = [msgs valueForKey:@"fetch"];
151   if ([msgs count] == 0)
152     return nil;
153   
154   self->coreInfos = [[msgs objectAtIndex:0] retain];
155   return self->coreInfos;
156 }
157
158 - (id)bodyStructure {
159   id body;
160
161   body = [[self fetchCoreInfos] valueForKey:@"body"];
162   if (debugBodyStructure)
163     [self logWithFormat:@"BODY: %@", body];
164   return body;
165 }
166
167 - (NGImap4Envelope *)envelope {
168   return [[self fetchCoreInfos] valueForKey:@"envelope"];
169 }
170 - (NSString *)subject {
171   return [[self envelope] subject];
172 }
173 - (NSCalendarDate *)date {
174   return [[self envelope] date];
175 }
176 - (NSArray *)fromEnvelopeAddresses {
177   return [[self envelope] from];
178 }
179 - (NSArray *)toEnvelopeAddresses {
180   return [[self envelope] to];
181 }
182 - (NSArray *)ccEnvelopeAddresses {
183   return [[self envelope] cc];
184 }
185
186 - (id)lookupInfoForBodyPart:(id)_path {
187   NSEnumerator *pe;
188   NSString *p;
189   id info;
190
191   if (![_path isNotNull])
192     return nil;
193   
194   if ((info = [self bodyStructure]) == nil) {
195     [self errorWithFormat:@"got no body part structure!"];
196     return nil;
197   }
198
199   /* ensure array argument */
200   
201   if ([_path isKindOfClass:[NSString class]]) {
202     if ([_path length] == 0)
203       return info;
204     
205     _path = [_path componentsSeparatedByString:@"."];
206   }
207   
208   /* 
209      For each path component, eg 1,1,3 
210      
211      Remember that we need special processing for message/rfc822 which maps the
212      namespace of multiparts directly into the main namespace.
213      
214      TODO(hh): no I don't remember, please explain in more detail!
215   */
216   pe = [_path objectEnumerator];
217   while ((p = [pe nextObject]) != nil && [info isNotNull]) {
218     unsigned idx;
219     NSArray  *parts;
220     NSString *mt;
221     
222     [self debugWithFormat:@"check PATH: %@", p];
223     idx = [p intValue] - 1;
224
225     parts = [info valueForKey:@"parts"];
226     mt = [[info valueForKey:@"type"] lowercaseString];
227     if ([mt isEqualToString:@"message"]) {
228       /* we have special behaviour for message types */
229       id body;
230       
231       if ((body = [info valueForKey:@"body"]) != nil) {
232         mt = [body valueForKey:@"type"];
233         if ([mt isEqualToString:@"multipart"])
234           parts = [body valueForKey:@"parts"];
235         else
236           parts = [NSArray arrayWithObject:body];
237       }
238     }
239     
240     if (idx >= [parts count]) {
241       [self errorWithFormat:
242               @"body part index out of bounds(idx=%d vs count=%d): %@", 
243               (idx + 1), [parts count], info];
244       return nil;
245     }
246     info = [parts objectAtIndex:idx];
247   }
248   return [info isNotNull] ? info : nil;
249 }
250
251 /* content */
252
253 - (NSData *)content {
254   NSData *content;
255   id     result, fullResult;
256   
257   fullResult = [self fetchParts:[NSArray arrayWithObject:@"RFC822"]];
258   if (fullResult == nil)
259     return nil;
260   
261   if ([fullResult isKindOfClass:[NSException class]])
262     return fullResult;
263   
264   /* extract fetch result */
265   
266   result = [fullResult valueForKey:@"fetch"];
267   if (![result isKindOfClass:[NSArray class]]) {
268     [self logWithFormat:
269             @"ERROR: unexpected IMAP4 result (missing 'fetch'): %@", 
270             fullResult];
271     return [NSException exceptionWithHTTPStatus:500 /* server error */
272                         reason:@"unexpected IMAP4 result"];
273   }
274   if ([result count] == 0)
275     return nil;
276   
277   result = [result objectAtIndex:0];
278   
279   /* extract message */
280   
281   if ((content = [result valueForKey:@"message"]) == nil) {
282     [self logWithFormat:
283             @"ERROR: unexpected IMAP4 result (missing 'message'): %@", 
284             result];
285     return [NSException exceptionWithHTTPStatus:500 /* server error */
286                         reason:@"unexpected IMAP4 result"];
287   }
288   
289   return [[content copy] autorelease];
290 }
291
292 - (NSString *)contentAsString {
293   NSString *s;
294   NSData *content;
295   
296   if ((content = [self content]) == nil)
297     return nil;
298   if ([content isKindOfClass:[NSException class]])
299     return (id)content;
300   
301   s = [[NSString alloc] initWithData:content 
302                         encoding:NSISOLatin1StringEncoding];
303   if (s == nil) {
304     [self logWithFormat:
305             @"ERROR: could not convert data of length %d to string", 
306             [content length]];
307     return nil;
308   }
309   return [s autorelease];
310 }
311
312 /* bulk fetching of plain/text content */
313
314 - (BOOL)shouldFetchPartOfType:(NSString *)_type subtype:(NSString *)_subtype {
315   _type    = [_type    lowercaseString];
316   _subtype = [_subtype lowercaseString];
317   
318   if ([_type isEqualToString:@"text"]) {
319     if ([_subtype isEqualToString:@"plain"])
320       return YES;
321
322     if ([_subtype isEqualToString:@"calendar"]) /* we also fetch calendars */
323       return YES;
324   }
325
326   if ([_type isEqualToString:@"application"]) {
327     if ([_subtype isEqualToString:@"pgp-signature"])
328       return YES;
329   }
330   
331   return NO;
332 }
333
334 - (void)addRequiredKeysOfStructure:(id)_info path:(NSString *)_p
335   toArray:(NSMutableArray *)_keys
336   recurse:(BOOL)_recurse
337 {
338   NSArray  *parts;
339   unsigned i, count;
340   BOOL fetchPart;
341   id body;
342   
343   fetchPart = [self shouldFetchPartOfType:[_info valueForKey:@"type"]
344                     subtype:[_info valueForKey:@"subtype"]];
345   if (fetchPart) {
346     NSString *k;
347     
348     if ([_p length] > 0) {
349       k = [[@"body[" stringByAppendingString:_p] stringByAppendingString:@"]"];
350     }
351     else {
352       /*
353         for some reason we need to add ".TEXT" for plain text stuff on root
354         entities?
355         TODO: check with HTML
356       */
357       k = @"body[text]";
358     }
359     [_keys addObject:k];
360   }
361   
362   if (!_recurse)
363     return;
364   
365   /* recurse */
366   
367   parts = [_info objectForKey:@"parts"];
368   for (i = 0, count = [parts count]; i < count; i++) {
369     NSString *sp;
370     id childInfo;
371     
372     sp = [_p length] > 0
373       ? [_p stringByAppendingFormat:@".%d", i + 1]
374       : [NSString stringWithFormat:@"%d", i + 1];
375     
376     childInfo = [parts objectAtIndex:i];
377     
378     [self addRequiredKeysOfStructure:childInfo path:sp toArray:_keys
379           recurse:YES];
380   }
381   
382   /* check body */
383   
384   if ((body = [_info objectForKey:@"body"]) != nil) {
385     NSString *sp;
386
387     sp = [[body valueForKey:@"type"] lowercaseString];
388     if ([sp isEqualToString:@"multipart"])
389       sp = _p;
390     else
391       sp = [_p length] > 0 ? [_p stringByAppendingString:@".1"] : @"1";
392     [self addRequiredKeysOfStructure:body path:sp toArray:_keys
393           recurse:YES];
394   }
395 }
396
397 - (NSArray *)plainTextContentFetchKeys {
398   NSMutableArray *ma;
399   
400   ma = [NSMutableArray arrayWithCapacity:4];
401   [self addRequiredKeysOfStructure:[[self clientObject] bodyStructure]
402         path:@"" toArray:ma recurse:YES];
403   return ma;
404 }
405
406 - (NSDictionary *)fetchPlainTextParts:(NSArray *)_fetchKeys {
407   NSMutableDictionary *flatContents;
408   unsigned i, count;
409   id result;
410   
411   [self debugWithFormat:@"fetch keys: %@", _fetchKeys];
412   
413   result = [self fetchParts:_fetchKeys];
414   result = [result valueForKey:@"RawResponse"]; // hackish
415   result = [result objectForKey:@"fetch"]; // Note: -valueForKey: doesn't work!
416   
417   count        = [_fetchKeys count];
418   flatContents = [NSMutableDictionary dictionaryWithCapacity:count];
419   for (i = 0; i < count; i++) {
420     NSString *key;
421     NSData   *data;
422     
423     key  = [_fetchKeys objectAtIndex:i];
424     data = [[result objectForKey:key] objectForKey:@"data"];
425     
426     if (![data isNotNull]) {
427       [self debugWithFormat:@"got no data fork key: %@", key];
428       continue;
429     }
430
431     if ([key isEqualToString:@"body[text]"])
432       key = @""; // see key collector
433     else if ([key hasPrefix:@"body["]) {
434       NSRange r;
435       
436       key = [key substringFromIndex:5];
437       r   = [key rangeOfString:@"]"];
438       if (r.length > 0)
439         key = [key substringToIndex:r.location];
440     }
441     [flatContents setObject:data forKey:key];
442   }
443   return flatContents;
444 }
445
446 - (NSDictionary *)fetchPlainTextParts {
447   return [self fetchPlainTextParts:[self plainTextContentFetchKeys]];
448 }
449
450 /* convert parts to strings */
451
452 - (NSString *)stringForData:(NSData *)_data partInfo:(NSDictionary *)_info {
453   NSString *charset;
454   NSString *s;
455   
456   if (![_data isNotNull])
457     return nil;
458
459   s = nil;
460   
461   charset = [[_info valueForKey:@"parameterList"] valueForKey:@"charset"];
462   if ([charset isNotNull] && [charset length] > 0)
463     s = [NSString stringWithData:_data usingEncodingNamed:charset];
464   
465   if (s == nil) { /* no charset provided, fall back to UTF-8 */
466     s = [[NSString alloc] initWithData:_data encoding:NSUTF8StringEncoding];
467     s = [s autorelease];
468   }
469   
470   return s;
471 }
472
473 - (NSDictionary *)stringifyTextParts:(NSDictionary *)_datas {
474   NSMutableDictionary *md;
475   NSEnumerator *keys;
476   NSString     *key;
477   
478   md   = [NSMutableDictionary dictionaryWithCapacity:4];
479   keys = [_datas keyEnumerator];
480   while ((key = [keys nextObject]) != nil) {
481     NSDictionary *info;
482     NSString *s;
483     
484     info = [self lookupInfoForBodyPart:key];
485     if ((s = [self stringForData:[_datas objectForKey:key] partInfo:info]))
486       [md setObject:s forKey:key];
487   }
488   return md;
489 }
490 - (NSDictionary *)fetchPlainTextStrings:(NSArray *)_fetchKeys {
491   /*
492     The fetched parts are NSData objects, this method converts them into
493     NSString objects based on the information inside the bodystructure.
494     
495     The fetch-keys are body fetch-keys like: body[text] or body[1.2.3].
496     The keys in the result dictionary are "" for 'text' and 1.2.3 for parts.
497   */
498   NSDictionary *datas;
499   
500   if ((datas = [self fetchPlainTextParts:_fetchKeys]) == nil)
501     return nil;
502   if ([datas isKindOfClass:[NSException class]])
503     return datas;
504   
505   return [self stringifyTextParts:datas];
506 }
507
508 /* flags */
509
510 - (NSException *)addFlags:(id)_flags {
511   return [[self mailManager] addFlags:_flags toURL:[self imap4URL] 
512                              password:[self imap4Password]];
513 }
514 - (NSException *)removeFlags:(id)_flags {
515   return [[self mailManager] removeFlags:_flags toURL:[self imap4URL] 
516                              password:[self imap4Password]];
517 }
518
519 /* permissions */
520
521 - (BOOL)isDeletionAllowed {
522   return [[self container] isDeleteAndExpungeAllowed];
523 }
524
525 /* name lookup */
526
527 - (id)lookupImap4BodyPartKey:(NSString *)_key inContext:(id)_ctx {
528   // TODO: we might want to check for existence prior controller creation
529   Class clazz;
530   
531   clazz = [SOGoMailBodyPart bodyPartClassForKey:_key inContext:_ctx];
532   return [[[clazz alloc] initWithName:_key inContainer:self] autorelease];
533 }
534
535 - (id)lookupName:(NSString *)_key inContext:(id)_ctx acquire:(BOOL)_flag {
536   id obj;
537   
538   /* first check attributes directly bound to the application */
539   if ((obj = [super lookupName:_key inContext:_ctx acquire:NO]) != nil)
540     return obj;
541   
542   /* lookup body part */
543   
544   if ([self isBodyPartKey:_key inContext:_ctx]) {
545     if ((obj = [self lookupImap4BodyPartKey:_key inContext:_ctx]) != nil)
546       return obj;
547   }
548   
549   /* return 404 to stop acquisition */
550   return [NSException exceptionWithHTTPStatus:404 /* Not Found */];
551 }
552
553 /* WebDAV */
554
555 - (BOOL)davIsCollection {
556   /* while a mail has child objects, it should appear as a file in WebDAV */
557   return NO;
558 }
559
560 - (id)davContentLength {
561   return [[self fetchCoreInfos] valueForKey:@"size"];
562 }
563
564 - (NSDate *)davCreationDate {
565   // TODO: use INTERNALDATE once NGImap4 supports that
566   return nil;
567 }
568 - (NSDate *)davLastModified {
569   return [self davCreationDate];
570 }
571
572 - (NSException *)davMoveToTargetObject:(id)_target newName:(NSString *)_name
573   inContext:(id)_ctx
574 {
575   [self logWithFormat:@"TODO: should move mail as '%@' to: %@",
576         _name, _target];
577   return [NSException exceptionWithHTTPStatus:501 /* Not Implemented */
578                       reason:@"not implemented"];
579 }
580
581 - (NSException *)davCopyToTargetObject:(id)_target newName:(NSString *)_name
582   inContext:(id)_ctx
583 {
584   /* 
585      Note: this is special because we create SOGoMailObject's even if they do
586            not exist (for performance reasons).
587
588      Also: we cannot really take a target resource, the ID will be assigned by
589            the IMAP4 server.
590            We even cannot return a 'location' header instead because IMAP4
591            doesn't tell us the new ID.
592   */
593   NSURL *destImap4URL;
594   
595   destImap4URL = ([_name length] == 0)
596     ? [[_target container] imap4URL]
597     : [_target imap4URL];
598   
599   return [[self mailManager] copyMailURL:[self imap4URL] 
600                              toFolderURL:destImap4URL
601                              password:[self imap4Password]];
602 }
603
604 /* actions */
605
606 - (id)GETAction:(WOContext *)_ctx {
607   WOResponse *r;
608   NSData     *content;
609   
610   content = [self content];
611   if ([content isKindOfClass:[NSException class]])
612     return content;
613   if (content == nil) {
614     return [NSException exceptionWithHTTPStatus:404 /* Not Found */
615                         reason:@"did not find IMAP4 message"];
616   }
617   
618   r = [_ctx response];
619   [r setHeader:@"message/rfc822" forKey:@"content-type"];
620   [r setContent:content];
621   return r;
622 }
623
624 /* operations */
625
626 - (NSException *)trashInContext:(id)_ctx {
627   /*
628     Trashing is three actions:
629     a) copy to trash folder
630     b) mark mail as deleted
631     c) expunge folder
632     
633     In case b) or c) fails, we can't do anything because IMAP4 doesn't tell us
634     the ID used in the trash folder.
635   */
636   SOGoMailFolder *trashFolder;
637   NSException    *error;
638
639   // TODO: check for safe HTTP method
640   
641   trashFolder = [[self mailAccountFolder] trashFolderInContext:_ctx];
642   if ([trashFolder isKindOfClass:[NSException class]])
643     return (NSException *)trashFolder;
644   if (![trashFolder isNotNull]) {
645     return [NSException exceptionWithHTTPStatus:500 /* Server Error */
646                         reason:@"Did not find Trash folder!"];
647   }
648   [trashFolder flushMailCaches];
649
650   /* a) copy */
651   
652   error = [self davCopyToTargetObject:trashFolder
653                 newName:@"fakeNewUnusedByIMAP4" /* autoassigned */
654                 inContext:_ctx];
655   if (error != nil) return error;
656
657   /* b) mark deleted */
658   
659   error = [[self mailManager] markURLDeleted:[self imap4URL] 
660                               password:[self imap4Password]];
661   if (error != nil) return error;
662   
663   /* c) expunge */
664
665   error = [[self mailManager] expungeAtURL:[[self container] imap4URL]
666                               password:[self imap4Password]];
667   if (error != nil) return error; // TODO: unflag as deleted?
668   [self flushMailCaches];
669   
670   return nil;
671 }
672
673 - (NSException *)delete {
674   /* 
675      Note: delete is different to DELETEAction: for mails! The 'delete' runs
676            either flags a message as deleted or moves it to the Trash while
677            the DELETEAction: really deletes a message (by flagging it as
678            deleted _AND_ performing an expunge).
679   */
680   // TODO: copy to Trash folder
681   NSException *error;
682
683   // TODO: check for safe HTTP method
684   
685   error = [[self mailManager] markURLDeleted:[self imap4URL] 
686                               password:[self imap4Password]];
687   return error;
688 }
689 - (id)DELETEAction:(id)_ctx {
690   NSException *error;
691   
692   // TODO: ensure safe HTTP method
693   
694   error = [[self mailManager] markURLDeleted:[self imap4URL] 
695                               password:[self imap4Password]];
696   if (error != nil) return error;
697   
698   error = [[self mailManager] expungeAtURL:[[self container] imap4URL]
699                               password:[self imap4Password]];
700   if (error != nil) return error; // TODO: unflag as deleted?
701   
702   return [NSNumber numberWithBool:YES]; /* delete was successful */
703 }
704
705 /* debugging */
706
707 - (BOOL)isDebuggingEnabled {
708   return debugOn;
709 }
710
711 @end /* SOGoMailObject */