1/*
2  Copyright (C) 2007-2017 Inverse inc.
3  Copyright (C) 2004-2005 SKYRIX Software AG
4
5  This file is part of SOGo.
6
7  SOGo is free software; you can redistribute it and/or modify it under
8  the terms of the GNU Lesser General Public License as published by the
9  Free Software Foundation; either version 2, or (at your option) any
10  later version.
11
12  SOGo is distributed in the hope that it will be useful, but WITHOUT ANY
13  WARRANTY; without even the implied warranty of MERCHANTABILITY or
14  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
15  License for more details.
16
17  You should have received a copy of the GNU Lesser General Public
18  License along with OGo; see the file COPYING.  If not, write to the
19  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
20  02111-1307, USA.
21*/
22
23#import <Foundation/NSURL.h>
24#import <Foundation/NSValue.h>
25#import <Foundation/NSFileHandle.h>
26
27#import <NGObjWeb/WOContext+SoObjects.h>
28#import <NGObjWeb/WORequest.h>
29#import <NGObjWeb/NSException+HTTP.h>
30#import <NGExtensions/NGHashMap.h>
31#import <NGExtensions/NSFileManager+Extensions.h>
32#import <NGExtensions/NSNull+misc.h>
33#import <NGExtensions/NSObject+Logs.h>
34#import <NGExtensions/NSString+Encoding.h>
35#import <NGExtensions/NSString+misc.h>
36#import <NGImap4/NGImap4Connection.h>
37#import <NGImap4/NGImap4Envelope.h>
38#import <NGImap4/NGImap4EnvelopeAddress.h>
39#import <NGMail/NGMimeMessageParser.h>
40#import <NGMime/NGMimeMultipartBody.h>
41#import <NGMime/NGMimeType.h>
42
43
44#import <SOGo/NSArray+Utilities.h>
45#import <SOGo/NSDictionary+Utilities.h>
46#import <SOGo/NSString+Utilities.h>
47#import <SOGo/SOGoPermissions.h>
48#import <SOGo/SOGoSystemDefaults.h>
49#import <SOGo/SOGoUser.h>
50#import <SOGo/SOGoUserDefaults.h>
51#import <SOGo/NSCalendarDate+SOGo.h>
52#import <SOGo/SOGoZipArchiver.h>
53
54#import "NSString+Mail.h"
55#import "NSData+Mail.h"
56#import "NSData+SMIME.h"
57#import "NSDictionary+Mail.h"
58#import "SOGoMailAccount.h"
59#import "SOGoMailFolder.h"
60#import "SOGoMailManager.h"
61#import "SOGoMailBodyPart.h"
62
63#import "SOGoMailObject.h"
64
65@implementation SOGoMailObject
66
67NSArray *SOGoMailCoreInfoKeys = nil;
68static NSString *mailETag = nil;
69static BOOL heavyDebug         = NO;
70static BOOL debugOn            = NO;
71static BOOL debugBodyStructure = NO;
72static BOOL debugSoParts       = NO;
73
74+ (void) initialize
75{
76  if (!SOGoMailCoreInfoKeys)
77    {
78      /* Note: see SOGoMailManager.m for allowed IMAP4 keys */
79      SOGoMailCoreInfoKeys
80        = [[NSArray alloc] initWithObjects:
81                             @"FLAGS", @"ENVELOPE", @"BODYSTRUCTURE",
82                           @"RFC822.SIZE",
83                           @"RFC822.HEADER",
84                           // not yet supported: @"INTERNALDATE",
85                           nil];
86
87      /* The following disabled code should not be needed, except if we use
88         annotations (see davEntityTag below) */
89      // if (![[ud objectForKey: @"SOGoMailDisableETag"] boolValue]) {
90      mailETag = [[NSString alloc] initWithFormat: @"\"imap4url_%@_%@_%@\"",
91                                   UIX_MAILER_MAJOR_VERSION,
92                                   UIX_MAILER_MINOR_VERSION,
93                                   UIX_MAILER_SUBMINOR_VERSION];
94    }
95}
96
97- (id) init
98{
99  if ((self = [super init]))
100    {
101      headers = nil;
102      headerPart = nil;
103      coreInfos = nil;
104    }
105
106  return self;
107}
108
109- (void) dealloc
110{
111  [headers release];
112  [headerPart release];
113  [coreInfos release];
114  [super dealloc];
115}
116
117/* IMAP4 */
118
119- (NSString *) relativeImap4Name
120{
121  return [nameInContainer stringByDeletingPathExtension];
122}
123
124/* hierarchy */
125
126- (SOGoMailObject *) mailObject
127{
128  return self;
129}
130
131/* part hierarchy */
132
133- (NSString *) keyExtensionForPart: (id) _partInfo
134{
135  NSString *mt, *st;
136
137  if (_partInfo == nil)
138    return nil;
139
140  mt = [_partInfo valueForKey: @"type"];
141  st = [[_partInfo valueForKey: @"subtype"] lowercaseString];
142  if ([mt isEqualToString: @"text"]) {
143    if ([st isEqualToString: @"plain"])    return @".txt";
144    if ([st isEqualToString: @"html"])     return @".html";
145    if ([st isEqualToString: @"calendar"]) return @".ics";
146    if ([st isEqualToString: @"x-vcard"])  return @".vcf";
147  }
148  else if ([mt isEqualToString: @"image"])
149    return [@"." stringByAppendingString:st];
150  else if ([mt isEqualToString: @"application"]) {
151    if ([st isEqualToString: @"pgp-signature"])
152      return @".asc";
153  }
154
155  return nil;
156}
157
158- (NSArray *)relationshipKeysWithParts:(BOOL)_withParts {
159  /* should return non-multipart children */
160  NSMutableArray *ma;
161  NSArray *parts;
162  unsigned i, count;
163
164  parts = [[self bodyStructure] valueForKey: @"parts"];
165  if (![parts isNotNull])
166    return nil;
167  if ((count = [parts count]) == 0)
168    return nil;
169
170  for (i = 0, ma = nil; i < count; i++) {
171    NSString *key, *ext;
172    id   part;
173    BOOL hasParts;
174
175    part     = [parts objectAtIndex:i];
176    hasParts = [part valueForKey: @"parts"] != nil ? YES:NO;
177    if ((hasParts && !_withParts) || (_withParts && !hasParts))
178      continue;
179
180    if (ma == nil)
181      ma = [NSMutableArray arrayWithCapacity:count - i];
182
183    ext = [self keyExtensionForPart:part];
184    key = [[NSString alloc] initWithFormat: @"%d%@", i + 1, ((id)ext?(id)ext: (id)@"")];
185    [ma addObject:key];
186    [key release];
187  }
188  return ma;
189}
190
191- (NSArray *) toOneRelationshipKeys
192{
193  return [self relationshipKeysWithParts:NO];
194}
195
196- (NSArray *) toManyRelationshipKeys
197{
198  return [self relationshipKeysWithParts:YES];
199}
200
201/* message */
202
203- (id) fetchParts: (NSArray *) _parts
204{
205  // TODO: explain what it does
206  /*
207    Called by -fetchPlainTextParts:
208  */
209  return [[self imap4Connection] fetchURL: [self imap4URL] parts:_parts];
210}
211
212/* core infos */
213
214- (BOOL) doesMailExist
215{
216  static NSArray *existsKey = nil;
217  id msgs;
218
219  if (coreInfos != nil) /* if we have coreinfos, we can use them */
220    return [coreInfos isNotNull];
221
222  /* otherwise fetch something really simple */
223
224  if (existsKey == nil) /* we use size, other suggestions? */
225    existsKey = [[NSArray alloc] initWithObjects: @"RFC822.SIZE", nil];
226
227  msgs = [self fetchParts:existsKey]; // returns dict
228  msgs = [msgs valueForKey: @"fetch"];
229  return [msgs count] > 0 ? YES : NO;
230}
231
232- (id) fetchCoreInfos
233{
234  id msgs;
235  int i;
236
237  if (!coreInfos)
238    {
239      msgs = [self fetchParts: SOGoMailCoreInfoKeys]; // returns dict
240      if (heavyDebug)
241	[self logWithFormat: @"M: %@", msgs];
242      msgs = [msgs valueForKey: @"fetch"];
243
244      // We MUST honor untagged IMAP responses here otherwise we could
245      // return really borken and nasty results.
246      if ([msgs count] > 0)
247	{
248	  for (i = 0; i < [msgs count]; i++)
249	    {
250	      coreInfos = [msgs objectAtIndex: i];
251
252	      if ([[coreInfos objectForKey: @"uid"] intValue] == [[self nameInContainer] intValue])
253		break;
254
255	      coreInfos = nil;
256	    }
257	}
258      [coreInfos retain];
259    }
260
261  return coreInfos;
262}
263
264- (void) setCoreInfos: (NSDictionary *) newCoreInfos
265{
266  ASSIGN (coreInfos, newCoreInfos);
267}
268
269- (id) bodyStructure
270{
271  id bodyStructure;
272
273  bodyStructure = [[self fetchCoreInfos] valueForKey: @"bodystructure"];
274  if (debugBodyStructure)
275    [self logWithFormat: @"BODYSTRUCTURE: %@", bodyStructure];
276
277  return bodyStructure;
278}
279
280- (NGImap4Envelope *) envelope
281{
282  return [[self fetchCoreInfos] valueForKey: @"envelope"];
283}
284
285- (NSString *) subject
286{
287  return [[self envelope] subject];
288}
289
290- (NSString *) displayName
291{
292  return [self decodedSubject];
293}
294
295- (NSString *) decodedSubject
296{
297  return [[self subject] decodedHeader];
298}
299
300- (NSCalendarDate *) date
301{
302  SOGoUserDefaults *ud;
303  NSCalendarDate *date;
304
305  ud = [[context activeUser] userDefaults];
306  date = [[self envelope] date];
307  [date setTimeZone: [ud timeZone]];
308
309  return date;
310}
311
312- (NSArray *) fromEnvelopeAddresses
313{
314  return [[self envelope] from];
315}
316
317- (NSArray *) toEnvelopeAddresses
318{
319  return [[self envelope] to];
320}
321
322- (NSArray *) ccEnvelopeAddresses
323{
324  return [[self envelope] cc];
325}
326
327- (NSArray *) bccEnvelopeAddresses
328{
329  return [[self envelope] bcc];
330}
331
332- (NSArray *) replyToEnvelopeAddresses
333{
334  return [[self envelope] replyTo];
335}
336
337- (NSData *) mailHeaderData
338{
339  return [[self fetchCoreInfos] valueForKey: @"header"];
340}
341
342- (id) mailHeaderPart
343{
344  NGMimeMessageParser *parser;
345  NSData *data;
346
347  if (headerPart != nil)
348    return [headerPart isNotNull] ? headerPart : nil;
349
350  if ([(data = [self mailHeaderData]) length] == 0)
351    return nil;
352
353  // TODO: do we need to set some delegate method which stops parsing the body?
354  parser = [[NGMimeMessageParser alloc] init];
355  headerPart = [[parser parsePartFromData: data] retain];
356  [parser release]; parser = nil;
357
358  if (headerPart == nil) {
359    headerPart = [[NSNull null] retain];
360    return nil;
361  }
362  return headerPart;
363}
364
365- (NSDictionary *) mailHeaders
366{
367  if (!headers)
368    headers = [[[self mailHeaderPart] headers] copy];
369
370  return headers;
371}
372
373- (id) lookupInfoForBodyPart: (id) _path
374{
375  NSEnumerator *pe;
376  NSString *p;
377  id info;
378
379  if (![_path isNotNull])
380    return nil;
381
382  if ((info = [self bodyStructure]) == nil) {
383    [self errorWithFormat: @"got no body part structure!"];
384    return nil;
385  }
386
387  /* ensure array argument */
388
389  if ([_path isKindOfClass:[NSString class]]) {
390    if ([_path length] == 0 || [_path isEqualToString: @"text"])
391      return info;
392
393    _path = [_path componentsSeparatedByString: @"."];
394  }
395
396  // deal with mails of type text/calendar
397  if ([[[info valueForKey: @"type"] lowercaseString] isEqualToString: @"text"] &&
398      [[[info valueForKey: @"subtype"] lowercaseString] isEqualToString: @"calendar"])
399    return info;
400
401  // deal with mails that contain only an attachment, for example:
402  // application/pkcs7-mime
403  // application/pdf
404  // etc.
405  if ([[[info valueForKey: @"type"] lowercaseString] isEqualToString: @"application"] ||
406      [[[info valueForKey: @"type"] lowercaseString] isEqualToString: @"audio"])
407    return info;
408
409  /*
410     For each path component, eg 1,1,3
411
412     Remember that we need special processing for message/rfc822 which maps the
413     namespace of multiparts directly into the main namespace.
414
415     TODO(hh): no I don't remember, please explain in more detail!
416  */
417  pe = [_path objectEnumerator];
418  while ((p = [pe nextObject]) != nil && [info isNotNull]) {
419    unsigned idx;
420    NSArray  *parts;
421    NSString *mt;
422
423    [self debugWithFormat: @"check PATH: %@", p];
424    idx = [p intValue] - 1;
425
426    parts = [info valueForKey: @"parts"];
427    mt = [[info valueForKey: @"type"] lowercaseString];
428    if ([mt isEqualToString: @"message"]) {
429      /* we have special behaviour for message types */
430      id body;
431
432      if ((body = [info valueForKey: @"body"]) != nil) {
433	mt = [body valueForKey: @"type"];
434	if ([mt isEqualToString: @"multipart"])
435	  parts = [body valueForKey: @"parts"];
436	else
437	  parts = [NSArray arrayWithObject:body];
438      }
439    }
440
441    if (idx >= [parts count]) {
442      [self errorWithFormat:
443	      @"body part index out of bounds(idx=%d vs count=%d): %@",
444              (idx + 1), [parts count], info];
445      return nil;
446    }
447    info = [parts objectAtIndex:idx];
448  }
449  return [info isNotNull] ? info : nil;
450}
451
452/* content */
453
454- (NSData *) content
455{
456  NSData *content;
457  id     result, fullResult;
458
459  // We avoid using RFC822 here as the part name as it'll flag the message as Seen
460  fullResult = [self fetchParts: [NSArray arrayWithObject: @"BODY.PEEK[]"]];
461  if (fullResult == nil)
462    return nil;
463
464  if ([fullResult isKindOfClass: [NSException class]])
465    return fullResult;
466
467  /* extract fetch result */
468
469  result = [fullResult valueForKey: @"fetch"];
470  if (![result isKindOfClass:[NSArray class]]) {
471    [self logWithFormat:
472	    @"ERROR: unexpected IMAP4 result (missing 'fetch'): %@",
473	    fullResult];
474    return [NSException exceptionWithHTTPStatus:500 /* server error */
475			reason: @"unexpected IMAP4 result"];
476  }
477  if ([result count] == 0)
478    return nil;
479
480  result = [result objectAtIndex:0];
481
482  /* extract message */
483
484  if ((content = [[result valueForKey: @"body[]"] valueForKey: @"data"]) == nil) {
485    [self logWithFormat:
486	    @"ERROR: unexpected IMAP4 result (missing 'message'): %@",
487	    result];
488    return [NSException exceptionWithHTTPStatus:500 /* server error */
489			reason: @"unexpected IMAP4 result"];
490  }
491
492  return [[content copy] autorelease];
493}
494
495- (NSString *) davContentType
496{
497  return @"message/rfc822";
498}
499
500- (NSString *) contentAsString
501{
502  id s;
503  NSData *content;
504
505  content = [self content];
506  if (content)
507    {
508      if ([content isKindOfClass: [NSData class]])
509	{
510#warning we ignore the charset here?
511	  s = [[NSString alloc] initWithData: content
512				encoding: NSISOLatin1StringEncoding];
513	  if (s)
514	    [s autorelease];
515	  else
516	    [self logWithFormat:
517		    @"ERROR: could not convert data of length %d to string",
518		  [content length]];
519	}
520      else
521	s = content;
522    }
523  else
524    s = nil;
525
526  return s;
527}
528
529/* This is defined before the public version without parentMimeType
530   argument to be able to call it recursively */
531/* bulk fetching of plain/text content */
532- (void) addRequiredKeysOfStructure: (NSDictionary *) info
533                               path: (NSString *) p
534                            toArray: (NSMutableArray *) keys
535                      acceptedTypes: (NSArray *) types
536                           withPeek: (BOOL) withPeek
537                    parentMultipart: (NSString *) parentMPart
538{
539  /*
540     This is used to collect the set of IMAP4 fetch-keys required to fetch
541     the basic parts of the body structure. That is, to fetch all parts which
542     are displayed 'inline' in a single IMAP4 fetch.
543
544     The method calls itself recursively to walk the body structure.
545  */
546  NSArray *parts;
547  unsigned i, count;
548  NSString *k;
549  id body;
550  NSString *bodyToken, *sp, *mimeType;
551  id childInfo;
552  NSString *multipart;
553
554  bodyToken = (withPeek ? @"body.peek" : @"body");
555
556  mimeType = [[NSString stringWithFormat: @"%@/%@",
557			[info valueForKey: @"type"],
558			[info valueForKey: @"subtype"]]
559	       lowercaseString];
560
561  if ([[info valueForKey: @"type"] isEqualToString: @"multipart"])
562    multipart = mimeType;
563  else
564    multipart = parentMPart;
565
566  if ([types containsObject: mimeType])
567    {
568      if ([p length] > 0)
569	k = [NSString stringWithFormat: @"%@[%@]", bodyToken, p];
570      else
571	{
572	  /*
573	    for some reason we need to add ".TEXT" for plain text stuff on root
574	    entities?
575	    TODO: check with HTML
576	  */
577	  k = [NSString stringWithFormat: @"%@[text]", bodyToken];
578	}
579      [keys addObject: [NSDictionary dictionaryWithObjectsAndKeys: k, @"key",
580                                     mimeType, @"mimeType",
581                                     multipart, @"multipart", nil]];
582    }
583
584  parts = [info objectForKey: @"parts"];
585  count = [parts count];
586  for (i = 0; i < count; i++)
587    {
588      sp = (([p length] > 0)
589	    ? (id)[p stringByAppendingFormat: @".%d", i + 1]
590	    : (id)[NSString stringWithFormat: @"%d", i + 1]);
591
592      childInfo = [parts objectAtIndex: i];
593
594      [self addRequiredKeysOfStructure: childInfo
595                                  path: sp
596                               toArray: keys
597                         acceptedTypes: types
598                              withPeek: withPeek
599                       parentMultipart: multipart];
600    }
601
602  /* check body */
603  body = [info objectForKey: @"body"];
604  if (body)
605    {
606      /* FIXME: this seems to generate bad mime part keys, which triggers a
607         exceptions such as this:
608
609         ERROR(-[NGImap4Client _processCommandParserException:]): catched
610         IMAP4 parser exception NGImap4ParserException: unsupported fetch key:
611         nil)
612
613         Do we really need to assign p to sp in a multipart body part? Or do
614         we need to do this only when the part in question is the first one in
615         the message? */
616
617      sp = [[body valueForKey: @"type"] lowercaseString];
618      if ([sp isEqualToString: @"multipart"])
619	sp = p;
620      else
621	sp = [p length] > 0 ? (id)[p stringByAppendingString: @".1"] : (id)@"1";
622      [self addRequiredKeysOfStructure: body
623                                  path: sp
624                               toArray: keys
625                         acceptedTypes: types
626                              withPeek: withPeek
627                       parentMultipart: multipart];
628    }
629}
630
631- (void) addRequiredKeysOfStructure: (NSDictionary *) info
632                               path: (NSString *) p
633                            toArray: (NSMutableArray *) keys
634                      acceptedTypes: (NSArray *) types
635                           withPeek: (BOOL) withPeek
636{
637  [self addRequiredKeysOfStructure: (NSDictionary *) info
638                              path: (NSString *) p
639                           toArray: (NSMutableArray *) keys
640                     acceptedTypes: (NSArray *) types
641                          withPeek: (BOOL) withPeek
642                   parentMultipart: @""];
643}
644
645- (NSArray *) plainTextContentFetchKeys
646{
647  /*
648    The name is not 100% correct. The method returns all body structure fetch
649    keys which are marked by the -shouldFetchPartOfType:subtype: method.
650  */
651  NSMutableArray *ma;
652  NSArray *types;
653
654  types = [NSArray arrayWithObjects: @"text/plain", @"text/html",
655		   @"text/calendar", @"application/ics",
656		   @"application/pgp-signature", nil];
657  ma = [NSMutableArray arrayWithCapacity: 4];
658
659  [self addRequiredKeysOfStructure: [self bodyStructure]
660                              path: @""
661                           toArray: ma
662                     acceptedTypes: types
663                          withPeek: YES];
664
665  return ma;
666}
667
668- (NSDictionary *) fetchPlainTextParts: (NSArray *) _fetchKeys
669{
670  // TODO: is the name correct or does it also fetch other parts?
671  NSMutableDictionary *flatContents;
672  unsigned i, count;
673  NSArray *results;
674  id result;
675
676  [self debugWithFormat: @"fetch keys: %@", _fetchKeys];
677
678  result = [self fetchParts: [_fetchKeys objectsForKey: @"key"
679					 notFoundMarker: nil]];
680  result = [result valueForKey: @"RawResponse"]; // hackish
681
682  // Note: -valueForKey: doesn't work!
683  results = [(NGHashMap *)result objectsForKey: @"fetch"];
684  result = [results flattenedDictionaries];
685
686  count        = [_fetchKeys count];
687  flatContents = [NSMutableDictionary dictionaryWithCapacity:count];
688  for (i = 0; i < count; i++) {
689    NSString *key;
690    NSData   *data;
691
692    key  = [[_fetchKeys objectAtIndex:i] objectForKey: @"key"];
693
694    // We'll ask for the body.peek[] but SOPE returns us body[] responses
695    // so the key won't ever be found.
696    if ([key hasPrefix: @"body.peek["])
697      key = [NSString stringWithFormat: @"body[%@", [key substringFromIndex: 10]];
698
699    data = [(NSDictionary *)[(NSDictionary *)result objectForKey:key]
700			    objectForKey: @"data"];
701
702    if (![data isNotNull]) {
703      [self errorWithFormat: @"got no data for key: %@", key];
704      continue;
705    }
706
707    if ([key isEqualToString: @"body[text]"])
708      key = @""; // see key collector for explanation (TODO: where?)
709    else if ([key hasPrefix: @"body["]) {
710      NSRange r;
711
712      key = [key substringFromIndex:5];
713      r   = [key rangeOfString: @"]"];
714      if (r.length > 0)
715	key = [key substringToIndex:r.location];
716    }
717    [flatContents setObject:data forKey:key];
718  }
719  return flatContents;
720}
721
722- (NSDictionary *) fetchPlainTextParts
723{
724  return [self fetchPlainTextParts: [self plainTextContentFetchKeys]];
725}
726
727- (NSString *) _urlToPart: (NSDictionary *) infos
728	       withPrefix: (NSString *) urlPrefix
729{
730  NSDictionary *parameters;
731  NSString *urlToPart, *filename;
732
733  parameters = [infos objectForKey: @"parameterList"];
734  filename = [parameters objectForKey: @"name"];
735  if (!filename)
736    {
737      parameters = [[infos objectForKey: @"disposition"]
738		     objectForKey: @"parameterList"];
739      filename = [parameters objectForKey: @"filename"];
740    }
741
742  if ([filename length])
743    urlToPart = [NSString stringWithFormat: @"%@/%@", urlPrefix, filename];
744  else
745    urlToPart = urlPrefix;
746
747  return urlToPart;
748}
749
750- (void) _feedFileAttachmentIds: (NSMutableDictionary *) attachmentIds
751                      withInfos: (NSDictionary *) infos
752                      andPrefix: (NSString *) prefix
753{
754  NSArray *parts;
755  NSDictionary *currentPart;
756  unsigned int count, max;
757  NSString *url, *cid;
758
759  cid = [infos objectForKey: @"bodyId"];
760  if ([cid length])
761    {
762      url = [self _urlToPart: infos withPrefix: prefix];
763      if (url)
764	[attachmentIds setObject: url forKey: cid];
765    }
766
767  parts = [infos objectForKey: @"parts"];
768  max = [parts count];
769  for (count = 0; count < max; count++)
770    {
771      currentPart = [parts objectAtIndex: count];
772      [self _feedFileAttachmentIds: attachmentIds
773                         withInfos: currentPart
774                         andPrefix: [NSString stringWithFormat: @"%@/%d",
775                                              prefix, count + 1]];
776    }
777}
778
779- (NSDictionary *) fetchFileAttachmentIds
780{
781  NSMutableDictionary *attachmentIds;
782  NSString *prefix;
783
784  attachmentIds = [NSMutableDictionary dictionary];
785
786  [self fetchCoreInfos];
787  prefix = [[self soURL] absoluteString];
788  if ([prefix hasSuffix: @"/"])
789    prefix = [prefix substringToIndex: [prefix length] - 1];
790  [self _feedFileAttachmentIds: attachmentIds
791                     withInfos: [coreInfos objectForKey: @"bodystructure"]
792                     andPrefix: prefix];
793
794  return attachmentIds;
795}
796
797//
798//
799//
800- (void) _fetchFileAttachmentKey: (NSDictionary *) part
801		       intoArray: (NSMutableArray *) keys
802		        withPath: (NSString *) path
803                       andPrefix: (NSString *) prefix
804{
805  NSString *filename, *mimeType, *filenameURL;
806  NSDictionary *currentFile;
807
808  filename = [part filename];
809
810  mimeType = [NSString stringWithFormat: @"%@/%@",
811		       [part objectForKey: @"type"],
812		       [part objectForKey: @"subtype"]];
813
814  if (!filename)
815    {
816      filename = [mimeType asPreferredFilenameUsingPath: path];
817    }
818
819  if (filename)
820    {
821      // We replace any slash by a dash since Apache won't allow encoded slashes by default.
822      // See http://httpd.apache.org/docs/2.2/mod/core.html#allowencodedslashes
823      // See [UIxMailPartViewer _filenameForAttachment:]
824      filenameURL = [[filename stringByReplacingString: @"/" withString: @"-"] stringByEscapingURL];
825      currentFile = [NSDictionary dictionaryWithObjectsAndKeys:
826                                  [filename stringByUnescapingURL], @"filename",
827                                  [mimeType lowercaseString], @"mimetype",
828                                  path, @"path",
829                                  [part objectForKey: @"encoding"], @"encoding",
830                                  [part objectForKey:@ "size"], @"size",
831                                  [part objectForKey: @"bodyId"], @"bodyId",
832                                  [NSString stringWithFormat: @"%@/%@", prefix, filenameURL], @"url",
833                                  [NSString stringWithFormat: @"%@/asAttachment/%@", prefix, filenameURL], @"urlAsAttachment",
834                                  nil];
835      [keys addObject: currentFile];
836    }
837}
838
839//
840//
841//
842- (void) _fetchFileAttachmentKeysInPart: (NSDictionary *) part
843                              intoArray: (NSMutableArray *) keys
844                               withPath: (NSString *) path
845                              andPrefix: (NSString *) prefix
846{
847  NSMutableDictionary *currentPart;
848  NSString *newPath;
849  NSArray *subparts;
850  NSString *type, *subtype;
851  NSUInteger i;
852
853  type = [[part objectForKey: @"type"] lowercaseString];
854  if ([type isEqualToString: @"multipart"])
855    {
856      subparts = [part objectForKey: @"parts"];
857      for (i = 1; i <= [subparts count]; i++)
858	{
859	  currentPart = [subparts objectAtIndex: i-1];
860	  if (path)
861	    newPath = [NSString stringWithFormat: @"%@.%d", path, (int)i];
862	  else
863	    newPath = [NSString stringWithFormat: @"%d", (int)i];
864	  [self _fetchFileAttachmentKeysInPart: currentPart
865                                     intoArray: keys
866                                      withPath: newPath
867                                     andPrefix: [NSString stringWithFormat: @"%@/%i", prefix, (int)i]];
868	}
869    }
870  else
871    {
872      if (!path)
873        {
874          path = @"1";
875
876          // We set the path to 0 in case of a S/MIME mail if not provided.
877          subtype = [[part objectForKey: @"subtype"] lowercaseString];
878          if ([subtype isEqualToString: @"pkcs7-mime"] || [subtype isEqualToString: @"x-pkcs7-mime"])
879             path = @"0";
880        }
881
882      [self _fetchFileAttachmentKey: part
883                          intoArray: keys
884                           withPath: path
885                          andPrefix: prefix];
886    }
887}
888
889//
890//
891//
892#warning we might need to handle parts with a "name" attribute
893- (NSArray *) fetchFileAttachmentKeys
894{
895  NSString *prefix;
896  NSMutableArray *keys;
897
898  prefix = [[self soURL] absoluteString];
899  if ([prefix hasSuffix: @"/"])
900    prefix = [prefix substringToIndex: [prefix length] - 1];
901
902  keys = [NSMutableArray array];
903  [self _fetchFileAttachmentKeysInPart: [self bodyStructure]
904                             intoArray: keys
905                              withPath: nil
906                             andPrefix: prefix];
907
908  return keys;
909}
910
911/**
912 * Returns an array of dictionaries with the following keys:
913 * - encoding
914 * - filename
915 * - mimetype
916 * - path
917 * - size
918 * - url
919 * - urlAsAttachment
920 * - body (NSData)
921 */
922- (NSArray *) fetchFileAttachments
923{
924  unsigned int count, max;
925  NGHashMap *response;
926  NSArray *parts, *paths; //, *bodies;
927  NSData *body;
928  NSDictionary *fetch, *currentInfo, *currentBody;
929  NSMutableArray *attachments;
930  NSMutableDictionary *currentAttachment;
931  NSString *currentPath;
932
933  parts = [self fetchFileAttachmentKeys];
934  max = [parts count];
935  attachments = [NSMutableArray arrayWithCapacity: max];
936  if (max > 0)
937    {
938      paths = [parts keysWithFormat: @"BODY[%{path}]"];
939      response = [[self fetchParts: paths] objectForKey: @"RawResponse"];
940      fetch = [response objectForKey: @"fetch"];
941      for (count = 0; count < max; count++)
942	{
943	  currentInfo = [parts objectAtIndex: count];
944          currentPath = [[paths objectAtIndex: count] lowercaseString];
945          currentBody = [fetch objectForKey: currentPath];
946
947          if (currentBody)
948            {
949              body = [currentBody objectForKey: @"data"];
950              body = [body bodyDataFromEncoding: [currentInfo objectForKey: @"encoding"]];
951            }
952          else
953            body = [NSData data];
954
955          currentAttachment = [NSMutableDictionary dictionaryWithDictionary: currentInfo];
956          [currentAttachment setObject: body forKey: @"body"];
957          [attachments addObject: currentAttachment];
958	}
959    }
960
961  return attachments;
962}
963
964- (WOResponse *) archiveAllFilesinArchiveNamed: (NSString *) archiveName
965{
966#warning duplicated code from [SOGoMailFolder archiveUIDs]
967  NSArray *attachments;
968  NSData *body, *zipContent;
969  NSDictionary *currentAttachment;
970  NSException *error;
971  NSFileManager *fm;
972  NSString *spoolPath, *name, *baseName, *extension, *zipPath, *qpFileName;
973  SOGoMailFolder *folder;
974  WOResponse *response;
975  unsigned int max, count;
976  SOGoZipArchiver *archiver;
977  NSFileHandle *zipFileHandle;;
978
979  if (!archiveName)
980    archiveName = @"attachments.zip";
981
982  folder = [self container];
983  spoolPath = [folder userSpoolFolderPath];
984
985  if (![folder ensureSpoolFolderPath])
986    {
987      [self errorWithFormat: @"spool directory '%@' doesn't exist", spoolPath];
988      error = [NSException exceptionWithHTTPStatus: 500
989                                            reason: @"spool directory does not exist"];
990      return (WOResponse *)error;
991  }
992
993  fm = [NSFileManager defaultManager];
994  zipPath = [NSString stringWithFormat: @"%@/%@", spoolPath, archiveName];
995  archiver = [SOGoZipArchiver archiverAtPath: zipPath];
996  if (archiver == nil) {
997      [self errorWithFormat: @"Failed to create zip archive at %@", spoolPath];
998      error = [NSException exceptionWithHTTPStatus: 500
999                                            reason: @"Internal server error"];
1000      return (WOResponse *)error;
1001  }
1002
1003  // Fetch attachments and write them on disk
1004  attachments = [self fetchFileAttachments];
1005  max = [attachments count];
1006  for (count = 0; count < max; count++)
1007    {
1008      currentAttachment = [attachments objectAtIndex: count];
1009      body = [currentAttachment objectForKey: @"body"];
1010      name = [[currentAttachment objectForKey: @"filename"] asSafeFilename];
1011      [archiver putFileWithName: name andData: body];
1012    }
1013
1014  [archiver close];
1015
1016  response = [context response];
1017
1018  // Check if SOPE has support for serving files directly
1019  if ([response respondsToSelector: @selector(setContentFile:)]) {
1020     zipFileHandle = [NSFileHandle fileHandleForReadingAtPath: zipPath];
1021     [response setContentFile: zipFileHandle];
1022  } else {
1023     zipContent = [[NSData alloc] initWithContentsOfFile:zipPath];
1024     [response setContent:zipContent];
1025     [zipContent release];
1026  }
1027
1028  [fm removeFileAtPath: zipPath handler: nil];
1029
1030  baseName = [archiveName stringByDeletingPathExtension];
1031  extension = [archiveName pathExtension];
1032  if ([extension length] > 0)
1033    extension = [@"." stringByAppendingString: extension];
1034  else
1035    extension = @"";
1036
1037  qpFileName = [NSString stringWithFormat: @"%@%@",
1038                         [baseName asQPSubjectString: @"utf-8"], extension];
1039  [response setHeader: [NSString stringWithFormat: @"application/zip;"
1040                                 @" name=\"%@\"", qpFileName]
1041               forKey: @"content-type"];
1042  [response setHeader: [NSString stringWithFormat: @"attachment; filename=\"%@\"",
1043                                 qpFileName]
1044               forKey: @"Content-Disposition"];
1045
1046  return response;
1047}
1048
1049/* convert parts to strings */
1050- (NSString *) stringForData: (NSData *) _data
1051		    partInfo: (NSDictionary *) _info
1052{
1053  NSString *charset, *s;
1054  NSData *mailData;
1055
1056  if ([_data isNotNull])
1057    {
1058      mailData
1059	= [_data bodyDataFromEncoding: [_info objectForKey: @"encoding"]];
1060
1061      charset = [[_info valueForKey: @"parameterList"] valueForKey: @"charset"];
1062      if (![charset length])
1063	{
1064	  s = nil;
1065	}
1066      else
1067	{
1068	  s = [NSString stringWithData: mailData usingEncodingNamed: charset];
1069	}
1070
1071      // If it has failed, we try at least using UTF-8. Normally, this can NOT fail.
1072      // Unfortunately, it seems to fail under GNUstep so we try latin1 if that's
1073      // the case
1074      if (!s)
1075	s = [[[NSString alloc] initWithData: mailData encoding: NSUTF8StringEncoding] autorelease];
1076
1077      if (!s)
1078	s = [[[NSString alloc] initWithData: mailData encoding: NSISOLatin1StringEncoding] autorelease];
1079    }
1080  else
1081    s = nil;
1082
1083  return s;
1084}
1085
1086- (NSDictionary *) stringifyTextParts: (NSDictionary *) _datas
1087{
1088  NSMutableDictionary *md;
1089  NSDictionary *info;
1090  NSEnumerator *keys;
1091  NSString     *key, *s;
1092
1093  md = [NSMutableDictionary dictionaryWithCapacity:4];
1094  keys = [_datas keyEnumerator];
1095  while ((key = [keys nextObject]))
1096    {
1097      info = [self lookupInfoForBodyPart: key];
1098      s = [self stringForData: [_datas objectForKey:key] partInfo: info];
1099      if (s)
1100	[md setObject: s forKey: key];
1101    }
1102
1103  return md;
1104}
1105
1106- (NSDictionary *) fetchPlainTextStrings: (NSArray *) _fetchKeys
1107{
1108  /*
1109    The fetched parts are NSData objects, this method converts them into
1110    NSString objects based on the information inside the bodystructure.
1111
1112    The fetch-keys are body fetch-keys like: body[text] or body[1.2.3].
1113    The keys in the result dictionary are "" for 'text' and 1.2.3 for parts.
1114  */
1115  NSDictionary *datas;
1116
1117  if ((datas = [self fetchPlainTextParts:_fetchKeys]) == nil)
1118    return nil;
1119  if ([datas isKindOfClass:[NSException class]])
1120    return datas;
1121
1122  return [self stringifyTextParts:datas];
1123}
1124
1125/* flags */
1126
1127- (NSException *) addFlags: (id) _flags
1128{
1129  [coreInfos release];
1130  coreInfos = nil;
1131  return [[self imap4Connection] addFlags:_flags toURL: [self imap4URL]];
1132}
1133
1134- (NSException *) removeFlags: (id) _flags
1135{
1136  [coreInfos release];
1137  coreInfos = nil;
1138  return [[self imap4Connection] removeFlags:_flags toURL: [self imap4URL]];
1139}
1140
1141/* permissions */
1142
1143- (BOOL) isDeletionAllowed
1144{
1145  NSArray *parentAcl;
1146  NSString *login;
1147
1148  login = [[context activeUser] login];
1149  parentAcl = [[self container] aclsForUser: login];
1150
1151  return [parentAcl containsObject: SOGoRole_ObjectEraser];
1152}
1153
1154/* name lookup */
1155
1156- (id) lookupImap4BodyPartKey: (NSString *) _key
1157		    inContext: (id) _ctx
1158{
1159  // TODO: we might want to check for existence prior controller creation
1160  NSDictionary *partDesc;
1161  NSString *mimeType;
1162  NSArray *parts;
1163  Class clazz;
1164
1165  int part;
1166
1167  if ([self isEncrypted])
1168    {
1169      NSData *certificate;
1170
1171      certificate = [[self mailAccountFolder] certificate];
1172
1173      // If we got a user certificate, let's use it. Otherwise we fallback
1174      // to the current parts fetching code.
1175      if (certificate)
1176        {
1177          NGMimeMessage *m;
1178          id part;
1179
1180          m = [[self content] messageFromEncryptedDataAndCertificate: certificate];
1181
1182          part = [[[m body] parts] objectAtIndex: ([_key intValue]-1)];
1183          mimeType = [[part contentType] stringValue];
1184          clazz = [SOGoMailBodyPart bodyPartClassForMimeType: mimeType
1185                                                   inContext: _ctx];
1186          return [clazz objectWithName:_key inContainer: self];
1187        }
1188    }
1189  else if ([self isOpaqueSigned])
1190    {
1191      NGMimeMessage *m;
1192      id part;
1193
1194      m = [[self content] messageFromOpaqueSignedData];
1195
1196      part = [[[m body] parts] objectAtIndex: ([_key intValue]-1)];
1197      mimeType = [[part contentType] stringValue];
1198      clazz = [SOGoMailBodyPart bodyPartClassForMimeType: mimeType
1199                                               inContext: _ctx];
1200      return [clazz objectWithName:_key inContainer: self];
1201    }
1202
1203  parts = [[self bodyStructure] objectForKey: @"parts"];
1204
1205  /* We don't have parts here but we're trying to download the message's
1206     content that could be an image/jpeg, as an example */
1207  if ([parts count] == 0 && ![_key intValue])
1208    {
1209      partDesc = [self bodyStructure];
1210      _key = @"1";
1211    }
1212  else
1213    {
1214      part = [_key intValue] - 1;
1215      if (part > -1 && part < [parts count])
1216	partDesc = [parts objectAtIndex: part];
1217      else
1218	partDesc = nil;
1219    }
1220
1221  if (partDesc)
1222    {
1223      mimeType = [[partDesc keysWithFormat: @"%{type}/%{subtype}"] lowercaseString];
1224      clazz = [SOGoMailBodyPart bodyPartClassForMimeType: mimeType
1225				inContext: _ctx];
1226    }
1227  else
1228    clazz = Nil;
1229
1230  return [clazz objectWithName:_key inContainer: self];
1231}
1232
1233- (id) lookupName: (NSString *) _key
1234	inContext: (id) _ctx
1235	  acquire: (BOOL) _flag
1236{
1237  id obj;
1238
1239  /* first check attributes directly bound to the application */
1240  if ((obj = [super lookupName:_key inContext:_ctx acquire:NO]) != nil)
1241    return obj;
1242
1243  /* lookup body part */
1244
1245  if ([self isBodyPartKey:_key]) {
1246    if ((obj = [self lookupImap4BodyPartKey:_key inContext:_ctx]) != nil) {
1247      if (debugSoParts)
1248	[self logWithFormat: @"mail looked up part %@: %@", _key, obj];
1249      return obj;
1250    }
1251  }
1252  // Handles cases where the email is itself an attachment, so its Content-Type
1253  // is application/*, image/* etc.
1254  else if ([_key isEqualToString: @"asAttachment"] &&
1255           (obj = [self lookupImap4BodyPartKey: @"0" inContext:_ctx]) != nil)
1256    {
1257      [obj setAsAttachment];
1258      return obj;
1259    }
1260
1261  /* return 404 to stop acquisition */
1262  return [NSException exceptionWithHTTPStatus:404 /* Not Found */
1263		      reason: @"Did not find mail method or part-reference!"];
1264}
1265
1266/* WebDAV */
1267
1268- (BOOL) davIsCollection
1269{
1270  /* while a mail has child objects, it should appear as a file in WebDAV */
1271  return NO;
1272}
1273
1274- (NSString *) davContentLength
1275{
1276  return [NSString stringWithFormat: @"%@", [[self fetchCoreInfos] valueForKey: @"size"]];
1277}
1278
1279- (NSDate *) davCreationDate
1280{
1281  // TODO: use INTERNALDATE once NGImap4 supports that
1282  return nil;
1283}
1284
1285- (NSDate *) davLastModified
1286{
1287  return [self davCreationDate];
1288}
1289
1290- (NSException *) davMoveToTargetObject: (id) _target
1291				newName: (NSString *) _name
1292			      inContext: (id)_ctx
1293{
1294  [self logWithFormat: @"TODO: should move mail as '%@' to: %@",
1295	_name, _target];
1296  return [NSException exceptionWithHTTPStatus: 501 /* Not Implemented */
1297		      reason: @"not implemented"];
1298}
1299
1300- (NSException *) davCopyToTargetObject: (id) _target
1301				newName: (NSString *) _name
1302			      inContext: (id)_ctx
1303{
1304  /*
1305     Note: this is special because we create SOGoMailObject's even if they do
1306           not exist (for performance reasons).
1307
1308     Also: we cannot really take a target resource, the ID will be assigned by
1309           the IMAP4 server.
1310	   We even cannot return a 'location' header instead because IMAP4
1311	   doesn't tell us the new ID.
1312  */
1313  NSURL *destImap4URL;
1314  NGImap4ConnectionManager *manager;
1315  NSException *exc;
1316  NSString *password;
1317
1318  destImap4URL = ([_name length] == 0)
1319    ? [[_target container] imap4URL]
1320    : [_target imap4URL];
1321
1322  manager = [self mailManager];
1323  [self imap4URL];
1324  password = [self imap4PasswordRenewed: NO];
1325  if (password)
1326    {
1327      exc = [manager copyMailURL: imap4URL
1328                     toFolderURL: destImap4URL
1329                        password: password];
1330      if (exc)
1331        {
1332          [self
1333            logWithFormat: @"failure. Attempting with renewed imap4 password"];
1334          password = [self imap4PasswordRenewed: YES];
1335          if (password)
1336            exc = [manager copyMailURL: imap4URL
1337                           toFolderURL: destImap4URL
1338                              password: password];
1339        }
1340    }
1341  else
1342    exc = nil;
1343
1344  return exc;
1345}
1346
1347/* actions */
1348
1349- (id) GETAction: (id) _ctx
1350{
1351  NSException *error;
1352  WOResponse  *r;
1353  NSData      *content;
1354
1355  if ((error = [self matchesRequestConditionInContext:_ctx]) != nil) {
1356    /* check whether the mail still exists */
1357    if (![self doesMailExist]) {
1358      return [NSException exceptionWithHTTPStatus:404 /* Not Found */
1359			  reason: @"mail was deleted"];
1360    }
1361    return error; /* return 304 or 416 */
1362  }
1363
1364  content = [self content];
1365  if ([content isKindOfClass:[NSException class]])
1366    return content;
1367  if (content == nil) {
1368    return [NSException exceptionWithHTTPStatus:404 /* Not Found */
1369			reason: @"did not find IMAP4 message"];
1370  }
1371
1372  r = [(WOContext *)_ctx response];
1373  [r setHeader: @"message/rfc822" forKey: @"content-type"];
1374  [r setContent:content];
1375  return r;
1376}
1377
1378/* operations */
1379
1380- (NSException *) copyToFolderNamed: (NSString *) folderName
1381                          inContext: (id)_ctx
1382{
1383  SOGoMailFolder *destFolder;
1384  NSEnumerator *folders;
1385  NSString *currentFolderName, *reason;
1386
1387  // TODO: check for safe HTTP method
1388
1389  destFolder = (SOGoMailFolder *) [self mailAccountsFolder];
1390  folders = [[folderName componentsSeparatedByString: @"/"] objectEnumerator];
1391  currentFolderName = [folders nextObject];
1392  currentFolderName = [folders nextObject];
1393
1394  while (currentFolderName)
1395    {
1396      destFolder = [destFolder lookupName: currentFolderName
1397                               inContext: _ctx
1398                               acquire: NO];
1399      if ([destFolder isKindOfClass: [NSException class]])
1400        return (NSException *) destFolder;
1401      currentFolderName = [folders nextObject];
1402    }
1403
1404  if (!([destFolder isKindOfClass: [SOGoMailFolder class]]
1405        && [destFolder isNotNull]))
1406    {
1407      reason = [NSString stringWithFormat: @"Did not find folder name '%@'!",
1408                         folderName];
1409      return [NSException exceptionWithHTTPStatus:500 /* Server Error */
1410                          reason: reason];
1411    }
1412  [destFolder flushMailCaches];
1413
1414  /* a) copy */
1415
1416  return [self davCopyToTargetObject: destFolder
1417	       newName: @"fakeNewUnusedByIMAP4" /* autoassigned */
1418	       inContext:_ctx];
1419}
1420
1421- (NSException *) moveToFolderNamed: (NSString *) folderName
1422                          inContext: (id)_ctx
1423{
1424  NSException *error;
1425
1426  if (![self copyToFolderNamed: folderName
1427	     inContext: _ctx])
1428    {
1429      /* b) mark deleted */
1430
1431      error = [[self imap4Connection] markURLDeleted: [self imap4URL]];
1432      if (error != nil) return error;
1433
1434      [self flushMailCaches];
1435    }
1436
1437  return nil;
1438}
1439
1440- (NSException *) delete
1441{
1442  /*
1443     Note: delete is different to DELETEAction: for mails! The 'delete' runs
1444           either flags a message as deleted or moves it to the Trash while
1445	   the DELETEAction: really deletes a message (by flagging it as
1446	   deleted _AND_ performing an expunge).
1447  */
1448  // TODO: copy to Trash folder
1449  NSException *error;
1450
1451  // TODO: check for safe HTTP method
1452
1453  error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
1454  return error;
1455}
1456
1457- (id) DELETEAction: (id) _ctx
1458{
1459  NSException *error;
1460
1461  // TODO: ensure safe HTTP method
1462
1463  error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
1464  if (error != nil) return error;
1465
1466  error = [[self imap4Connection] expungeAtURL:[[self container] imap4URL]];
1467  if (error != nil) return error; // TODO: unflag as deleted?
1468
1469  return [NSNumber numberWithBool:YES]; /* delete was successful */
1470}
1471
1472/* some mail classification */
1473
1474- (BOOL) isMailingListMail
1475{
1476  NSDictionary *h;
1477
1478  if ((h = [self mailHeaders]) == nil)
1479    return NO;
1480
1481  return [[h objectForKey: @"list-id"] isNotEmpty];
1482}
1483
1484- (BOOL) isVirusScanned
1485{
1486  NSDictionary *h;
1487
1488  if ((h = [self mailHeaders]) == nil)
1489    return NO;
1490
1491  if (![[h objectForKey: @"x-virus-status"]  isNotEmpty]) return NO;
1492  if (![[h objectForKey: @"x-virus-scanned"] isNotEmpty]) return NO;
1493  return YES;
1494}
1495
1496- (NSString *) scanListHeaderValue: (id) _value
1497		forFieldWithPrefix: (NSString *) _prefix
1498{
1499  /* Note: not very tolerant on embedded commands and <> */
1500  // TODO: does not really belong here, should be a header-field-parser
1501  NSRange r;
1502
1503  if (![_value isNotEmpty])
1504    return nil;
1505
1506  if ([_value isKindOfClass:[NSArray class]]) {
1507    NSEnumerator *e;
1508    id value;
1509
1510    e = [_value objectEnumerator];
1511    while ((value = [e nextObject]) != nil) {
1512      value = [self scanListHeaderValue:value forFieldWithPrefix:_prefix];
1513      if (value != nil) return value;
1514    }
1515    return nil;
1516  }
1517
1518  if (![_value isKindOfClass:[NSString class]])
1519    return nil;
1520
1521  /* check for commas in string values */
1522  r = [_value rangeOfString: @","];
1523  if (r.length > 0) {
1524    return [self scanListHeaderValue:[_value componentsSeparatedByString: @","]
1525		 forFieldWithPrefix:_prefix];
1526  }
1527
1528  /* value qualifies */
1529  if (![(NSString *)_value hasPrefix:_prefix])
1530    return nil;
1531
1532  /* unquote */
1533  if ([_value characterAtIndex:0] == '<') {
1534    r = [_value rangeOfString: @">"];
1535    _value = (r.length == 0)
1536      ? [_value substringFromIndex:1]
1537      : [_value substringWithRange:NSMakeRange(1, r.location - 2)];
1538  }
1539
1540  return _value;
1541}
1542
1543- (NSString *) mailingListArchiveURL
1544{
1545  return [self scanListHeaderValue:
1546		 [[self mailHeaders] objectForKey: @"list-archive"]
1547	       forFieldWithPrefix: @"<http://"];
1548}
1549
1550- (NSString *) mailingListSubscribeURL
1551{
1552  return [self scanListHeaderValue:
1553		 [[self mailHeaders] objectForKey: @"list-subscribe"]
1554	       forFieldWithPrefix: @"<http://"];
1555}
1556
1557- (NSString *) mailingListUnsubscribeURL
1558{
1559  return [self scanListHeaderValue:
1560		 [[self mailHeaders] objectForKey: @"list-unsubscribe"]
1561	       forFieldWithPrefix: @"<http://"];
1562}
1563
1564/* etag support */
1565
1566- (id) davEntityTag
1567{
1568  /*
1569    Note: There is one thing which *can* change for an existing message,
1570          those are the IMAP4 flags (and annotations, which we do not use).
1571	  Since we don't render the flags, it should be OK, if this changes
1572	  we must embed the flagging into the etag.
1573  */
1574  return mailETag;
1575}
1576
1577- (NSArray *) aclsForUser: (NSString *) uid
1578{
1579  return [container aclsForUser: uid];
1580}
1581
1582/* debugging */
1583
1584- (BOOL) isDebuggingEnabled
1585{
1586  return debugOn;
1587}
1588
1589
1590// For DAV PUT
1591- (id) PUTAction: (WOContext *) _ctx
1592{
1593  WORequest *rq;
1594  NSException *error;
1595  WOResponse *response;
1596  SOGoMailFolder *folder;
1597  int imap4id;
1598
1599  error = [self matchesRequestConditionInContext: _ctx];
1600  if (error)
1601    response = (WOResponse *) error;
1602  else
1603    {
1604      rq = [_ctx request];
1605      folder = [self container];
1606
1607      if ([self doesMailExist])
1608        response = [NSException exceptionWithHTTPStatus: 403
1609                                                 reason: @"Can't overwrite messages"];
1610      else
1611        response = [folder appendMessage: [rq content]
1612                                 usingId: &imap4id];
1613    }
1614
1615  return response;
1616}
1617
1618// For DAV REPORT
1619- (id) _fetchProperty: (NSString *) property
1620{
1621  NSArray *parts;
1622  id rc, msgs;
1623
1624  rc = nil;
1625
1626  if (property)
1627    {
1628      parts = [NSArray arrayWithObject: property];
1629
1630      msgs = [self fetchParts: parts];
1631      msgs = [msgs valueForKey: @"fetch"];
1632      if ([msgs count]) {
1633          rc = [msgs objectAtIndex: 0];
1634      }
1635    }
1636
1637  return rc;
1638}
1639
1640- (BOOL) _hasFlag: (NSString *) flag
1641{
1642  BOOL rc;
1643  NSArray *flags;
1644
1645  flags = [[self fetchCoreInfos] objectForKey: @"flags"];
1646  rc = [flags containsObject: flag];
1647
1648  return rc;
1649}
1650
1651- (NSString *) _emailAddressesFrom: (NSArray *) enveloppeAddresses
1652{
1653  NSMutableArray *addresses;
1654  NSString *rc;
1655  NGImap4EnvelopeAddress *address;
1656  NSString *email;
1657  int count, max;
1658
1659  rc = nil;
1660  max = [enveloppeAddresses count];
1661
1662  if (max > 0)
1663    {
1664      addresses = [NSMutableArray array];
1665      for (count = 0; count < max; count++)
1666        {
1667          address = [enveloppeAddresses objectAtIndex: count];
1668          email = [NSString stringWithFormat: @"%@", [address email]];
1669
1670          [addresses addObject: email];
1671        }
1672      rc = [addresses componentsJoinedByString: @", "];
1673    }
1674
1675  return rc;
1676}
1677
1678// Properties
1679
1680//{urn:schemas:httpmail:}
1681
1682// date already exists, but this one is the correct format
1683- (NSString *) davDate
1684{
1685  return [[self date] rfc822DateString];
1686}
1687
1688- (BOOL) hasAttachment
1689{
1690  return ([[self fetchFileAttachmentKeys] count] > 0);
1691}
1692
1693- (BOOL) isNewMail
1694{
1695  return [self _hasFlag: @"recent"];
1696}
1697
1698- (BOOL) read
1699{
1700  return [self _hasFlag: @"seen"];
1701}
1702
1703- (BOOL) flagged
1704{
1705  return [self _hasFlag: @"flagged"];
1706}
1707
1708- (BOOL) replied
1709{
1710  return [self _hasFlag: @"answered"];
1711}
1712
1713- (BOOL) forwarded
1714{
1715  return [self _hasFlag: @"$forwarded"];
1716}
1717
1718- (BOOL) deleted
1719{
1720  return [self _hasFlag: @"deleted"];
1721}
1722
1723- (BOOL) isSigned
1724{
1725  NSString *type, *subtype, *protocol;
1726  NGMimeType *contentType;
1727
1728  contentType = [[self mailHeaders] objectForKey: @"content-type"];
1729  type = [[contentType type] lowercaseString];
1730  subtype = [[contentType subType] lowercaseString];
1731  protocol = [[contentType valueOfParameter: @"protocol"] lowercaseString];
1732
1733  return ([type isEqualToString: @"multipart"] &&
1734          [subtype isEqualToString: @"signed"] &&
1735          ([protocol isEqualToString: @"application/x-pkcs7-signature"] ||
1736           [protocol isEqualToString: @"application/pkcs7-signature"]));
1737}
1738
1739- (BOOL) isOpaqueSigned
1740{
1741  NSString *type, *subtype, *smimetype;
1742  NGMimeType *contentType;
1743
1744  contentType = [[self mailHeaders] objectForKey: @"content-type"];
1745  type = [[contentType type] lowercaseString];
1746  subtype = [[contentType subType] lowercaseString];
1747
1748  if ([type isEqualToString: @"application"])
1749    {
1750      if ([subtype isEqualToString: @"x-pkcs7-mime"] ||
1751          [subtype isEqualToString: @"pkcs7-mime"])
1752        {
1753          smimetype = [[contentType valueOfParameter: @"smime-type"] lowercaseString];
1754          if ([smimetype isEqualToString: @"signed-data"])
1755              return YES;
1756        }
1757    }
1758
1759  return NO;
1760}
1761
1762- (BOOL) isEncrypted
1763{
1764  NSString *type, *subtype, *smimetype;
1765  NGMimeType *contentType;
1766
1767  contentType = [[self mailHeaders] objectForKey: @"content-type"];
1768  type = [[contentType type] lowercaseString];
1769  subtype = [[contentType subType] lowercaseString];
1770
1771  if ([type isEqualToString: @"application"])
1772    {
1773      if ([subtype isEqualToString: @"x-pkcs7-mime"] ||
1774          [subtype isEqualToString: @"pkcs7-mime"])
1775        {
1776          smimetype = [[contentType valueOfParameter: @"smime-type"] lowercaseString];
1777          if ([smimetype isEqualToString: @"enveloped-data"])
1778              return YES;
1779        }
1780    }
1781
1782  return NO;
1783}
1784
1785- (NSString *) textDescription
1786{
1787#warning We should send the content as an NSData
1788  return [NSString stringWithFormat: @"<![CDATA[%@]]>", [self contentAsString]];
1789}
1790
1791
1792//{urn:schemas:mailheader:}
1793
1794- (NSString *) to
1795{
1796  return [self _emailAddressesFrom: [self toEnvelopeAddresses]];
1797}
1798
1799- (NSString *) cc
1800{
1801  return [self _emailAddressesFrom: [self ccEnvelopeAddresses]];
1802}
1803
1804- (NSString *) from
1805{
1806  return [self _emailAddressesFrom: [self fromEnvelopeAddresses]];
1807}
1808
1809- (NSString *) inReplyTo
1810{
1811  return [[self envelope] inReplyTo];
1812}
1813
1814- (NSString *) messageId
1815{
1816  return [[self envelope] messageID];
1817}
1818
1819- (NSString *) received
1820{
1821  NSDictionary *fetch;
1822  NSData *data;
1823  NSString *value, *rc;
1824  NSRange range;
1825
1826  rc = nil;
1827  fetch = [self _fetchProperty: @"BODY.PEEK[HEADER.FIELDS (RECEIVED)]"];
1828
1829  if ([fetch count])
1830    {
1831      data = [fetch objectForKey: @"header"];
1832      value = [[NSString alloc] initWithData: data
1833                                    encoding: NSUTF8StringEncoding];
1834      range = [value rangeOfString: @"received:"
1835                           options: NSCaseInsensitiveSearch
1836                             range: NSMakeRange (10, [value length] - 11)];
1837      if (range.length
1838          && range.location < [value length]
1839          && range.length < [value length])
1840        {
1841          // We want to keep the first part
1842          range.length = range.location;
1843          range.location = 0;
1844          rc = [[value substringWithRange: range] stringByTrimmingSpaces];
1845        }
1846      else
1847        rc = [value stringByTrimmingSpaces];
1848
1849      [value release];
1850    }
1851
1852  return rc;
1853}
1854
1855- (NSString *) references
1856{
1857  NSDictionary *fetch;
1858  NSData *data;
1859  NSString *value, *rc;
1860
1861  rc = nil;
1862  fetch = [self _fetchProperty: @"BODY.PEEK[HEADER.FIELDS (REFERENCES)]"];
1863
1864  if ([fetch count])
1865    {
1866      data = [fetch objectForKey: @"header"];
1867      value = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
1868      if (value && [value length] > 11)
1869        rc = [[value substringFromIndex: 11] stringByTrimmingSpaces];
1870      [value release];
1871    }
1872
1873  return rc;
1874}
1875
1876- (NSString *) davDisplayName
1877{
1878  return [self subject];
1879}
1880
1881@end /* SOGoMailObject */
1882