2 Copyright (C) 2004-2005 SKYRIX Software AG
4 This file is part of OpenGroupware.org.
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
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.
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
22 #import <Foundation/NSArray.h>
23 #import <Foundation/NSCalendarDate.h>
24 #import <Foundation/NSDictionary.h>
25 #import <Foundation/NSEnumerator.h>
26 #import <Foundation/NSString.h>
27 #import <Foundation/NSUserDefaults.h>
28 #import <Foundation/NSValue.h>
30 #import <NGObjWeb/WOContext.h>
31 #import <NGObjWeb/WOContext+SoObjects.h>
32 #import <NGObjWeb/WOResponse.h>
33 #import <NGObjWeb/NSException+HTTP.h>
34 #import <NGExtensions/NSNull+misc.h>
35 #import <NGExtensions/NSObject+Logs.h>
36 #import <NGExtensions/NSString+Encoding.h>
37 #import <NGExtensions/NSString+misc.h>
38 #import <NGImap4/NGImap4Connection.h>
39 #import <NGImap4/NGImap4Envelope.h>
40 #import <NGImap4/NGImap4EnvelopeAddress.h>
41 #import <NGMail/NGMimeMessageParser.h>
43 #import <SoObjects/SOGo/NSArray+Utilities.h>
44 #import <SoObjects/SOGo/SOGoPermissions.h>
45 #import <SoObjects/SOGo/SOGoUser.h>
47 #import "NSData+Mail.h"
48 #import "SOGoMailFolder.h"
49 #import "SOGoMailAccount.h"
50 #import "SOGoMailManager.h"
51 #import "SOGoMailBodyPart.h"
53 #import "SOGoMailObject.h"
55 @implementation SOGoMailObject
57 static NSArray *coreInfoKeys = nil;
58 static NSString *mailETag = nil;
59 static BOOL heavyDebug = NO;
60 static BOOL fetchHeader = YES;
61 static BOOL debugOn = NO;
62 static BOOL debugBodyStructure = NO;
63 static BOOL debugSoParts = NO;
67 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
69 if ((fetchHeader = ([ud boolForKey: @"SOGoDoNotFetchMailHeader"] ? NO : YES)))
70 NSLog(@"Note: fetching full mail header.");
72 NSLog(@"Note: not fetching full mail header: 'SOGoDoNotFetchMailHeader'");
74 /* Note: see SOGoMailManager.m for allowed IMAP4 keys */
75 /* Note: "BODY" actually returns the structure! */
77 coreInfoKeys = [[NSArray alloc] initWithObjects:
78 @"FLAGS", @"ENVELOPE", @"BODYSTRUCTURE",
81 // not yet supported: @"INTERNALDATE",
85 coreInfoKeys = [[NSArray alloc] initWithObjects:
86 @"FLAGS", @"ENVELOPE", @"BODYSTRUCTURE",
88 // not yet supported: @"INTERNALDATE",
92 if (![[ud objectForKey: @"SOGoMailDisableETag"] boolValue]) {
93 mailETag = [[NSString alloc] initWithFormat: @"\"imap4url_%d_%d_%03d\"",
94 UIX_MAILER_MAJOR_VERSION,
95 UIX_MAILER_MINOR_VERSION,
96 UIX_MAILER_SUBMINOR_VERSION];
97 NSLog(@"Note(SOGoMailObject): using constant etag for mail parts: '%@'",
101 NSLog(@"Note(SOGoMailObject): etag caching disabled!");
107 [headerPart release];
114 - (NSString *) relativeImap4Name
116 return [nameInContainer stringByDeletingPathExtension];
121 - (SOGoMailObject *)mailObject {
127 - (NSString *) keyExtensionForPart: (id) _partInfo
131 if (_partInfo == nil)
134 mt = [_partInfo valueForKey: @"type"];
135 st = [[_partInfo valueForKey: @"subtype"] lowercaseString];
136 if ([mt isEqualToString: @"text"]) {
137 if ([st isEqualToString: @"plain"]) return @".txt";
138 if ([st isEqualToString: @"html"]) return @".html";
139 if ([st isEqualToString: @"calendar"]) return @".ics";
140 if ([st isEqualToString: @"x-vcard"]) return @".vcf";
142 else if ([mt isEqualToString: @"image"])
143 return [@"." stringByAppendingString:st];
144 else if ([mt isEqualToString: @"application"]) {
145 if ([st isEqualToString: @"pgp-signature"])
152 - (NSArray *)relationshipKeysWithParts:(BOOL)_withParts {
153 /* should return non-multipart children */
158 parts = [[self bodyStructure] valueForKey: @"parts"];
159 if (![parts isNotNull])
161 if ((count = [parts count]) == 0)
164 for (i = 0, ma = nil; i < count; i++) {
169 part = [parts objectAtIndex:i];
170 hasParts = [part valueForKey: @"parts"] != nil ? YES:NO;
171 if ((hasParts && !_withParts) || (_withParts && !hasParts))
175 ma = [NSMutableArray arrayWithCapacity:count - i];
177 ext = [self keyExtensionForPart:part];
178 key = [[NSString alloc] initWithFormat: @"%d%@", i + 1, ext?ext: @""];
185 - (NSArray *) toOneRelationshipKeys
187 return [self relationshipKeysWithParts:NO];
190 - (NSArray *) toManyRelationshipKeys
192 return [self relationshipKeysWithParts:YES];
197 - (id) fetchParts: (NSArray *) _parts
199 // TODO: explain what it does
201 Called by -fetchPlainTextParts:
203 return [[self imap4Connection] fetchURL: [self imap4URL] parts:_parts];
208 - (BOOL)doesMailExist {
209 static NSArray *existsKey = nil;
212 if (coreInfos != nil) /* if we have coreinfos, we can use them */
213 return [coreInfos isNotNull];
215 /* otherwise fetch something really simple */
217 if (existsKey == nil) /* we use size, other suggestions? */
218 existsKey = [[NSArray alloc] initWithObjects: @"RFC822.SIZE", nil];
220 msgs = [self fetchParts:existsKey]; // returns dict
221 msgs = [msgs valueForKey: @"fetch"];
222 return [msgs count] > 0 ? YES : NO;
225 - (id) fetchCoreInfos
231 msgs = [self fetchParts: coreInfoKeys]; // returns dict
233 [self logWithFormat: @"M: %@", msgs];
234 msgs = [msgs valueForKey: @"fetch"];
235 if ([msgs count] > 0)
236 coreInfos = [msgs objectAtIndex: 0];
247 body = [[self fetchCoreInfos] valueForKey: @"body"];
248 if (debugBodyStructure)
249 [self logWithFormat: @"BODY: %@", body];
254 - (NGImap4Envelope *) envelope
256 return [[self fetchCoreInfos] valueForKey: @"envelope"];
259 - (NSString *) subject
261 return [[self envelope] subject];
264 - (NSCalendarDate *) date
267 NSCalendarDate *date;
269 userTZ = [[context activeUser] timeZone];
270 date = [[self envelope] date];
271 [date setTimeZone: userTZ];
276 - (NSArray *) fromEnvelopeAddresses
278 return [[self envelope] from];
281 - (NSArray *) toEnvelopeAddresses
283 return [[self envelope] to];
286 - (NSArray *) ccEnvelopeAddresses
288 return [[self envelope] cc];
291 - (NSData *) mailHeaderData
293 return [[self fetchCoreInfos] valueForKey: @"header"];
296 - (BOOL) hasMailHeaderInCoreInfos
298 return [[self mailHeaderData] length] > 0 ? YES : NO;
301 - (id) mailHeaderPart
303 NGMimeMessageParser *parser;
306 if (headerPart != nil)
307 return [headerPart isNotNull] ? headerPart : nil;
309 if ([(data = [self mailHeaderData]) length] == 0)
312 // TODO: do we need to set some delegate method which stops parsing the body?
313 parser = [[NGMimeMessageParser alloc] init];
314 headerPart = [[parser parsePartFromData:data] retain];
315 [parser release]; parser = nil;
317 if (headerPart == nil) {
318 headerPart = [[NSNull null] retain];
324 - (NSDictionary *) mailHeaders
327 headers = [[[self mailHeaderPart] headers] copy];
332 - (id) lookupInfoForBodyPart: (id) _path
338 if (![_path isNotNull])
341 if ((info = [self bodyStructure]) == nil) {
342 [self errorWithFormat: @"got no body part structure!"];
346 /* ensure array argument */
348 if ([_path isKindOfClass:[NSString class]]) {
349 if ([_path length] == 0)
352 _path = [_path componentsSeparatedByString: @"."];
356 For each path component, eg 1,1,3
358 Remember that we need special processing for message/rfc822 which maps the
359 namespace of multiparts directly into the main namespace.
361 TODO(hh): no I don't remember, please explain in more detail!
363 pe = [_path objectEnumerator];
364 while ((p = [pe nextObject]) != nil && [info isNotNull]) {
369 [self debugWithFormat: @"check PATH: %@", p];
370 idx = [p intValue] - 1;
372 parts = [info valueForKey: @"parts"];
373 mt = [[info valueForKey: @"type"] lowercaseString];
374 if ([mt isEqualToString: @"message"]) {
375 /* we have special behaviour for message types */
378 if ((body = [info valueForKey: @"body"]) != nil) {
379 mt = [body valueForKey: @"type"];
380 if ([mt isEqualToString: @"multipart"])
381 parts = [body valueForKey: @"parts"];
383 parts = [NSArray arrayWithObject:body];
387 if (idx >= [parts count]) {
388 [self errorWithFormat:
389 @"body part index out of bounds(idx=%d vs count=%d): %@",
390 (idx + 1), [parts count], info];
393 info = [parts objectAtIndex:idx];
395 return [info isNotNull] ? info : nil;
403 id result, fullResult;
405 fullResult = [self fetchParts: [NSArray arrayWithObject: @"RFC822"]];
406 if (fullResult == nil)
409 if ([fullResult isKindOfClass: [NSException class]])
412 /* extract fetch result */
414 result = [fullResult valueForKey: @"fetch"];
415 if (![result isKindOfClass:[NSArray class]]) {
417 @"ERROR: unexpected IMAP4 result (missing 'fetch'): %@",
419 return [NSException exceptionWithHTTPStatus:500 /* server error */
420 reason: @"unexpected IMAP4 result"];
422 if ([result count] == 0)
425 result = [result objectAtIndex:0];
427 /* extract message */
429 if ((content = [result valueForKey: @"message"]) == nil) {
431 @"ERROR: unexpected IMAP4 result (missing 'message'): %@",
433 return [NSException exceptionWithHTTPStatus:500 /* server error */
434 reason: @"unexpected IMAP4 result"];
437 return [[content copy] autorelease];
440 - (NSString *) davContentType
442 return @"message/rfc822";
445 - (NSString *) contentAsString
450 content = [self content];
453 if ([content isKindOfClass: [NSData class]])
455 s = [[NSString alloc] initWithData: content
456 encoding: NSISOLatin1StringEncoding];
461 @"ERROR: could not convert data of length %d to string",
473 /* bulk fetching of plain/text content */
475 // - (BOOL) shouldFetchPartOfType: (NSString *) _type
476 // subtype: (NSString *) _subtype
479 // This method decides which parts are 'prefetched' for display. Those are
480 // usually text parts (the set is currently hardcoded in this method ...).
482 // _type = [_type lowercaseString];
483 // _subtype = [_subtype lowercaseString];
485 // return (([_type isEqualToString: @"text"]
486 // && ([_subtype isEqualToString: @"plain"]
487 // || [_subtype isEqualToString: @"html"]
488 // || [_subtype isEqualToString: @"calendar"]))
489 // || ([_type isEqualToString: @"application"]
490 // && ([_subtype isEqualToString: @"pgp-signature"]
491 // || [_subtype hasPrefix: @"x-vnd.kolab."])));
494 - (void) addRequiredKeysOfStructure: (NSDictionary *) info
496 toArray: (NSMutableArray *) keys
497 acceptedTypes: (NSArray *) types
500 This is used to collect the set of IMAP4 fetch-keys required to fetch
501 the basic parts of the body structure. That is, to fetch all parts which
502 are displayed 'inline' in a single IMAP4 fetch.
504 The method calls itself recursively to walk the body structure.
510 NSString *sp, *mimeType;
513 mimeType = [[NSString stringWithFormat: @"%@/%@",
514 [info valueForKey: @"type"],
515 [info valueForKey: @"subtype"]]
517 if ([types containsObject: mimeType])
520 k = [NSString stringWithFormat: @"body[%@]", p];
524 for some reason we need to add ".TEXT" for plain text stuff on root
526 TODO: check with HTML
530 [keys addObject: [NSDictionary dictionaryWithObjectsAndKeys: k, @"key",
531 mimeType, @"mimeType", nil]];
534 parts = [info objectForKey: @"parts"];
535 count = [parts count];
536 for (i = 0; i < count; i++)
538 sp = (([p length] > 0)
539 ? [p stringByAppendingFormat: @".%d", i + 1]
540 : [NSString stringWithFormat: @"%d", i + 1]);
542 childInfo = [parts objectAtIndex: i];
544 [self addRequiredKeysOfStructure: childInfo
545 path: sp toArray: keys
546 acceptedTypes: types];
550 body = [info objectForKey: @"body"];
553 sp = [[body valueForKey: @"type"] lowercaseString];
554 if ([sp isEqualToString: @"multipart"])
557 sp = [p length] > 0 ? [p stringByAppendingString: @".1"] : @"1";
558 [self addRequiredKeysOfStructure: body
559 path: sp toArray: keys
560 acceptedTypes: types];
564 - (NSArray *) plainTextContentFetchKeys
567 The name is not 100% correct. The method returns all body structure fetch
568 keys which are marked by the -shouldFetchPartOfType:subtype: method.
573 types = [NSArray arrayWithObjects: @"text/plain", @"text/html",
574 @"text/calendar", @"application/pgp-signature", nil];
575 ma = [NSMutableArray arrayWithCapacity: 4];
576 [self addRequiredKeysOfStructure: [self bodyStructure]
577 path: @"" toArray: ma acceptedTypes: types];
582 - (NSDictionary *) fetchPlainTextParts: (NSArray *) _fetchKeys
584 // TODO: is the name correct or does it also fetch other parts?
585 NSMutableDictionary *flatContents;
589 [self debugWithFormat: @"fetch keys: %@", _fetchKeys];
591 result = [self fetchParts: [_fetchKeys objectsForKey: @"key"]];
592 result = [result valueForKey: @"RawResponse"]; // hackish
594 // Note: -valueForKey: doesn't work!
595 result = [(NSDictionary *)result objectForKey: @"fetch"];
597 count = [_fetchKeys count];
598 flatContents = [NSMutableDictionary dictionaryWithCapacity:count];
599 for (i = 0; i < count; i++) {
603 key = [[_fetchKeys objectAtIndex:i] objectForKey: @"key"];
604 data = [(NSDictionary *)[(NSDictionary *)result objectForKey:key]
605 objectForKey: @"data"];
607 if (![data isNotNull]) {
608 [self errorWithFormat: @"got no data for key: %@", key];
612 if ([key isEqualToString: @"body[text]"])
613 key = @""; // see key collector for explanation (TODO: where?)
614 else if ([key hasPrefix: @"body["]) {
617 key = [key substringFromIndex:5];
618 r = [key rangeOfString: @"]"];
620 key = [key substringToIndex:r.location];
622 [flatContents setObject:data forKey:key];
627 - (NSDictionary *) fetchPlainTextParts
629 return [self fetchPlainTextParts: [self plainTextContentFetchKeys]];
632 /* convert parts to strings */
633 - (NSString *) stringForData: (NSData *) _data
634 partInfo: (NSDictionary *) _info
636 NSString *charset, *s;
639 if ([_data isNotNull])
642 = [_data bodyDataFromEncoding: [_info objectForKey: @"encoding"]];
644 charset = [[_info valueForKey: @"parameterList"] valueForKey: @"charset"];
645 if (![charset length])
647 s = [[NSString alloc] initWithData: mailData encoding: NSUTF8StringEncoding];
651 s = [NSString stringWithData: mailData
652 usingEncodingNamed: charset];
660 - (NSDictionary *) stringifyTextParts: (NSDictionary *) _datas
662 NSMutableDictionary *md;
667 md = [NSMutableDictionary dictionaryWithCapacity:4];
668 keys = [_datas keyEnumerator];
669 while ((key = [keys nextObject]))
671 info = [self lookupInfoForBodyPart: key];
672 s = [self stringForData: [_datas objectForKey:key] partInfo: info];
674 [md setObject: s forKey: key];
680 - (NSDictionary *) fetchPlainTextStrings: (NSArray *) _fetchKeys
683 The fetched parts are NSData objects, this method converts them into
684 NSString objects based on the information inside the bodystructure.
686 The fetch-keys are body fetch-keys like: body[text] or body[1.2.3].
687 The keys in the result dictionary are "" for 'text' and 1.2.3 for parts.
691 if ((datas = [self fetchPlainTextParts:_fetchKeys]) == nil)
693 if ([datas isKindOfClass:[NSException class]])
696 return [self stringifyTextParts:datas];
701 - (NSException *) addFlags: (id) _flags
703 return [[self imap4Connection] addFlags:_flags toURL: [self imap4URL]];
706 - (NSException *) removeFlags: (id) _flags
708 return [[self imap4Connection] removeFlags:_flags toURL: [self imap4URL]];
713 - (BOOL) isDeletionAllowed
718 login = [[context activeUser] login];
719 parentAcl = [[self container] aclsForUser: login];
721 return [parentAcl containsObject: SOGoRole_ObjectEraser];
726 - (id) lookupImap4BodyPartKey: (NSString *) _key
729 // TODO: we might want to check for existence prior controller creation
732 clazz = [SOGoMailBodyPart bodyPartClassForKey:_key inContext:_ctx];
734 return [clazz objectWithName:_key inContainer: self];
737 - (id) lookupName: (NSString *) _key
739 acquire: (BOOL) _flag
743 /* first check attributes directly bound to the application */
744 if ((obj = [super lookupName:_key inContext:_ctx acquire:NO]) != nil)
747 /* lookup body part */
749 if ([self isBodyPartKey:_key inContext:_ctx]) {
750 if ((obj = [self lookupImap4BodyPartKey:_key inContext:_ctx]) != nil) {
752 [self logWithFormat: @"mail looked up part %@: %@", _key, obj];
757 /* return 404 to stop acquisition */
758 return [NSException exceptionWithHTTPStatus:404 /* Not Found */
759 reason: @"Did not find mail method or part-reference!"];
764 - (BOOL) davIsCollection
766 /* while a mail has child objects, it should appear as a file in WebDAV */
770 - (id) davContentLength
772 return [[self fetchCoreInfos] valueForKey: @"size"];
775 - (NSDate *) davCreationDate
777 // TODO: use INTERNALDATE once NGImap4 supports that
781 - (NSDate *) davLastModified
783 return [self davCreationDate];
786 - (NSException *) davMoveToTargetObject: (id) _target
787 newName: (NSString *) _name
790 [self logWithFormat: @"TODO: should move mail as '%@' to: %@",
792 return [NSException exceptionWithHTTPStatus: 501 /* Not Implemented */
793 reason: @"not implemented"];
796 - (NSException *) davCopyToTargetObject: (id) _target
797 newName: (NSString *) _name
801 Note: this is special because we create SOGoMailObject's even if they do
802 not exist (for performance reasons).
804 Also: we cannot really take a target resource, the ID will be assigned by
806 We even cannot return a 'location' header instead because IMAP4
807 doesn't tell us the new ID.
811 destImap4URL = ([_name length] == 0)
812 ? [[_target container] imap4URL]
813 : [_target imap4URL];
815 return [[self mailManager] copyMailURL:[self imap4URL]
816 toFolderURL:destImap4URL
817 password:[self imap4Password]];
822 - (id) GETAction: (id) _ctx
828 if ((error = [self matchesRequestConditionInContext:_ctx]) != nil) {
829 /* check whether the mail still exists */
830 if (![self doesMailExist]) {
831 return [NSException exceptionWithHTTPStatus:404 /* Not Found */
832 reason: @"mail was deleted"];
834 return error; /* return 304 or 416 */
837 content = [self content];
838 if ([content isKindOfClass:[NSException class]])
840 if (content == nil) {
841 return [NSException exceptionWithHTTPStatus:404 /* Not Found */
842 reason: @"did not find IMAP4 message"];
845 r = [(WOContext *)_ctx response];
846 [r setHeader: @"message/rfc822" forKey: @"content-type"];
847 [r setContent:content];
853 - (NSException *) trashInContext: (id) _ctx
856 Trashing is three actions:
857 a) copy to trash folder
858 b) mark mail as deleted
861 In case b) or c) fails, we can't do anything because IMAP4 doesn't tell us
862 the ID used in the trash folder.
864 SOGoMailFolder *trashFolder;
867 // TODO: check for safe HTTP method
869 trashFolder = [[self mailAccountFolder] trashFolderInContext:_ctx];
870 if ([trashFolder isKindOfClass:[NSException class]])
871 return (NSException *)trashFolder;
872 if (![trashFolder isNotNull]) {
873 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
874 reason: @"Did not find Trash folder!"];
876 [trashFolder flushMailCaches];
880 error = [self davCopyToTargetObject:trashFolder
881 newName: @"fakeNewUnusedByIMAP4" /* autoassigned */
883 if (error != nil) return error;
885 /* b) mark deleted */
887 error = [[self imap4Connection] markURLDeleted: [self imap4URL]];
888 if (error != nil) return error;
890 [container markForExpunge];
892 [self flushMailCaches];
897 - (NSException *) copyToFolderNamed: (NSString *) folderName
900 SOGoMailAccounts *destFolder;
901 NSEnumerator *folders;
902 NSString *currentFolderName, *reason;
904 // TODO: check for safe HTTP method
906 destFolder = [self mailAccountsFolder];
907 folders = [[folderName componentsSeparatedByString: @"/"] objectEnumerator];
908 currentFolderName = [folders nextObject];
909 currentFolderName = [folders nextObject];
911 while (currentFolderName)
913 destFolder = [destFolder lookupName: currentFolderName
916 if ([destFolder isKindOfClass: [NSException class]])
917 return (NSException *) destFolder;
918 currentFolderName = [folders nextObject];
921 if (!([destFolder isKindOfClass: [SOGoMailFolder class]]
922 && [destFolder isNotNull]))
924 reason = [NSString stringWithFormat: @"Did not find folder name '%@'!",
926 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
929 [destFolder flushMailCaches];
933 return [self davCopyToTargetObject: destFolder
934 newName: @"fakeNewUnusedByIMAP4" /* autoassigned */
938 - (NSException *) moveToFolderNamed: (NSString *) folderName
943 if (![self copyToFolderNamed: folderName
946 /* b) mark deleted */
948 error = [[self imap4Connection] markURLDeleted: [self imap4URL]];
949 if (error != nil) return error;
951 [self flushMailCaches];
957 - (NSException *) delete
960 Note: delete is different to DELETEAction: for mails! The 'delete' runs
961 either flags a message as deleted or moves it to the Trash while
962 the DELETEAction: really deletes a message (by flagging it as
963 deleted _AND_ performing an expunge).
965 // TODO: copy to Trash folder
968 // TODO: check for safe HTTP method
970 error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
974 - (id) DELETEAction: (id) _ctx
978 // TODO: ensure safe HTTP method
980 error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
981 if (error != nil) return error;
983 error = [[self imap4Connection] expungeAtURL:[[self container] imap4URL]];
984 if (error != nil) return error; // TODO: unflag as deleted?
986 return [NSNumber numberWithBool:YES]; /* delete was successful */
989 /* some mail classification */
991 - (BOOL) isKolabObject
995 if ((h = [self mailHeaders]) != nil)
996 return [[h objectForKey: @"x-kolab-type"] isNotEmpty];
998 // TODO: we could check the body structure?
1003 - (BOOL) isMailingListMail
1007 if ((h = [self mailHeaders]) == nil)
1010 return [[h objectForKey: @"list-id"] isNotEmpty];
1013 - (BOOL) isVirusScanned
1017 if ((h = [self mailHeaders]) == nil)
1020 if (![[h objectForKey: @"x-virus-status"] isNotEmpty]) return NO;
1021 if (![[h objectForKey: @"x-virus-scanned"] isNotEmpty]) return NO;
1025 - (NSString *) scanListHeaderValue: (id) _value
1026 forFieldWithPrefix: (NSString *) _prefix
1028 /* Note: not very tolerant on embedded commands and <> */
1029 // TODO: does not really belong here, should be a header-field-parser
1032 if (![_value isNotEmpty])
1035 if ([_value isKindOfClass:[NSArray class]]) {
1039 e = [_value objectEnumerator];
1040 while ((value = [e nextObject]) != nil) {
1041 value = [self scanListHeaderValue:value forFieldWithPrefix:_prefix];
1042 if (value != nil) return value;
1047 if (![_value isKindOfClass:[NSString class]])
1050 /* check for commas in string values */
1051 r = [_value rangeOfString: @","];
1053 return [self scanListHeaderValue:[_value componentsSeparatedByString: @","]
1054 forFieldWithPrefix:_prefix];
1057 /* value qualifies */
1058 if (![(NSString *)_value hasPrefix:_prefix])
1062 if ([_value characterAtIndex:0] == '<') {
1063 r = [_value rangeOfString: @">"];
1064 _value = (r.length == 0)
1065 ? [_value substringFromIndex:1]
1066 : [_value substringWithRange:NSMakeRange(1, r.location - 2)];
1072 - (NSString *) mailingListArchiveURL
1074 return [self scanListHeaderValue:
1075 [[self mailHeaders] objectForKey: @"list-archive"]
1076 forFieldWithPrefix: @"<http://"];
1079 - (NSString *) mailingListSubscribeURL
1081 return [self scanListHeaderValue:
1082 [[self mailHeaders] objectForKey: @"list-subscribe"]
1083 forFieldWithPrefix: @"<http://"];
1086 - (NSString *) mailingListUnsubscribeURL
1088 return [self scanListHeaderValue:
1089 [[self mailHeaders] objectForKey: @"list-unsubscribe"]
1090 forFieldWithPrefix: @"<http://"];
1098 Note: There is one thing which *can* change for an existing message,
1099 those are the IMAP4 flags (and annotations, which we do not use).
1100 Since we don't render the flags, it should be OK, if this changes
1101 we must embed the flagging into the etag.
1106 - (int) zlGenerationCount
1108 return 0; /* mails never change */
1111 /* Outlook mail tagging */
1113 - (NSString *) outlookMessageClass
1117 if ((type = [[self mailHeaders] objectForKey: @"x-kolab-type"]) != nil) {
1118 if ([type isEqualToString: @"application/x-vnd.kolab.contact"])
1119 return @"IPM.Contact";
1120 if ([type isEqualToString: @"application/x-vnd.kolab.task"])
1122 if ([type isEqualToString: @"application/x-vnd.kolab.event"])
1123 return @"IPM.Appointment";
1124 if ([type isEqualToString: @"application/x-vnd.kolab.note"])
1126 if ([type isEqualToString: @"application/x-vnd.kolab.journal"])
1127 return @"IPM.Journal";
1130 return @"IPM.Message"; /* email, default class */
1133 - (NSArray *) aclsForUser: (NSString *) uid
1135 return [container aclsForUser: uid];
1140 - (BOOL) isDebuggingEnabled
1145 @end /* SOGoMailObject */