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 "NSString+Mail.h"
48 #import "NSData+Mail.h"
49 #import "SOGoMailFolder.h"
50 #import "SOGoMailAccount.h"
51 #import "SOGoMailManager.h"
52 #import "SOGoMailBodyPart.h"
54 #import "SOGoMailObject.h"
56 @implementation SOGoMailObject
58 static NSArray *coreInfoKeys = nil;
59 static NSString *mailETag = nil;
60 static BOOL heavyDebug = NO;
61 static BOOL fetchHeader = YES;
62 static BOOL debugOn = NO;
63 static BOOL debugBodyStructure = NO;
64 static BOOL debugSoParts = NO;
68 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
70 if ((fetchHeader = ([ud boolForKey: @"SOGoDoNotFetchMailHeader"] ? NO : YES)))
71 NSLog(@"Note: fetching full mail header.");
73 NSLog(@"Note: not fetching full mail header: 'SOGoDoNotFetchMailHeader'");
75 /* Note: see SOGoMailManager.m for allowed IMAP4 keys */
76 /* Note: "BODY" actually returns the structure! */
78 coreInfoKeys = [[NSArray alloc] initWithObjects:
79 @"FLAGS", @"ENVELOPE", @"BODYSTRUCTURE",
82 // not yet supported: @"INTERNALDATE",
86 coreInfoKeys = [[NSArray alloc] initWithObjects:
87 @"FLAGS", @"ENVELOPE", @"BODYSTRUCTURE",
89 // not yet supported: @"INTERNALDATE",
93 if (![[ud objectForKey: @"SOGoMailDisableETag"] boolValue]) {
94 mailETag = [[NSString alloc] initWithFormat: @"\"imap4url_%d_%d_%03d\"",
95 UIX_MAILER_MAJOR_VERSION,
96 UIX_MAILER_MINOR_VERSION,
97 UIX_MAILER_SUBMINOR_VERSION];
98 NSLog(@"Note(SOGoMailObject): using constant etag for mail parts: '%@'",
102 NSLog(@"Note(SOGoMailObject): etag caching disabled!");
108 [headerPart release];
115 - (NSString *) relativeImap4Name
117 return [nameInContainer stringByDeletingPathExtension];
122 - (SOGoMailObject *)mailObject {
128 - (NSString *) keyExtensionForPart: (id) _partInfo
132 if (_partInfo == nil)
135 mt = [_partInfo valueForKey: @"type"];
136 st = [[_partInfo valueForKey: @"subtype"] lowercaseString];
137 if ([mt isEqualToString: @"text"]) {
138 if ([st isEqualToString: @"plain"]) return @".txt";
139 if ([st isEqualToString: @"html"]) return @".html";
140 if ([st isEqualToString: @"calendar"]) return @".ics";
141 if ([st isEqualToString: @"x-vcard"]) return @".vcf";
143 else if ([mt isEqualToString: @"image"])
144 return [@"." stringByAppendingString:st];
145 else if ([mt isEqualToString: @"application"]) {
146 if ([st isEqualToString: @"pgp-signature"])
153 - (NSArray *)relationshipKeysWithParts:(BOOL)_withParts {
154 /* should return non-multipart children */
159 parts = [[self bodyStructure] valueForKey: @"parts"];
160 if (![parts isNotNull])
162 if ((count = [parts count]) == 0)
165 for (i = 0, ma = nil; i < count; i++) {
170 part = [parts objectAtIndex:i];
171 hasParts = [part valueForKey: @"parts"] != nil ? YES:NO;
172 if ((hasParts && !_withParts) || (_withParts && !hasParts))
176 ma = [NSMutableArray arrayWithCapacity:count - i];
178 ext = [self keyExtensionForPart:part];
179 key = [[NSString alloc] initWithFormat: @"%d%@", i + 1, ext?ext: @""];
186 - (NSArray *) toOneRelationshipKeys
188 return [self relationshipKeysWithParts:NO];
191 - (NSArray *) toManyRelationshipKeys
193 return [self relationshipKeysWithParts:YES];
198 - (id) fetchParts: (NSArray *) _parts
200 // TODO: explain what it does
202 Called by -fetchPlainTextParts:
204 return [[self imap4Connection] fetchURL: [self imap4URL] parts:_parts];
209 - (BOOL)doesMailExist {
210 static NSArray *existsKey = nil;
213 if (coreInfos != nil) /* if we have coreinfos, we can use them */
214 return [coreInfos isNotNull];
216 /* otherwise fetch something really simple */
218 if (existsKey == nil) /* we use size, other suggestions? */
219 existsKey = [[NSArray alloc] initWithObjects: @"RFC822.SIZE", nil];
221 msgs = [self fetchParts:existsKey]; // returns dict
222 msgs = [msgs valueForKey: @"fetch"];
223 return [msgs count] > 0 ? YES : NO;
226 - (id) fetchCoreInfos
232 msgs = [self fetchParts: coreInfoKeys]; // returns dict
234 [self logWithFormat: @"M: %@", msgs];
235 msgs = [msgs valueForKey: @"fetch"];
236 if ([msgs count] > 0)
237 coreInfos = [msgs objectAtIndex: 0];
248 body = [[self fetchCoreInfos] valueForKey: @"body"];
249 if (debugBodyStructure)
250 [self logWithFormat: @"BODY: %@", body];
255 - (NGImap4Envelope *) envelope
257 return [[self fetchCoreInfos] valueForKey: @"envelope"];
260 - (NSString *) subject
262 return [[self envelope] subject];
265 - (NSString *) decodedSubject
267 return [[self subject] decodedSubject];
270 - (NSCalendarDate *) date
273 NSCalendarDate *date;
275 userTZ = [[context activeUser] timeZone];
276 date = [[self envelope] date];
277 [date setTimeZone: userTZ];
282 - (NSArray *) fromEnvelopeAddresses
284 return [[self envelope] from];
287 - (NSArray *) toEnvelopeAddresses
289 return [[self envelope] to];
292 - (NSArray *) ccEnvelopeAddresses
294 return [[self envelope] cc];
297 - (NSData *) mailHeaderData
299 return [[self fetchCoreInfos] valueForKey: @"header"];
302 - (BOOL) hasMailHeaderInCoreInfos
304 return [[self mailHeaderData] length] > 0 ? YES : NO;
307 - (id) mailHeaderPart
309 NGMimeMessageParser *parser;
312 if (headerPart != nil)
313 return [headerPart isNotNull] ? headerPart : nil;
315 if ([(data = [self mailHeaderData]) length] == 0)
318 // TODO: do we need to set some delegate method which stops parsing the body?
319 parser = [[NGMimeMessageParser alloc] init];
320 headerPart = [[parser parsePartFromData:data] retain];
321 [parser release]; parser = nil;
323 if (headerPart == nil) {
324 headerPart = [[NSNull null] retain];
330 - (NSDictionary *) mailHeaders
333 headers = [[[self mailHeaderPart] headers] copy];
338 - (id) lookupInfoForBodyPart: (id) _path
344 if (![_path isNotNull])
347 if ((info = [self bodyStructure]) == nil) {
348 [self errorWithFormat: @"got no body part structure!"];
352 /* ensure array argument */
354 if ([_path isKindOfClass:[NSString class]]) {
355 if ([_path length] == 0)
358 _path = [_path componentsSeparatedByString: @"."];
362 For each path component, eg 1,1,3
364 Remember that we need special processing for message/rfc822 which maps the
365 namespace of multiparts directly into the main namespace.
367 TODO(hh): no I don't remember, please explain in more detail!
369 pe = [_path objectEnumerator];
370 while ((p = [pe nextObject]) != nil && [info isNotNull]) {
375 [self debugWithFormat: @"check PATH: %@", p];
376 idx = [p intValue] - 1;
378 parts = [info valueForKey: @"parts"];
379 mt = [[info valueForKey: @"type"] lowercaseString];
380 if ([mt isEqualToString: @"message"]) {
381 /* we have special behaviour for message types */
384 if ((body = [info valueForKey: @"body"]) != nil) {
385 mt = [body valueForKey: @"type"];
386 if ([mt isEqualToString: @"multipart"])
387 parts = [body valueForKey: @"parts"];
389 parts = [NSArray arrayWithObject:body];
393 if (idx >= [parts count]) {
394 [self errorWithFormat:
395 @"body part index out of bounds(idx=%d vs count=%d): %@",
396 (idx + 1), [parts count], info];
399 info = [parts objectAtIndex:idx];
401 return [info isNotNull] ? info : nil;
409 id result, fullResult;
411 fullResult = [self fetchParts: [NSArray arrayWithObject: @"RFC822"]];
412 if (fullResult == nil)
415 if ([fullResult isKindOfClass: [NSException class]])
418 /* extract fetch result */
420 result = [fullResult valueForKey: @"fetch"];
421 if (![result isKindOfClass:[NSArray class]]) {
423 @"ERROR: unexpected IMAP4 result (missing 'fetch'): %@",
425 return [NSException exceptionWithHTTPStatus:500 /* server error */
426 reason: @"unexpected IMAP4 result"];
428 if ([result count] == 0)
431 result = [result objectAtIndex:0];
433 /* extract message */
435 if ((content = [result valueForKey: @"message"]) == nil) {
437 @"ERROR: unexpected IMAP4 result (missing 'message'): %@",
439 return [NSException exceptionWithHTTPStatus:500 /* server error */
440 reason: @"unexpected IMAP4 result"];
443 return [[content copy] autorelease];
446 - (NSString *) davContentType
448 return @"message/rfc822";
451 - (NSString *) contentAsString
456 content = [self content];
459 if ([content isKindOfClass: [NSData class]])
461 s = [[NSString alloc] initWithData: content
462 encoding: NSISOLatin1StringEncoding];
467 @"ERROR: could not convert data of length %d to string",
479 /* bulk fetching of plain/text content */
481 // - (BOOL) shouldFetchPartOfType: (NSString *) _type
482 // subtype: (NSString *) _subtype
485 // This method decides which parts are 'prefetched' for display. Those are
486 // usually text parts (the set is currently hardcoded in this method ...).
488 // _type = [_type lowercaseString];
489 // _subtype = [_subtype lowercaseString];
491 // return (([_type isEqualToString: @"text"]
492 // && ([_subtype isEqualToString: @"plain"]
493 // || [_subtype isEqualToString: @"html"]
494 // || [_subtype isEqualToString: @"calendar"]))
495 // || ([_type isEqualToString: @"application"]
496 // && ([_subtype isEqualToString: @"pgp-signature"]
497 // || [_subtype hasPrefix: @"x-vnd.kolab."])));
500 - (void) addRequiredKeysOfStructure: (NSDictionary *) info
502 toArray: (NSMutableArray *) keys
503 acceptedTypes: (NSArray *) types
506 This is used to collect the set of IMAP4 fetch-keys required to fetch
507 the basic parts of the body structure. That is, to fetch all parts which
508 are displayed 'inline' in a single IMAP4 fetch.
510 The method calls itself recursively to walk the body structure.
516 NSString *sp, *mimeType;
519 mimeType = [[NSString stringWithFormat: @"%@/%@",
520 [info valueForKey: @"type"],
521 [info valueForKey: @"subtype"]]
523 if ([types containsObject: mimeType])
526 k = [NSString stringWithFormat: @"body[%@]", p];
530 for some reason we need to add ".TEXT" for plain text stuff on root
532 TODO: check with HTML
536 [keys addObject: [NSDictionary dictionaryWithObjectsAndKeys: k, @"key",
537 mimeType, @"mimeType", nil]];
540 parts = [info objectForKey: @"parts"];
541 count = [parts count];
542 for (i = 0; i < count; i++)
544 sp = (([p length] > 0)
545 ? [p stringByAppendingFormat: @".%d", i + 1]
546 : [NSString stringWithFormat: @"%d", i + 1]);
548 childInfo = [parts objectAtIndex: i];
550 [self addRequiredKeysOfStructure: childInfo
551 path: sp toArray: keys
552 acceptedTypes: types];
556 body = [info objectForKey: @"body"];
559 sp = [[body valueForKey: @"type"] lowercaseString];
560 if ([sp isEqualToString: @"multipart"])
563 sp = [p length] > 0 ? [p stringByAppendingString: @".1"] : @"1";
564 [self addRequiredKeysOfStructure: body
565 path: sp toArray: keys
566 acceptedTypes: types];
570 - (NSArray *) plainTextContentFetchKeys
573 The name is not 100% correct. The method returns all body structure fetch
574 keys which are marked by the -shouldFetchPartOfType:subtype: method.
579 types = [NSArray arrayWithObjects: @"text/plain", @"text/html",
580 @"text/calendar", @"application/pgp-signature", nil];
581 ma = [NSMutableArray arrayWithCapacity: 4];
582 [self addRequiredKeysOfStructure: [self bodyStructure]
583 path: @"" toArray: ma acceptedTypes: types];
588 - (NSDictionary *) fetchPlainTextParts: (NSArray *) _fetchKeys
590 // TODO: is the name correct or does it also fetch other parts?
591 NSMutableDictionary *flatContents;
595 [self debugWithFormat: @"fetch keys: %@", _fetchKeys];
597 result = [self fetchParts: [_fetchKeys objectsForKey: @"key"]];
598 result = [result valueForKey: @"RawResponse"]; // hackish
600 // Note: -valueForKey: doesn't work!
601 result = [(NSDictionary *)result objectForKey: @"fetch"];
603 count = [_fetchKeys count];
604 flatContents = [NSMutableDictionary dictionaryWithCapacity:count];
605 for (i = 0; i < count; i++) {
609 key = [[_fetchKeys objectAtIndex:i] objectForKey: @"key"];
610 data = [(NSDictionary *)[(NSDictionary *)result objectForKey:key]
611 objectForKey: @"data"];
613 if (![data isNotNull]) {
614 [self errorWithFormat: @"got no data for key: %@", key];
618 if ([key isEqualToString: @"body[text]"])
619 key = @""; // see key collector for explanation (TODO: where?)
620 else if ([key hasPrefix: @"body["]) {
623 key = [key substringFromIndex:5];
624 r = [key rangeOfString: @"]"];
626 key = [key substringToIndex:r.location];
628 [flatContents setObject:data forKey:key];
633 - (NSDictionary *) fetchPlainTextParts
635 return [self fetchPlainTextParts: [self plainTextContentFetchKeys]];
638 /* convert parts to strings */
639 - (NSString *) stringForData: (NSData *) _data
640 partInfo: (NSDictionary *) _info
642 NSString *charset, *s;
645 if ([_data isNotNull])
648 = [_data bodyDataFromEncoding: [_info objectForKey: @"encoding"]];
650 charset = [[_info valueForKey: @"parameterList"] valueForKey: @"charset"];
651 if (![charset length])
653 s = [[NSString alloc] initWithData: mailData encoding: NSUTF8StringEncoding];
657 s = [NSString stringWithData: mailData
658 usingEncodingNamed: charset];
666 - (NSDictionary *) stringifyTextParts: (NSDictionary *) _datas
668 NSMutableDictionary *md;
673 md = [NSMutableDictionary dictionaryWithCapacity:4];
674 keys = [_datas keyEnumerator];
675 while ((key = [keys nextObject]))
677 info = [self lookupInfoForBodyPart: key];
678 s = [self stringForData: [_datas objectForKey:key] partInfo: info];
680 [md setObject: s forKey: key];
686 - (NSDictionary *) fetchPlainTextStrings: (NSArray *) _fetchKeys
689 The fetched parts are NSData objects, this method converts them into
690 NSString objects based on the information inside the bodystructure.
692 The fetch-keys are body fetch-keys like: body[text] or body[1.2.3].
693 The keys in the result dictionary are "" for 'text' and 1.2.3 for parts.
697 if ((datas = [self fetchPlainTextParts:_fetchKeys]) == nil)
699 if ([datas isKindOfClass:[NSException class]])
702 return [self stringifyTextParts:datas];
707 - (NSException *) addFlags: (id) _flags
709 return [[self imap4Connection] addFlags:_flags toURL: [self imap4URL]];
712 - (NSException *) removeFlags: (id) _flags
714 return [[self imap4Connection] removeFlags:_flags toURL: [self imap4URL]];
719 - (BOOL) isDeletionAllowed
724 login = [[context activeUser] login];
725 parentAcl = [[self container] aclsForUser: login];
727 return [parentAcl containsObject: SOGoRole_ObjectEraser];
732 - (id) lookupImap4BodyPartKey: (NSString *) _key
735 // TODO: we might want to check for existence prior controller creation
738 clazz = [SOGoMailBodyPart bodyPartClassForKey:_key inContext:_ctx];
740 return [clazz objectWithName:_key inContainer: self];
743 - (id) lookupName: (NSString *) _key
745 acquire: (BOOL) _flag
749 /* first check attributes directly bound to the application */
750 if ((obj = [super lookupName:_key inContext:_ctx acquire:NO]) != nil)
753 /* lookup body part */
755 if ([self isBodyPartKey:_key inContext:_ctx]) {
756 if ((obj = [self lookupImap4BodyPartKey:_key inContext:_ctx]) != nil) {
758 [self logWithFormat: @"mail looked up part %@: %@", _key, obj];
763 /* return 404 to stop acquisition */
764 return [NSException exceptionWithHTTPStatus:404 /* Not Found */
765 reason: @"Did not find mail method or part-reference!"];
770 - (BOOL) davIsCollection
772 /* while a mail has child objects, it should appear as a file in WebDAV */
776 - (id) davContentLength
778 return [[self fetchCoreInfos] valueForKey: @"size"];
781 - (NSDate *) davCreationDate
783 // TODO: use INTERNALDATE once NGImap4 supports that
787 - (NSDate *) davLastModified
789 return [self davCreationDate];
792 - (NSException *) davMoveToTargetObject: (id) _target
793 newName: (NSString *) _name
796 [self logWithFormat: @"TODO: should move mail as '%@' to: %@",
798 return [NSException exceptionWithHTTPStatus: 501 /* Not Implemented */
799 reason: @"not implemented"];
802 - (NSException *) davCopyToTargetObject: (id) _target
803 newName: (NSString *) _name
807 Note: this is special because we create SOGoMailObject's even if they do
808 not exist (for performance reasons).
810 Also: we cannot really take a target resource, the ID will be assigned by
812 We even cannot return a 'location' header instead because IMAP4
813 doesn't tell us the new ID.
817 destImap4URL = ([_name length] == 0)
818 ? [[_target container] imap4URL]
819 : [_target imap4URL];
821 return [[self mailManager] copyMailURL:[self imap4URL]
822 toFolderURL:destImap4URL
823 password:[self imap4Password]];
828 - (id) GETAction: (id) _ctx
834 if ((error = [self matchesRequestConditionInContext:_ctx]) != nil) {
835 /* check whether the mail still exists */
836 if (![self doesMailExist]) {
837 return [NSException exceptionWithHTTPStatus:404 /* Not Found */
838 reason: @"mail was deleted"];
840 return error; /* return 304 or 416 */
843 content = [self content];
844 if ([content isKindOfClass:[NSException class]])
846 if (content == nil) {
847 return [NSException exceptionWithHTTPStatus:404 /* Not Found */
848 reason: @"did not find IMAP4 message"];
851 r = [(WOContext *)_ctx response];
852 [r setHeader: @"message/rfc822" forKey: @"content-type"];
853 [r setContent:content];
859 - (NSException *) trashInContext: (id) _ctx
862 Trashing is three actions:
863 a) copy to trash folder
864 b) mark mail as deleted
867 In case b) or c) fails, we can't do anything because IMAP4 doesn't tell us
868 the ID used in the trash folder.
870 SOGoMailFolder *trashFolder;
873 // TODO: check for safe HTTP method
875 trashFolder = [[self mailAccountFolder] trashFolderInContext:_ctx];
876 if ([trashFolder isKindOfClass:[NSException class]])
877 return (NSException *)trashFolder;
878 if (![trashFolder isNotNull]) {
879 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
880 reason: @"Did not find Trash folder!"];
882 [trashFolder flushMailCaches];
886 error = [self davCopyToTargetObject:trashFolder
887 newName: @"fakeNewUnusedByIMAP4" /* autoassigned */
889 if (error != nil) return error;
891 /* b) mark deleted */
893 error = [[self imap4Connection] markURLDeleted: [self imap4URL]];
894 if (error != nil) return error;
896 [container markForExpunge];
898 [self flushMailCaches];
903 - (NSException *) copyToFolderNamed: (NSString *) folderName
906 SOGoMailAccounts *destFolder;
907 NSEnumerator *folders;
908 NSString *currentFolderName, *reason;
910 // TODO: check for safe HTTP method
912 destFolder = [self mailAccountsFolder];
913 folders = [[folderName componentsSeparatedByString: @"/"] objectEnumerator];
914 currentFolderName = [folders nextObject];
915 currentFolderName = [folders nextObject];
917 while (currentFolderName)
919 destFolder = [destFolder lookupName: currentFolderName
922 if ([destFolder isKindOfClass: [NSException class]])
923 return (NSException *) destFolder;
924 currentFolderName = [folders nextObject];
927 if (!([destFolder isKindOfClass: [SOGoMailFolder class]]
928 && [destFolder isNotNull]))
930 reason = [NSString stringWithFormat: @"Did not find folder name '%@'!",
932 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
935 [destFolder flushMailCaches];
939 return [self davCopyToTargetObject: destFolder
940 newName: @"fakeNewUnusedByIMAP4" /* autoassigned */
944 - (NSException *) moveToFolderNamed: (NSString *) folderName
949 if (![self copyToFolderNamed: folderName
952 /* b) mark deleted */
954 error = [[self imap4Connection] markURLDeleted: [self imap4URL]];
955 if (error != nil) return error;
957 [self flushMailCaches];
963 - (NSException *) delete
966 Note: delete is different to DELETEAction: for mails! The 'delete' runs
967 either flags a message as deleted or moves it to the Trash while
968 the DELETEAction: really deletes a message (by flagging it as
969 deleted _AND_ performing an expunge).
971 // TODO: copy to Trash folder
974 // TODO: check for safe HTTP method
976 error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
980 - (id) DELETEAction: (id) _ctx
984 // TODO: ensure safe HTTP method
986 error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
987 if (error != nil) return error;
989 error = [[self imap4Connection] expungeAtURL:[[self container] imap4URL]];
990 if (error != nil) return error; // TODO: unflag as deleted?
992 return [NSNumber numberWithBool:YES]; /* delete was successful */
995 /* some mail classification */
997 - (BOOL) isKolabObject
1001 if ((h = [self mailHeaders]) != nil)
1002 return [[h objectForKey: @"x-kolab-type"] isNotEmpty];
1004 // TODO: we could check the body structure?
1009 - (BOOL) isMailingListMail
1013 if ((h = [self mailHeaders]) == nil)
1016 return [[h objectForKey: @"list-id"] isNotEmpty];
1019 - (BOOL) isVirusScanned
1023 if ((h = [self mailHeaders]) == nil)
1026 if (![[h objectForKey: @"x-virus-status"] isNotEmpty]) return NO;
1027 if (![[h objectForKey: @"x-virus-scanned"] isNotEmpty]) return NO;
1031 - (NSString *) scanListHeaderValue: (id) _value
1032 forFieldWithPrefix: (NSString *) _prefix
1034 /* Note: not very tolerant on embedded commands and <> */
1035 // TODO: does not really belong here, should be a header-field-parser
1038 if (![_value isNotEmpty])
1041 if ([_value isKindOfClass:[NSArray class]]) {
1045 e = [_value objectEnumerator];
1046 while ((value = [e nextObject]) != nil) {
1047 value = [self scanListHeaderValue:value forFieldWithPrefix:_prefix];
1048 if (value != nil) return value;
1053 if (![_value isKindOfClass:[NSString class]])
1056 /* check for commas in string values */
1057 r = [_value rangeOfString: @","];
1059 return [self scanListHeaderValue:[_value componentsSeparatedByString: @","]
1060 forFieldWithPrefix:_prefix];
1063 /* value qualifies */
1064 if (![(NSString *)_value hasPrefix:_prefix])
1068 if ([_value characterAtIndex:0] == '<') {
1069 r = [_value rangeOfString: @">"];
1070 _value = (r.length == 0)
1071 ? [_value substringFromIndex:1]
1072 : [_value substringWithRange:NSMakeRange(1, r.location - 2)];
1078 - (NSString *) mailingListArchiveURL
1080 return [self scanListHeaderValue:
1081 [[self mailHeaders] objectForKey: @"list-archive"]
1082 forFieldWithPrefix: @"<http://"];
1085 - (NSString *) mailingListSubscribeURL
1087 return [self scanListHeaderValue:
1088 [[self mailHeaders] objectForKey: @"list-subscribe"]
1089 forFieldWithPrefix: @"<http://"];
1092 - (NSString *) mailingListUnsubscribeURL
1094 return [self scanListHeaderValue:
1095 [[self mailHeaders] objectForKey: @"list-unsubscribe"]
1096 forFieldWithPrefix: @"<http://"];
1104 Note: There is one thing which *can* change for an existing message,
1105 those are the IMAP4 flags (and annotations, which we do not use).
1106 Since we don't render the flags, it should be OK, if this changes
1107 we must embed the flagging into the etag.
1112 - (int) zlGenerationCount
1114 return 0; /* mails never change */
1117 /* Outlook mail tagging */
1119 - (NSString *) outlookMessageClass
1123 if ((type = [[self mailHeaders] objectForKey: @"x-kolab-type"]) != nil) {
1124 if ([type isEqualToString: @"application/x-vnd.kolab.contact"])
1125 return @"IPM.Contact";
1126 if ([type isEqualToString: @"application/x-vnd.kolab.task"])
1128 if ([type isEqualToString: @"application/x-vnd.kolab.event"])
1129 return @"IPM.Appointment";
1130 if ([type isEqualToString: @"application/x-vnd.kolab.note"])
1132 if ([type isEqualToString: @"application/x-vnd.kolab.journal"])
1133 return @"IPM.Journal";
1136 return @"IPM.Message"; /* email, default class */
1139 - (NSArray *) aclsForUser: (NSString *) uid
1141 return [container aclsForUser: uid];
1146 - (BOOL) isDebuggingEnabled
1151 @end /* SOGoMailObject */