]> err.no Git - scalable-opengroupware.org/blob - SoObjects/Mailer/SOGoMailObject.m
4b30424b0cd0b2ab732964d93b1b2aa6ef562203
[scalable-opengroupware.org] / 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 <NGMail/NGMimeMessageParser.h>
30 #include "common.h"
31
32 @implementation SOGoMailObject
33
34 static NSArray  *coreInfoKeys = nil;
35 static NSString *mailETag = nil;
36 static BOOL heavyDebug         = NO;
37 static BOOL fetchHeader        = YES;
38 static BOOL debugOn            = NO;
39 static BOOL debugBodyStructure = NO;
40 static BOOL debugSoParts       = NO;
41
42 + (int)version {
43   return [super version] + 0 /* v1 */;
44 }
45
46 + (void)initialize {
47   NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
48   
49   NSAssert2([super version] == 1,
50             @"invalid superclass (%@) version %i !",
51             NSStringFromClass([self superclass]), [super version]);
52   
53   if ((fetchHeader = ([ud boolForKey:@"SOGoDoNotFetchMailHeader"] ? NO : YES)))
54     NSLog(@"Note: fetching full mail header.");
55   else
56     NSLog(@"Note: not fetching full mail header: 'SOGoDoNotFetchMailHeader'");
57   
58   /* Note: see SOGoMailManager.m for allowed IMAP4 keys */
59   /* Note: "BODY" actually returns the structure! */
60   if (fetchHeader) {
61     coreInfoKeys = [[NSArray alloc] initWithObjects:
62                                       @"FLAGS", @"ENVELOPE", @"BODY",
63                                       @"RFC822.SIZE",
64                                       @"RFC822.HEADER",
65                                       // not yet supported: @"INTERNALDATE",
66                                     nil];
67   }
68   else {
69     coreInfoKeys = [[NSArray alloc] initWithObjects:
70                                       @"FLAGS", @"ENVELOPE", @"BODY",
71                                       @"RFC822.SIZE",
72                                       // not yet supported: @"INTERNALDATE",
73                                     nil];
74   }
75
76   if (![[ud objectForKey:@"SOGoMailDisableETag"] boolValue]) {
77     mailETag = [[NSString alloc] initWithFormat:@"\"imap4url_%d_%d_%03d\"",
78                                  UIX_MAILER_MAJOR_VERSION,
79                                  UIX_MAILER_MINOR_VERSION,
80                                  UIX_MAILER_SUBMINOR_VERSION];
81     NSLog(@"Note(SOGoMailObject): using constant etag for mail parts: '%@'", 
82           mailETag);
83   }
84   else
85     NSLog(@"Note(SOGoMailObject): etag caching disabled!");
86 }
87
88 - (void)dealloc {
89   [self->headers    release];
90   [self->headerPart release];
91   [self->coreInfos  release];
92   [super dealloc];
93 }
94
95 /* IMAP4 */
96
97 - (NSString *)relativeImap4Name {
98   return [[self nameInContainer] stringByDeletingPathExtension];
99 }
100
101 /* hierarchy */
102
103 - (SOGoMailObject *)mailObject {
104   return self;
105 }
106
107 /* part hierarchy */
108
109 - (NSString *)keyExtensionForPart:(id)_partInfo {
110   NSString *mt, *st;
111   
112   if (_partInfo == nil)
113     return nil;
114   
115   mt = [_partInfo valueForKey:@"type"];
116   st = [[_partInfo valueForKey:@"subtype"] lowercaseString];
117   if ([mt isEqualToString:@"text"]) {
118     if ([st isEqualToString:@"plain"])    return @".txt";
119     if ([st isEqualToString:@"html"])     return @".html";
120     if ([st isEqualToString:@"calendar"]) return @".ics";
121     if ([st isEqualToString:@"x-vcard"])  return @".vcf";
122   }
123   else if ([mt isEqualToString:@"image"])
124     return [@"." stringByAppendingString:st];
125   else if ([mt isEqualToString:@"application"]) {
126     if ([st isEqualToString:@"pgp-signature"])
127       return @".asc";
128   }
129   
130   return nil;
131 }
132
133 - (NSArray *)relationshipKeysWithParts:(BOOL)_withParts {
134   /* should return non-multipart children */
135   NSMutableArray *ma;
136   NSArray *parts;
137   unsigned i, count;
138   
139   parts = [[self bodyStructure] valueForKey:@"parts"];
140   if (![parts isNotNull]) 
141     return nil;
142   if ((count = [parts count]) == 0)
143     return nil;
144   
145   for (i = 0, ma = nil; i < count; i++) {
146     NSString *key, *ext;
147     id   part;
148     BOOL hasParts;
149     
150     part     = [parts objectAtIndex:i];
151     hasParts = [part valueForKey:@"parts"] != nil ? YES:NO;
152     if ((hasParts && !_withParts) || (_withParts && !hasParts))
153       continue;
154
155     if (ma == nil)
156       ma = [NSMutableArray arrayWithCapacity:count - i];
157     
158     ext = [self keyExtensionForPart:part];
159     key = [[NSString alloc] initWithFormat:@"%d%@", i + 1, ext?ext:@""];
160     [ma addObject:key];
161     [key release];
162   }
163   return ma;
164 }
165
166 - (NSArray *)toOneRelationshipKeys {
167   return [self relationshipKeysWithParts:NO];
168 }
169 - (NSArray *)toManyRelationshipKeys {
170   return [self relationshipKeysWithParts:YES];
171 }
172
173 /* message */
174
175 - (id)fetchParts:(NSArray *)_parts {
176   // TODO: explain what it does
177   /*
178     Called by -fetchPlainTextParts:
179   */
180   return [[self imap4Connection] fetchURL:[self imap4URL] parts:_parts];
181 }
182
183 /* core infos */
184
185 - (BOOL)doesMailExist {
186   static NSArray *existsKey = nil;
187   id msgs;
188   
189   if (self->coreInfos != nil) /* if we have coreinfos, we can use them */
190     return [self->coreInfos isNotNull];
191   
192   /* otherwise fetch something really simple */
193   
194   if (existsKey == nil) /* we use size, other suggestions? */
195     existsKey = [[NSArray alloc] initWithObjects:@"RFC822.SIZE", nil];
196   
197   msgs = [self fetchParts:existsKey]; // returns dict
198   msgs = [msgs valueForKey:@"fetch"];
199   return [msgs count] > 0 ? YES : NO;
200 }
201
202 - (id)fetchCoreInfos {
203   id msgs;
204   
205   if (self->coreInfos != nil)
206     return [self->coreInfos isNotNull] ? self->coreInfos : nil;
207   
208 #if 0 // TODO: old code, why was it using clientObject??
209   msgs = [[self clientObject] fetchParts:coreInfoKeys]; // returns dict
210 #else
211   msgs = [self fetchParts:coreInfoKeys]; // returns dict
212 #endif
213   if (heavyDebug) [self logWithFormat:@"M: %@", msgs];
214   msgs = [msgs valueForKey:@"fetch"];
215   if ([msgs count] == 0)
216     return nil;
217   
218   self->coreInfos = [[msgs objectAtIndex:0] retain];
219   return self->coreInfos;
220 }
221
222 - (id)bodyStructure {
223   id body;
224
225   body = [[self fetchCoreInfos] valueForKey:@"body"];
226   if (debugBodyStructure)
227     [self logWithFormat:@"BODY: %@", body];
228   return body;
229 }
230
231 - (NGImap4Envelope *)envelope {
232   return [[self fetchCoreInfos] valueForKey:@"envelope"];
233 }
234 - (NSString *)subject {
235   return [[self envelope] subject];
236 }
237 - (NSCalendarDate *)date {
238   return [[self envelope] date];
239 }
240 - (NSArray *)fromEnvelopeAddresses {
241   return [[self envelope] from];
242 }
243 - (NSArray *)toEnvelopeAddresses {
244   return [[self envelope] to];
245 }
246 - (NSArray *)ccEnvelopeAddresses {
247   return [[self envelope] cc];
248 }
249
250 - (NSData *)mailHeaderData {
251   return [[self fetchCoreInfos] valueForKey:@"header"];
252 }
253 - (BOOL)hasMailHeaderInCoreInfos {
254   return [[self mailHeaderData] length] > 0 ? YES : NO;
255 }
256
257 - (id)mailHeaderPart {
258   NGMimeMessageParser *parser;
259   NSData *data;
260   
261   if (self->headerPart != nil)
262     return [self->headerPart isNotNull] ? self->headerPart : nil;
263   
264   if ([(data = [self mailHeaderData]) length] == 0)
265     return nil;
266   
267   // TODO: do we need to set some delegate method which stops parsing the body?
268   parser = [[NGMimeMessageParser alloc] init];
269   self->headerPart = [[parser parsePartFromData:data] retain];
270   [parser release]; parser = nil;
271
272   if (self->headerPart == nil) {
273     self->headerPart = [[NSNull null] retain];
274     return nil;
275   }
276   return self->headerPart;
277 }
278 - (NSDictionary *)mailHeaders {
279   if (self->headers == nil)
280     self->headers = [[[self mailHeaderPart] headers] copy];
281   return self->headers;
282 }
283
284 - (id)lookupInfoForBodyPart:(id)_path {
285   NSEnumerator *pe;
286   NSString *p;
287   id info;
288
289   if (![_path isNotNull])
290     return nil;
291   
292   if ((info = [self bodyStructure]) == nil) {
293     [self errorWithFormat:@"got no body part structure!"];
294     return nil;
295   }
296
297   /* ensure array argument */
298   
299   if ([_path isKindOfClass:[NSString class]]) {
300     if ([_path length] == 0)
301       return info;
302     
303     _path = [_path componentsSeparatedByString:@"."];
304   }
305   
306   /* 
307      For each path component, eg 1,1,3 
308      
309      Remember that we need special processing for message/rfc822 which maps the
310      namespace of multiparts directly into the main namespace.
311      
312      TODO(hh): no I don't remember, please explain in more detail!
313   */
314   pe = [_path objectEnumerator];
315   while ((p = [pe nextObject]) != nil && [info isNotNull]) {
316     unsigned idx;
317     NSArray  *parts;
318     NSString *mt;
319     
320     [self debugWithFormat:@"check PATH: %@", p];
321     idx = [p intValue] - 1;
322
323     parts = [info valueForKey:@"parts"];
324     mt = [[info valueForKey:@"type"] lowercaseString];
325     if ([mt isEqualToString:@"message"]) {
326       /* we have special behaviour for message types */
327       id body;
328       
329       if ((body = [info valueForKey:@"body"]) != nil) {
330         mt = [body valueForKey:@"type"];
331         if ([mt isEqualToString:@"multipart"])
332           parts = [body valueForKey:@"parts"];
333         else
334           parts = [NSArray arrayWithObject:body];
335       }
336     }
337     
338     if (idx >= [parts count]) {
339       [self errorWithFormat:
340               @"body part index out of bounds(idx=%d vs count=%d): %@", 
341               (idx + 1), [parts count], info];
342       return nil;
343     }
344     info = [parts objectAtIndex:idx];
345   }
346   return [info isNotNull] ? info : nil;
347 }
348
349 /* content */
350
351 - (NSData *)content {
352   NSData *content;
353   id     result, fullResult;
354   
355   fullResult = [self fetchParts:[NSArray arrayWithObject:@"RFC822"]];
356   if (fullResult == nil)
357     return nil;
358   
359   if ([fullResult isKindOfClass:[NSException class]])
360     return fullResult;
361   
362   /* extract fetch result */
363   
364   result = [fullResult valueForKey:@"fetch"];
365   if (![result isKindOfClass:[NSArray class]]) {
366     [self logWithFormat:
367             @"ERROR: unexpected IMAP4 result (missing 'fetch'): %@", 
368             fullResult];
369     return [NSException exceptionWithHTTPStatus:500 /* server error */
370                         reason:@"unexpected IMAP4 result"];
371   }
372   if ([result count] == 0)
373     return nil;
374   
375   result = [result objectAtIndex:0];
376   
377   /* extract message */
378   
379   if ((content = [result valueForKey:@"message"]) == nil) {
380     [self logWithFormat:
381             @"ERROR: unexpected IMAP4 result (missing 'message'): %@", 
382             result];
383     return [NSException exceptionWithHTTPStatus:500 /* server error */
384                         reason:@"unexpected IMAP4 result"];
385   }
386   
387   return [[content copy] autorelease];
388 }
389
390 - (NSString *)contentAsString {
391   NSString *s;
392   NSData *content;
393   
394   if ((content = [self content]) == nil)
395     return nil;
396   if ([content isKindOfClass:[NSException class]])
397     return (id)content;
398   
399   s = [[NSString alloc] initWithData:content 
400                         encoding:NSISOLatin1StringEncoding];
401   if (s == nil) {
402     [self logWithFormat:
403             @"ERROR: could not convert data of length %d to string", 
404             [content length]];
405     return nil;
406   }
407   return [s autorelease];
408 }
409
410 /* bulk fetching of plain/text content */
411
412 - (BOOL)shouldFetchPartOfType:(NSString *)_type subtype:(NSString *)_subtype {
413   /*
414     This method decides which parts are 'prefetched' for display. Those are
415     usually text parts (the set is currently hardcoded in this method ...).
416   */
417   _type    = [_type    lowercaseString];
418   _subtype = [_subtype lowercaseString];
419   
420   if ([_type isEqualToString:@"text"]) {
421     if ([_subtype isEqualToString:@"plain"])
422       return YES;
423
424     if ([_subtype isEqualToString:@"calendar"]) /* we also fetch calendars */
425       return YES;
426   }
427
428   if ([_type isEqualToString:@"application"]) {
429     if ([_subtype isEqualToString:@"pgp-signature"])
430       return YES;
431     if ([_subtype hasPrefix:@"x-vnd.kolab."])
432       return YES;
433   }
434   
435   return NO;
436 }
437
438 - (void)addRequiredKeysOfStructure:(id)_info path:(NSString *)_p
439   toArray:(NSMutableArray *)_keys
440   recurse:(BOOL)_recurse
441 {
442   /* 
443      This is used to collect the set of IMAP4 fetch-keys required to fetch
444      the basic parts of the body structure. That is, to fetch all parts which
445      are displayed 'inline' in a single IMAP4 fetch.
446      
447      The method calls itself recursively to walk the body structure.
448   */
449   NSArray  *parts;
450   unsigned i, count;
451   BOOL fetchPart;
452   id body;
453   
454   /* Note: if the part itself doesn't qualify, we still check subparts */
455   fetchPart = [self shouldFetchPartOfType:[_info valueForKey:@"type"]
456                     subtype:[_info valueForKey:@"subtype"]];
457   if (fetchPart) {
458     NSString *k;
459     
460     if ([_p length] > 0) {
461       k = [[@"body[" stringByAppendingString:_p] stringByAppendingString:@"]"];
462     }
463     else {
464       /*
465         for some reason we need to add ".TEXT" for plain text stuff on root
466         entities?
467         TODO: check with HTML
468       */
469       k = @"body[text]";
470     }
471     [_keys addObject:k];
472   }
473   
474   if (!_recurse)
475     return;
476   
477   /* recurse */
478   
479   parts = [(NSDictionary *)_info objectForKey:@"parts"];
480   for (i = 0, count = [parts count]; i < count; i++) {
481     NSString *sp;
482     id childInfo;
483     
484     sp = ([_p length] > 0)
485       ? [_p stringByAppendingFormat:@".%d", i + 1]
486       : [NSString stringWithFormat:@"%d", i + 1];
487     
488     childInfo = [parts objectAtIndex:i];
489     
490     [self addRequiredKeysOfStructure:childInfo path:sp toArray:_keys
491           recurse:YES];
492   }
493   
494   /* check body */
495   
496   if ((body = [(NSDictionary *)_info objectForKey:@"body"]) != nil) {
497     NSString *sp;
498
499     sp = [[body valueForKey:@"type"] lowercaseString];
500     if ([sp isEqualToString:@"multipart"])
501       sp = _p;
502     else
503       sp = [_p length] > 0 ? [_p stringByAppendingString:@".1"] : @"1";
504     [self addRequiredKeysOfStructure:body path:sp toArray:_keys
505           recurse:YES];
506   }
507 }
508
509 - (NSArray *)plainTextContentFetchKeys {
510   /*
511     The name is not 100% correct. The method returns all body structure fetch
512     keys which are marked by the -shouldFetchPartOfType:subtype: method.
513   */
514   NSMutableArray *ma;
515   
516   ma = [NSMutableArray arrayWithCapacity:4];
517   [self addRequiredKeysOfStructure:[[self clientObject] bodyStructure]
518         path:@"" toArray:ma recurse:YES];
519   return ma;
520 }
521
522 - (NSDictionary *)fetchPlainTextParts:(NSArray *)_fetchKeys {
523   // TODO: is the name correct or does it also fetch other parts?
524   NSMutableDictionary *flatContents;
525   unsigned i, count;
526   id result;
527   
528   [self debugWithFormat:@"fetch keys: %@", _fetchKeys];
529   
530   result = [self fetchParts:_fetchKeys];
531   result = [result valueForKey:@"RawResponse"]; // hackish
532   
533   // Note: -valueForKey: doesn't work!
534   result = [(NSDictionary *)result objectForKey:@"fetch"]; 
535   
536   count        = [_fetchKeys count];
537   flatContents = [NSMutableDictionary dictionaryWithCapacity:count];
538   for (i = 0; i < count; i++) {
539     NSString *key;
540     NSData   *data;
541     
542     key  = [_fetchKeys objectAtIndex:i];
543     data = [(NSDictionary *)[(NSDictionary *)result objectForKey:key] 
544                             objectForKey:@"data"];
545     
546     if (![data isNotNull]) {
547       [self errorWithFormat:@"got no data for key: %@", key];
548       continue;
549     }
550     
551     if ([key isEqualToString:@"body[text]"])
552       key = @""; // see key collector for explanation (TODO: where?)
553     else if ([key hasPrefix:@"body["]) {
554       NSRange r;
555       
556       key = [key substringFromIndex:5];
557       r   = [key rangeOfString:@"]"];
558       if (r.length > 0)
559         key = [key substringToIndex:r.location];
560     }
561     [flatContents setObject:data forKey:key];
562   }
563   return flatContents;
564 }
565
566 - (NSDictionary *)fetchPlainTextParts {
567   return [self fetchPlainTextParts:[self plainTextContentFetchKeys]];
568 }
569
570 /* convert parts to strings */
571
572 - (NSString *)stringForData:(NSData *)_data partInfo:(NSDictionary *)_info
573 {
574   NSString *charset, *encoding, *s;
575   NSData *mailData;
576   
577   if (![_data isNotNull])
578     return nil;
579
580   s = nil;
581
582   encoding = [[_info objectForKey:@"encoding"] lowercaseString];
583
584   if ([encoding isEqualToString: @"7bit"]
585       || [encoding isEqualToString: @"8bit"])
586     mailData = _data;
587   else if ([encoding isEqualToString: @"base64"])
588     mailData = [_data dataByDecodingBase64];
589   else if ([encoding isEqualToString: @"quoted-printable"])
590     mailData = [_data dataByDecodingQuotedPrintable];
591   
592   charset = [[_info valueForKey:@"parameterList"] valueForKey: @"charset"];
593   if (![charset length])
594     {
595       s = [[NSString alloc] initWithData:mailData encoding:NSUTF8StringEncoding];
596       [s autorelease];
597     }
598   else
599     s = [NSString stringWithData: mailData
600                   usingEncodingNamed: charset];
601
602   return s;
603 }
604
605 - (NSDictionary *)stringifyTextParts:(NSDictionary *)_datas {
606   NSMutableDictionary *md;
607   NSEnumerator *keys;
608   NSString     *key;
609   
610   md   = [NSMutableDictionary dictionaryWithCapacity:4];
611   keys = [_datas keyEnumerator];
612   while ((key = [keys nextObject]) != nil) {
613     NSDictionary *info;
614     NSString *s;
615     
616     info = [self lookupInfoForBodyPart:key];
617     if ((s = [self stringForData:[_datas objectForKey:key] partInfo:info]))
618       [md setObject:s forKey:key];
619   }
620   return md;
621 }
622 - (NSDictionary *)fetchPlainTextStrings:(NSArray *)_fetchKeys {
623   /*
624     The fetched parts are NSData objects, this method converts them into
625     NSString objects based on the information inside the bodystructure.
626     
627     The fetch-keys are body fetch-keys like: body[text] or body[1.2.3].
628     The keys in the result dictionary are "" for 'text' and 1.2.3 for parts.
629   */
630   NSDictionary *datas;
631   
632   if ((datas = [self fetchPlainTextParts:_fetchKeys]) == nil)
633     return nil;
634   if ([datas isKindOfClass:[NSException class]])
635     return datas;
636   
637   return [self stringifyTextParts:datas];
638 }
639
640 /* flags */
641
642 - (NSException *)addFlags:(id)_flags {
643   return [[self imap4Connection] addFlags:_flags toURL:[self imap4URL]];
644 }
645 - (NSException *)removeFlags:(id)_flags {
646   return [[self imap4Connection] removeFlags:_flags toURL:[self imap4URL]];
647 }
648
649 /* permissions */
650
651 - (BOOL)isDeletionAllowed {
652   return [[self container] isDeleteAndExpungeAllowed];
653 }
654
655 /* name lookup */
656
657 - (id)lookupImap4BodyPartKey:(NSString *)_key inContext:(id)_ctx {
658   // TODO: we might want to check for existence prior controller creation
659   Class clazz;
660   
661   clazz = [SOGoMailBodyPart bodyPartClassForKey:_key inContext:_ctx];
662   return [[[clazz alloc] initWithName:_key inContainer:self] autorelease];
663 }
664
665 - (id)lookupName:(NSString *)_key inContext:(id)_ctx acquire:(BOOL)_flag {
666   id obj;
667   
668   /* first check attributes directly bound to the application */
669   if ((obj = [super lookupName:_key inContext:_ctx acquire:NO]) != nil)
670     return obj;
671   
672   /* lookup body part */
673   
674   if ([self isBodyPartKey:_key inContext:_ctx]) {
675     if ((obj = [self lookupImap4BodyPartKey:_key inContext:_ctx]) != nil) {
676       if (debugSoParts) 
677         [self logWithFormat:@"mail looked up part %@: %@", _key, obj];
678       return obj;
679     }
680   }
681   
682   /* return 404 to stop acquisition */
683   return [NSException exceptionWithHTTPStatus:404 /* Not Found */
684                       reason:@"Did not find mail method or part-reference!"];
685 }
686
687 /* WebDAV */
688
689 - (BOOL)davIsCollection {
690   /* while a mail has child objects, it should appear as a file in WebDAV */
691   return NO;
692 }
693
694 - (id)davContentLength {
695   return [[self fetchCoreInfos] valueForKey:@"size"];
696 }
697
698 - (NSDate *)davCreationDate {
699   // TODO: use INTERNALDATE once NGImap4 supports that
700   return nil;
701 }
702 - (NSDate *)davLastModified {
703   return [self davCreationDate];
704 }
705
706 - (NSException *)davMoveToTargetObject:(id)_target newName:(NSString *)_name
707   inContext:(id)_ctx
708 {
709   [self logWithFormat:@"TODO: should move mail as '%@' to: %@",
710         _name, _target];
711   return [NSException exceptionWithHTTPStatus:501 /* Not Implemented */
712                       reason:@"not implemented"];
713 }
714
715 - (NSException *)davCopyToTargetObject:(id)_target newName:(NSString *)_name
716   inContext:(id)_ctx
717 {
718   /* 
719      Note: this is special because we create SOGoMailObject's even if they do
720            not exist (for performance reasons).
721
722      Also: we cannot really take a target resource, the ID will be assigned by
723            the IMAP4 server.
724            We even cannot return a 'location' header instead because IMAP4
725            doesn't tell us the new ID.
726   */
727   NSURL *destImap4URL;
728   
729   destImap4URL = ([_name length] == 0)
730     ? [[_target container] imap4URL]
731     : [_target imap4URL];
732   
733   return [[self mailManager] copyMailURL:[self imap4URL] 
734                              toFolderURL:destImap4URL
735                              password:[self imap4Password]];
736 }
737
738 /* actions */
739
740 - (id)GETAction:(id)_ctx {
741   NSException *error;
742   WOResponse  *r;
743   NSData      *content;
744   
745   if ((error = [self matchesRequestConditionInContext:_ctx]) != nil) {
746     /* check whether the mail still exists */
747     if (![self doesMailExist]) {
748       return [NSException exceptionWithHTTPStatus:404 /* Not Found */
749                           reason:@"mail was deleted"];
750     }
751     return error; /* return 304 or 416 */
752   }
753   
754   content = [self content];
755   if ([content isKindOfClass:[NSException class]])
756     return content;
757   if (content == nil) {
758     return [NSException exceptionWithHTTPStatus:404 /* Not Found */
759                         reason:@"did not find IMAP4 message"];
760   }
761   
762   r = [(WOContext *)_ctx response];
763   [r setHeader:@"message/rfc822" forKey:@"content-type"];
764   [r setContent:content];
765   return r;
766 }
767
768 /* operations */
769
770 - (NSException *)trashInContext:(id)_ctx {
771   /*
772     Trashing is three actions:
773     a) copy to trash folder
774     b) mark mail as deleted
775     c) expunge folder
776     
777     In case b) or c) fails, we can't do anything because IMAP4 doesn't tell us
778     the ID used in the trash folder.
779   */
780   SOGoMailFolder *trashFolder;
781   NSException    *error;
782
783   // TODO: check for safe HTTP method
784   
785   trashFolder = [[self mailAccountFolder] trashFolderInContext:_ctx];
786   if ([trashFolder isKindOfClass:[NSException class]])
787     return (NSException *)trashFolder;
788   if (![trashFolder isNotNull]) {
789     return [NSException exceptionWithHTTPStatus:500 /* Server Error */
790                         reason:@"Did not find Trash folder!"];
791   }
792   [trashFolder flushMailCaches];
793
794   /* a) copy */
795   
796   error = [self davCopyToTargetObject:trashFolder
797                 newName:@"fakeNewUnusedByIMAP4" /* autoassigned */
798                 inContext:_ctx];
799   if (error != nil) return error;
800   
801   /* b) mark deleted */
802   
803   error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
804   if (error != nil) return error;
805   
806   /* c) expunge */
807
808   error = [[self imap4Connection] expungeAtURL:[[self container] imap4URL]];
809   if (error != nil) return error; // TODO: unflag as deleted?
810   [self flushMailCaches];
811   
812   return nil;
813 }
814
815 - (NSException *) moveToFolderNamed: (NSString *) folderName
816                           inContext: (id)_ctx
817 {
818   /*
819     Trashing is three actions:
820     a) copy to trash folder
821     b) mark mail as deleted
822     c) expunge folder
823     
824     In case b) or c) fails, we can't do anything because IMAP4 doesn't tell us
825     the ID used in the trash folder.
826   */
827   SOGoMailFolder *destFolder;
828   NSEnumerator *folders;
829   NSString *currentFolderName, *reason;
830   NSException    *error;
831
832   // TODO: check for safe HTTP method
833
834   destFolder = [self mailAccountsFolder];
835   folders = [[folderName componentsSeparatedByString: @"/"] objectEnumerator];
836   currentFolderName = [folders nextObject];
837   currentFolderName = [folders nextObject];
838
839   while (currentFolderName)
840     {
841       destFolder = [destFolder lookupName: currentFolderName
842                                inContext: _ctx
843                                acquire: NO];
844       if ([destFolder isKindOfClass: [NSException class]])
845         return (NSException *) destFolder;
846       currentFolderName = [folders nextObject];
847     }
848
849   if (!([destFolder isKindOfClass: [SOGoMailFolder class]]
850         && [destFolder isNotNull]))
851     {
852       reason = [NSString stringWithFormat: @"Did not find folder name '%@'!",
853                          folderName];
854       return [NSException exceptionWithHTTPStatus:500 /* Server Error */
855                           reason: reason];
856     }
857   [destFolder flushMailCaches];
858
859   /* a) copy */
860   
861   error = [self davCopyToTargetObject: destFolder
862                 newName:@"fakeNewUnusedByIMAP4" /* autoassigned */
863                 inContext:_ctx];
864   if (error != nil) return error;
865
866   /* b) mark deleted */
867   
868   error = [[self imap4Connection] markURLDeleted: [self imap4URL]];
869   if (error != nil) return error;
870   
871   /* c) expunge */
872
873   error = [[self imap4Connection] expungeAtURL:[[self container] imap4URL]];
874   if (error != nil) return error; // TODO: unflag as deleted?
875   [self flushMailCaches];
876   
877   return nil;
878 }
879
880 - (NSException *)delete {
881   /* 
882      Note: delete is different to DELETEAction: for mails! The 'delete' runs
883            either flags a message as deleted or moves it to the Trash while
884            the DELETEAction: really deletes a message (by flagging it as
885            deleted _AND_ performing an expunge).
886   */
887   // TODO: copy to Trash folder
888   NSException *error;
889
890   // TODO: check for safe HTTP method
891   
892   error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
893   return error;
894 }
895 - (id)DELETEAction:(id)_ctx {
896   NSException *error;
897   
898   // TODO: ensure safe HTTP method
899   
900   error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
901   if (error != nil) return error;
902   
903   error = [[self imap4Connection] expungeAtURL:[[self container] imap4URL]];
904   if (error != nil) return error; // TODO: unflag as deleted?
905   
906   return [NSNumber numberWithBool:YES]; /* delete was successful */
907 }
908
909 /* some mail classification */
910
911 - (BOOL)isKolabObject {
912   NSDictionary *h;
913   
914   if ((h = [self mailHeaders]) != nil)
915     return [[h objectForKey:@"x-kolab-type"] isNotEmpty];
916   
917   // TODO: we could check the body structure?
918   
919   return NO;
920 }
921
922 - (BOOL)isMailingListMail {
923   NSDictionary *h;
924   
925   if ((h = [self mailHeaders]) == nil)
926     return NO;
927   
928   return [[h objectForKey:@"list-id"] isNotEmpty];
929 }
930
931 - (BOOL)isVirusScanned {
932   NSDictionary *h;
933   
934   if ((h = [self mailHeaders]) == nil)
935     return NO;
936   
937   if (![[h objectForKey:@"x-virus-status"]  isNotEmpty]) return NO;
938   if (![[h objectForKey:@"x-virus-scanned"] isNotEmpty]) return NO;
939   return YES;
940 }
941
942 - (NSString *)scanListHeaderValue:(id)_value
943   forFieldWithPrefix:(NSString *)_prefix
944 {
945   /* Note: not very tolerant on embedded commands and <> */
946   // TODO: does not really belong here, should be a header-field-parser
947   NSRange r;
948   
949   if (![_value isNotEmpty])
950     return nil;
951   
952   if ([_value isKindOfClass:[NSArray class]]) {
953     NSEnumerator *e;
954     id value;
955
956     e = [_value objectEnumerator];
957     while ((value = [e nextObject]) != nil) {
958       value = [self scanListHeaderValue:value forFieldWithPrefix:_prefix];
959       if (value != nil) return value;
960     }
961     return nil;
962   }
963   
964   if (![_value isKindOfClass:[NSString class]])
965     return nil;
966   
967   /* check for commas in string values */
968   r = [_value rangeOfString:@","];
969   if (r.length > 0) {
970     return [self scanListHeaderValue:[_value componentsSeparatedByString:@","]
971                  forFieldWithPrefix:_prefix];
972   }
973
974   /* value qualifies */
975   if (![(NSString *)_value hasPrefix:_prefix])
976     return nil;
977   
978   /* unquote */
979   if ([_value characterAtIndex:0] == '<') {
980     r = [_value rangeOfString:@">"];
981     _value = (r.length == 0)
982       ? [_value substringFromIndex:1]
983       : [_value substringWithRange:NSMakeRange(1, r.location - 2)];
984   }
985
986   return _value;
987 }
988
989 - (NSString *)mailingListArchiveURL {
990   return [self scanListHeaderValue:
991                  [[self mailHeaders] objectForKey:@"list-archive"]
992                forFieldWithPrefix:@"<http://"];
993 }
994 - (NSString *)mailingListSubscribeURL {
995   return [self scanListHeaderValue:
996                  [[self mailHeaders] objectForKey:@"list-subscribe"]
997                forFieldWithPrefix:@"<http://"];
998 }
999 - (NSString *)mailingListUnsubscribeURL {
1000   return [self scanListHeaderValue:
1001                  [[self mailHeaders] objectForKey:@"list-unsubscribe"]
1002                forFieldWithPrefix:@"<http://"];
1003 }
1004
1005 /* etag support */
1006
1007 - (id)davEntityTag {
1008   /*
1009     Note: There is one thing which *can* change for an existing message,
1010           those are the IMAP4 flags (and annotations, which we do not use).
1011           Since we don't render the flags, it should be OK, if this changes
1012           we must embed the flagging into the etag.
1013   */
1014   return mailETag;
1015 }
1016 - (int)zlGenerationCount {
1017   return 0; /* mails never change */
1018 }
1019
1020 /* Outlook mail tagging */
1021
1022 - (NSString *)outlookMessageClass {
1023   NSString *type;
1024   
1025   if ((type = [[self mailHeaders] objectForKey:@"x-kolab-type"]) != nil) {
1026     if ([type isEqualToString:@"application/x-vnd.kolab.contact"])
1027       return @"IPM.Contact";
1028     if ([type isEqualToString:@"application/x-vnd.kolab.task"])
1029       return @"IPM.Task";
1030     if ([type isEqualToString:@"application/x-vnd.kolab.event"])
1031       return @"IPM.Appointment";
1032     if ([type isEqualToString:@"application/x-vnd.kolab.note"])
1033       return @"IPM.Note";
1034     if ([type isEqualToString:@"application/x-vnd.kolab.journal"])
1035       return @"IPM.Journal";
1036   }
1037   
1038   return @"IPM.Message"; /* email, default class */
1039 }
1040
1041 /* debugging */
1042
1043 - (BOOL)isDebuggingEnabled {
1044   return debugOn;
1045 }
1046
1047 @end /* SOGoMailObject */