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/SOGoPermissions.h>
44 #import <SoObjects/SOGo/SOGoUser.h>
46 #import "NSData+Mail.h"
47 #import "SOGoMailFolder.h"
48 #import "SOGoMailAccount.h"
49 #import "SOGoMailManager.h"
50 #import "SOGoMailBodyPart.h"
52 #import "SOGoMailObject.h"
54 @implementation SOGoMailObject
56 static NSArray *coreInfoKeys = nil;
57 static NSString *mailETag = nil;
58 static BOOL heavyDebug = NO;
59 static BOOL fetchHeader = YES;
60 static BOOL debugOn = NO;
61 static BOOL debugBodyStructure = NO;
62 static BOOL debugSoParts = NO;
66 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
68 if ((fetchHeader = ([ud boolForKey: @"SOGoDoNotFetchMailHeader"] ? NO : YES)))
69 NSLog(@"Note: fetching full mail header.");
71 NSLog(@"Note: not fetching full mail header: 'SOGoDoNotFetchMailHeader'");
73 /* Note: see SOGoMailManager.m for allowed IMAP4 keys */
74 /* Note: "BODY" actually returns the structure! */
76 coreInfoKeys = [[NSArray alloc] initWithObjects:
77 @"FLAGS", @"ENVELOPE", @"BODYSTRUCTURE",
80 // not yet supported: @"INTERNALDATE",
84 coreInfoKeys = [[NSArray alloc] initWithObjects:
85 @"FLAGS", @"ENVELOPE", @"BODYSTRUCTURE",
87 // not yet supported: @"INTERNALDATE",
91 if (![[ud objectForKey: @"SOGoMailDisableETag"] boolValue]) {
92 mailETag = [[NSString alloc] initWithFormat: @"\"imap4url_%d_%d_%03d\"",
93 UIX_MAILER_MAJOR_VERSION,
94 UIX_MAILER_MINOR_VERSION,
95 UIX_MAILER_SUBMINOR_VERSION];
96 NSLog(@"Note(SOGoMailObject): using constant etag for mail parts: '%@'",
100 NSLog(@"Note(SOGoMailObject): etag caching disabled!");
106 [headerPart release];
113 - (NSString *) relativeImap4Name
115 return [nameInContainer stringByDeletingPathExtension];
120 - (SOGoMailObject *)mailObject {
126 - (NSString *) keyExtensionForPart: (id) _partInfo
130 if (_partInfo == nil)
133 mt = [_partInfo valueForKey: @"type"];
134 st = [[_partInfo valueForKey: @"subtype"] lowercaseString];
135 if ([mt isEqualToString: @"text"]) {
136 if ([st isEqualToString: @"plain"]) return @".txt";
137 if ([st isEqualToString: @"html"]) return @".html";
138 if ([st isEqualToString: @"calendar"]) return @".ics";
139 if ([st isEqualToString: @"x-vcard"]) return @".vcf";
141 else if ([mt isEqualToString: @"image"])
142 return [@"." stringByAppendingString:st];
143 else if ([mt isEqualToString: @"application"]) {
144 if ([st isEqualToString: @"pgp-signature"])
151 - (NSArray *)relationshipKeysWithParts:(BOOL)_withParts {
152 /* should return non-multipart children */
157 parts = [[self bodyStructure] valueForKey: @"parts"];
158 if (![parts isNotNull])
160 if ((count = [parts count]) == 0)
163 for (i = 0, ma = nil; i < count; i++) {
168 part = [parts objectAtIndex:i];
169 hasParts = [part valueForKey: @"parts"] != nil ? YES:NO;
170 if ((hasParts && !_withParts) || (_withParts && !hasParts))
174 ma = [NSMutableArray arrayWithCapacity:count - i];
176 ext = [self keyExtensionForPart:part];
177 key = [[NSString alloc] initWithFormat: @"%d%@", i + 1, ext?ext: @""];
184 - (NSArray *)toOneRelationshipKeys {
185 return [self relationshipKeysWithParts:NO];
187 - (NSArray *)toManyRelationshipKeys {
188 return [self relationshipKeysWithParts:YES];
193 - (id) fetchParts: (NSArray *) _parts
195 // TODO: explain what it does
197 Called by -fetchPlainTextParts:
199 return [[self imap4Connection] fetchURL: [self imap4URL] parts:_parts];
204 - (BOOL)doesMailExist {
205 static NSArray *existsKey = nil;
208 if (coreInfos != nil) /* if we have coreinfos, we can use them */
209 return [coreInfos isNotNull];
211 /* otherwise fetch something really simple */
213 if (existsKey == nil) /* we use size, other suggestions? */
214 existsKey = [[NSArray alloc] initWithObjects: @"RFC822.SIZE", nil];
216 msgs = [self fetchParts:existsKey]; // returns dict
217 msgs = [msgs valueForKey: @"fetch"];
218 return [msgs count] > 0 ? YES : NO;
221 - (id) fetchCoreInfos
227 msgs = [self fetchParts: coreInfoKeys]; // returns dict
229 [self logWithFormat: @"M: %@", msgs];
230 msgs = [msgs valueForKey: @"fetch"];
231 if ([msgs count] > 0)
232 coreInfos = [msgs objectAtIndex: 0];
243 body = [[self fetchCoreInfos] valueForKey: @"body"];
244 if (debugBodyStructure)
245 [self logWithFormat: @"BODY: %@", body];
250 - (NGImap4Envelope *) envelope
252 return [[self fetchCoreInfos] valueForKey: @"envelope"];
255 - (NSString *) subject
257 return [[self envelope] subject];
260 - (NSCalendarDate *) date
263 NSCalendarDate *date;
265 userTZ = [[context activeUser] timeZone];
266 date = [[self envelope] date];
267 [date setTimeZone: userTZ];
272 - (NSArray *) fromEnvelopeAddresses
274 return [[self envelope] from];
277 - (NSArray *) toEnvelopeAddresses
279 return [[self envelope] to];
282 - (NSArray *) ccEnvelopeAddresses
284 return [[self envelope] cc];
287 - (NSData *) mailHeaderData
289 return [[self fetchCoreInfos] valueForKey: @"header"];
292 - (BOOL) hasMailHeaderInCoreInfos
294 return [[self mailHeaderData] length] > 0 ? YES : NO;
297 - (id) mailHeaderPart
299 NGMimeMessageParser *parser;
302 if (headerPart != nil)
303 return [headerPart isNotNull] ? headerPart : nil;
305 if ([(data = [self mailHeaderData]) length] == 0)
308 // TODO: do we need to set some delegate method which stops parsing the body?
309 parser = [[NGMimeMessageParser alloc] init];
310 headerPart = [[parser parsePartFromData:data] retain];
311 [parser release]; parser = nil;
313 if (headerPart == nil) {
314 headerPart = [[NSNull null] retain];
320 - (NSDictionary *) mailHeaders
323 headers = [[[self mailHeaderPart] headers] copy];
328 - (id) lookupInfoForBodyPart: (id) _path
334 if (![_path isNotNull])
337 if ((info = [self bodyStructure]) == nil) {
338 [self errorWithFormat: @"got no body part structure!"];
342 /* ensure array argument */
344 if ([_path isKindOfClass:[NSString class]]) {
345 if ([_path length] == 0)
348 _path = [_path componentsSeparatedByString: @"."];
352 For each path component, eg 1,1,3
354 Remember that we need special processing for message/rfc822 which maps the
355 namespace of multiparts directly into the main namespace.
357 TODO(hh): no I don't remember, please explain in more detail!
359 pe = [_path objectEnumerator];
360 while ((p = [pe nextObject]) != nil && [info isNotNull]) {
365 [self debugWithFormat: @"check PATH: %@", p];
366 idx = [p intValue] - 1;
368 parts = [info valueForKey: @"parts"];
369 mt = [[info valueForKey: @"type"] lowercaseString];
370 if ([mt isEqualToString: @"message"]) {
371 /* we have special behaviour for message types */
374 if ((body = [info valueForKey: @"body"]) != nil) {
375 mt = [body valueForKey: @"type"];
376 if ([mt isEqualToString: @"multipart"])
377 parts = [body valueForKey: @"parts"];
379 parts = [NSArray arrayWithObject:body];
383 if (idx >= [parts count]) {
384 [self errorWithFormat:
385 @"body part index out of bounds(idx=%d vs count=%d): %@",
386 (idx + 1), [parts count], info];
389 info = [parts objectAtIndex:idx];
391 return [info isNotNull] ? info : nil;
399 id result, fullResult;
401 fullResult = [self fetchParts:[NSArray arrayWithObject: @"RFC822"]];
402 if (fullResult == nil)
405 if ([fullResult isKindOfClass:[NSException class]])
408 /* extract fetch result */
410 result = [fullResult valueForKey: @"fetch"];
411 if (![result isKindOfClass:[NSArray class]]) {
413 @"ERROR: unexpected IMAP4 result (missing 'fetch'): %@",
415 return [NSException exceptionWithHTTPStatus:500 /* server error */
416 reason: @"unexpected IMAP4 result"];
418 if ([result count] == 0)
421 result = [result objectAtIndex:0];
423 /* extract message */
425 if ((content = [result valueForKey: @"message"]) == nil) {
427 @"ERROR: unexpected IMAP4 result (missing 'message'): %@",
429 return [NSException exceptionWithHTTPStatus:500 /* server error */
430 reason: @"unexpected IMAP4 result"];
433 return [[content copy] autorelease];
436 - (NSString *) davContentType
438 return @"message/rfc822";
441 - (NSString *) contentAsString
446 if ((content = [self content]) == nil)
448 if ([content isKindOfClass:[NSException class]])
451 s = [[NSString alloc] initWithData: content
452 encoding: NSISOLatin1StringEncoding];
455 @"ERROR: could not convert data of length %d to string",
459 return [s autorelease];
462 /* bulk fetching of plain/text content */
464 - (BOOL) shouldFetchPartOfType: (NSString *) _type
465 subtype: (NSString *) _subtype
468 This method decides which parts are 'prefetched' for display. Those are
469 usually text parts (the set is currently hardcoded in this method ...).
471 _type = [_type lowercaseString];
472 _subtype = [_subtype lowercaseString];
474 return (([_type isEqualToString: @"text"]
475 && ([_subtype isEqualToString: @"plain"]
476 || [_subtype isEqualToString: @"html"]
477 || [_subtype isEqualToString: @"calendar"]))
478 || ([_type isEqualToString: @"application"]
479 && ([_subtype isEqualToString: @"pgp-signature"]
480 || [_subtype hasPrefix: @"x-vnd.kolab."])));
483 - (void) addRequiredKeysOfStructure: (id) _info
484 path: (NSString *) _p
485 toArray: (NSMutableArray *) _keys
486 recurse: (BOOL) _recurse
489 This is used to collect the set of IMAP4 fetch-keys required to fetch
490 the basic parts of the body structure. That is, to fetch all parts which
491 are displayed 'inline' in a single IMAP4 fetch.
493 The method calls itself recursively to walk the body structure.
503 /* Note: if the part itself doesn't qualify, we still check subparts */
504 fetchPart = [self shouldFetchPartOfType: [_info valueForKey: @"type"]
505 subtype: [_info valueForKey: @"subtype"]];
509 k = [NSString stringWithFormat: @"body[%@]", _p];
513 for some reason we need to add ".TEXT" for plain text stuff on root
515 TODO: check with HTML
519 [_keys addObject: k];
525 parts = [(NSDictionary *)_info objectForKey: @"parts"];
526 count = [parts count];
527 for (i = 0; i < count; i++)
529 sp = (([_p length] > 0)
530 ? [_p stringByAppendingFormat: @".%d", i + 1]
531 : [NSString stringWithFormat: @"%d", i + 1]);
533 childInfo = [parts objectAtIndex: i];
535 [self addRequiredKeysOfStructure: childInfo
536 path: sp toArray: _keys
541 body = [(NSDictionary *)_info objectForKey: @"body"];
544 sp = [[body valueForKey: @"type"] lowercaseString];
545 if ([sp isEqualToString: @"multipart"])
548 sp = [_p length] > 0 ? [_p stringByAppendingString: @".1"] : @"1";
549 [self addRequiredKeysOfStructure: body
550 path: sp toArray: _keys
556 - (NSArray *) plainTextContentFetchKeys
559 The name is not 100% correct. The method returns all body structure fetch
560 keys which are marked by the -shouldFetchPartOfType:subtype: method.
564 ma = [NSMutableArray arrayWithCapacity:4];
565 [self addRequiredKeysOfStructure: [self bodyStructure]
566 path: @"" toArray: ma recurse: YES];
571 - (NSDictionary *) fetchPlainTextParts: (NSArray *) _fetchKeys
573 // TODO: is the name correct or does it also fetch other parts?
574 NSMutableDictionary *flatContents;
578 [self debugWithFormat: @"fetch keys: %@", _fetchKeys];
580 result = [self fetchParts:_fetchKeys];
581 result = [result valueForKey: @"RawResponse"]; // hackish
583 // Note: -valueForKey: doesn't work!
584 result = [(NSDictionary *)result objectForKey: @"fetch"];
586 count = [_fetchKeys count];
587 flatContents = [NSMutableDictionary dictionaryWithCapacity:count];
588 for (i = 0; i < count; i++) {
592 key = [_fetchKeys objectAtIndex:i];
593 data = [(NSDictionary *)[(NSDictionary *)result objectForKey:key]
594 objectForKey: @"data"];
596 if (![data isNotNull]) {
597 [self errorWithFormat: @"got no data for key: %@", key];
601 if ([key isEqualToString: @"body[text]"])
602 key = @""; // see key collector for explanation (TODO: where?)
603 else if ([key hasPrefix: @"body["]) {
606 key = [key substringFromIndex:5];
607 r = [key rangeOfString: @"]"];
609 key = [key substringToIndex:r.location];
611 [flatContents setObject:data forKey:key];
616 - (NSDictionary *) fetchPlainTextParts
618 return [self fetchPlainTextParts:[self plainTextContentFetchKeys]];
621 /* convert parts to strings */
622 - (NSString *) stringForData: (NSData *) _data
623 partInfo: (NSDictionary *) _info
625 NSString *charset, *s;
628 if ([_data isNotNull])
631 = [_data bodyDataFromEncoding: [_info objectForKey: @"encoding"]];
633 charset = [[_info valueForKey: @"parameterList"] valueForKey: @"charset"];
634 if (![charset length])
636 s = [[NSString alloc] initWithData: mailData encoding: NSUTF8StringEncoding];
640 s = [NSString stringWithData: mailData
641 usingEncodingNamed: charset];
649 - (NSDictionary *) stringifyTextParts: (NSDictionary *) _datas
651 NSMutableDictionary *md;
655 md = [NSMutableDictionary dictionaryWithCapacity:4];
656 keys = [_datas keyEnumerator];
657 while ((key = [keys nextObject]) != nil) {
661 info = [self lookupInfoForBodyPart:key];
662 if ((s = [self stringForData:[_datas objectForKey:key] partInfo:info]))
663 [md setObject:s forKey:key];
668 - (NSDictionary *) fetchPlainTextStrings: (NSArray *) _fetchKeys
671 The fetched parts are NSData objects, this method converts them into
672 NSString objects based on the information inside the bodystructure.
674 The fetch-keys are body fetch-keys like: body[text] or body[1.2.3].
675 The keys in the result dictionary are "" for 'text' and 1.2.3 for parts.
679 if ((datas = [self fetchPlainTextParts:_fetchKeys]) == nil)
681 if ([datas isKindOfClass:[NSException class]])
684 return [self stringifyTextParts:datas];
689 - (NSException *) addFlags: (id) _flags
691 return [[self imap4Connection] addFlags:_flags toURL: [self imap4URL]];
694 - (NSException *) removeFlags: (id) _flags
696 return [[self imap4Connection] removeFlags:_flags toURL: [self imap4URL]];
701 - (BOOL) isDeletionAllowed
706 login = [[context activeUser] login];
707 parentAcl = [[self container] aclsForUser: login];
709 return [parentAcl containsObject: SOGoRole_ObjectEraser];
714 - (id) lookupImap4BodyPartKey: (NSString *) _key
717 // TODO: we might want to check for existence prior controller creation
720 clazz = [SOGoMailBodyPart bodyPartClassForKey:_key inContext:_ctx];
722 return [clazz objectWithName:_key inContainer: self];
725 - (id) lookupName: (NSString *) _key
727 acquire: (BOOL) _flag
731 /* first check attributes directly bound to the application */
732 if ((obj = [super lookupName:_key inContext:_ctx acquire:NO]) != nil)
735 /* lookup body part */
737 if ([self isBodyPartKey:_key inContext:_ctx]) {
738 if ((obj = [self lookupImap4BodyPartKey:_key inContext:_ctx]) != nil) {
740 [self logWithFormat: @"mail looked up part %@: %@", _key, obj];
745 /* return 404 to stop acquisition */
746 return [NSException exceptionWithHTTPStatus:404 /* Not Found */
747 reason: @"Did not find mail method or part-reference!"];
752 - (BOOL) davIsCollection
754 /* while a mail has child objects, it should appear as a file in WebDAV */
758 - (id) davContentLength
760 return [[self fetchCoreInfos] valueForKey: @"size"];
763 - (NSDate *) davCreationDate
765 // TODO: use INTERNALDATE once NGImap4 supports that
769 - (NSDate *) davLastModified
771 return [self davCreationDate];
774 - (NSException *) davMoveToTargetObject: (id) _target
775 newName: (NSString *) _name
778 [self logWithFormat: @"TODO: should move mail as '%@' to: %@",
780 return [NSException exceptionWithHTTPStatus: 501 /* Not Implemented */
781 reason: @"not implemented"];
784 - (NSException *) davCopyToTargetObject: (id) _target
785 newName: (NSString *) _name
789 Note: this is special because we create SOGoMailObject's even if they do
790 not exist (for performance reasons).
792 Also: we cannot really take a target resource, the ID will be assigned by
794 We even cannot return a 'location' header instead because IMAP4
795 doesn't tell us the new ID.
799 destImap4URL = ([_name length] == 0)
800 ? [[_target container] imap4URL]
801 : [_target imap4URL];
803 return [[self mailManager] copyMailURL:[self imap4URL]
804 toFolderURL:destImap4URL
805 password:[self imap4Password]];
810 - (id) GETAction: (id) _ctx
816 if ((error = [self matchesRequestConditionInContext:_ctx]) != nil) {
817 /* check whether the mail still exists */
818 if (![self doesMailExist]) {
819 return [NSException exceptionWithHTTPStatus:404 /* Not Found */
820 reason: @"mail was deleted"];
822 return error; /* return 304 or 416 */
825 content = [self content];
826 if ([content isKindOfClass:[NSException class]])
828 if (content == nil) {
829 return [NSException exceptionWithHTTPStatus:404 /* Not Found */
830 reason: @"did not find IMAP4 message"];
833 r = [(WOContext *)_ctx response];
834 [r setHeader: @"message/rfc822" forKey: @"content-type"];
835 [r setContent:content];
841 - (NSException *) trashInContext: (id) _ctx
844 Trashing is three actions:
845 a) copy to trash folder
846 b) mark mail as deleted
849 In case b) or c) fails, we can't do anything because IMAP4 doesn't tell us
850 the ID used in the trash folder.
852 SOGoMailFolder *trashFolder;
855 // TODO: check for safe HTTP method
857 trashFolder = [[self mailAccountFolder] trashFolderInContext:_ctx];
858 if ([trashFolder isKindOfClass:[NSException class]])
859 return (NSException *)trashFolder;
860 if (![trashFolder isNotNull]) {
861 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
862 reason: @"Did not find Trash folder!"];
864 [trashFolder flushMailCaches];
868 error = [self davCopyToTargetObject:trashFolder
869 newName: @"fakeNewUnusedByIMAP4" /* autoassigned */
871 if (error != nil) return error;
873 /* b) mark deleted */
875 error = [[self imap4Connection] markURLDeleted: [self imap4URL]];
876 if (error != nil) return error;
878 [container markForExpunge];
880 [self flushMailCaches];
885 - (NSException *) copyToFolderNamed: (NSString *) folderName
888 SOGoMailAccounts *destFolder;
889 NSEnumerator *folders;
890 NSString *currentFolderName, *reason;
892 // TODO: check for safe HTTP method
894 destFolder = [self mailAccountsFolder];
895 folders = [[folderName componentsSeparatedByString: @"/"] objectEnumerator];
896 currentFolderName = [folders nextObject];
897 currentFolderName = [folders nextObject];
899 while (currentFolderName)
901 destFolder = [destFolder lookupName: currentFolderName
904 if ([destFolder isKindOfClass: [NSException class]])
905 return (NSException *) destFolder;
906 currentFolderName = [folders nextObject];
909 if (!([destFolder isKindOfClass: [SOGoMailFolder class]]
910 && [destFolder isNotNull]))
912 reason = [NSString stringWithFormat: @"Did not find folder name '%@'!",
914 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
917 [destFolder flushMailCaches];
921 return [self davCopyToTargetObject: destFolder
922 newName: @"fakeNewUnusedByIMAP4" /* autoassigned */
926 - (NSException *) moveToFolderNamed: (NSString *) folderName
931 if (![self copyToFolderNamed: folderName
934 /* b) mark deleted */
936 error = [[self imap4Connection] markURLDeleted: [self imap4URL]];
937 if (error != nil) return error;
939 [self flushMailCaches];
945 - (NSException *) delete
948 Note: delete is different to DELETEAction: for mails! The 'delete' runs
949 either flags a message as deleted or moves it to the Trash while
950 the DELETEAction: really deletes a message (by flagging it as
951 deleted _AND_ performing an expunge).
953 // TODO: copy to Trash folder
956 // TODO: check for safe HTTP method
958 error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
962 - (id) DELETEAction: (id) _ctx
966 // TODO: ensure safe HTTP method
968 error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
969 if (error != nil) return error;
971 error = [[self imap4Connection] expungeAtURL:[[self container] imap4URL]];
972 if (error != nil) return error; // TODO: unflag as deleted?
974 return [NSNumber numberWithBool:YES]; /* delete was successful */
977 /* some mail classification */
979 - (BOOL) isKolabObject
983 if ((h = [self mailHeaders]) != nil)
984 return [[h objectForKey: @"x-kolab-type"] isNotEmpty];
986 // TODO: we could check the body structure?
991 - (BOOL) isMailingListMail
995 if ((h = [self mailHeaders]) == nil)
998 return [[h objectForKey: @"list-id"] isNotEmpty];
1001 - (BOOL) isVirusScanned
1005 if ((h = [self mailHeaders]) == nil)
1008 if (![[h objectForKey: @"x-virus-status"] isNotEmpty]) return NO;
1009 if (![[h objectForKey: @"x-virus-scanned"] isNotEmpty]) return NO;
1013 - (NSString *) scanListHeaderValue: (id) _value
1014 forFieldWithPrefix: (NSString *) _prefix
1016 /* Note: not very tolerant on embedded commands and <> */
1017 // TODO: does not really belong here, should be a header-field-parser
1020 if (![_value isNotEmpty])
1023 if ([_value isKindOfClass:[NSArray class]]) {
1027 e = [_value objectEnumerator];
1028 while ((value = [e nextObject]) != nil) {
1029 value = [self scanListHeaderValue:value forFieldWithPrefix:_prefix];
1030 if (value != nil) return value;
1035 if (![_value isKindOfClass:[NSString class]])
1038 /* check for commas in string values */
1039 r = [_value rangeOfString: @","];
1041 return [self scanListHeaderValue:[_value componentsSeparatedByString: @","]
1042 forFieldWithPrefix:_prefix];
1045 /* value qualifies */
1046 if (![(NSString *)_value hasPrefix:_prefix])
1050 if ([_value characterAtIndex:0] == '<') {
1051 r = [_value rangeOfString: @">"];
1052 _value = (r.length == 0)
1053 ? [_value substringFromIndex:1]
1054 : [_value substringWithRange:NSMakeRange(1, r.location - 2)];
1060 - (NSString *) mailingListArchiveURL
1062 return [self scanListHeaderValue:
1063 [[self mailHeaders] objectForKey: @"list-archive"]
1064 forFieldWithPrefix: @"<http://"];
1067 - (NSString *) mailingListSubscribeURL
1069 return [self scanListHeaderValue:
1070 [[self mailHeaders] objectForKey: @"list-subscribe"]
1071 forFieldWithPrefix: @"<http://"];
1074 - (NSString *) mailingListUnsubscribeURL
1076 return [self scanListHeaderValue:
1077 [[self mailHeaders] objectForKey: @"list-unsubscribe"]
1078 forFieldWithPrefix: @"<http://"];
1086 Note: There is one thing which *can* change for an existing message,
1087 those are the IMAP4 flags (and annotations, which we do not use).
1088 Since we don't render the flags, it should be OK, if this changes
1089 we must embed the flagging into the etag.
1094 - (int) zlGenerationCount
1096 return 0; /* mails never change */
1099 /* Outlook mail tagging */
1101 - (NSString *) outlookMessageClass
1105 if ((type = [[self mailHeaders] objectForKey: @"x-kolab-type"]) != nil) {
1106 if ([type isEqualToString: @"application/x-vnd.kolab.contact"])
1107 return @"IPM.Contact";
1108 if ([type isEqualToString: @"application/x-vnd.kolab.task"])
1110 if ([type isEqualToString: @"application/x-vnd.kolab.event"])
1111 return @"IPM.Appointment";
1112 if ([type isEqualToString: @"application/x-vnd.kolab.note"])
1114 if ([type isEqualToString: @"application/x-vnd.kolab.journal"])
1115 return @"IPM.Journal";
1118 return @"IPM.Message"; /* email, default class */
1121 - (NSArray *) aclsForUser: (NSString *) uid
1123 return [container aclsForUser: uid];
1128 - (BOOL) isDebuggingEnabled
1133 @end /* SOGoMailObject */