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/NSDictionary+Utilities.h>
45 #import <SoObjects/SOGo/SOGoPermissions.h>
46 #import <SoObjects/SOGo/SOGoUser.h>
48 #import "NSString+Mail.h"
49 #import "NSData+Mail.h"
50 #import "SOGoMailFolder.h"
51 #import "SOGoMailAccount.h"
52 #import "SOGoMailAccounts.h"
53 #import "SOGoMailManager.h"
54 #import "SOGoMailBodyPart.h"
56 #import "SOGoMailObject.h"
58 @implementation SOGoMailObject
60 static NSArray *coreInfoKeys = nil;
61 static NSString *mailETag = nil;
62 static BOOL heavyDebug = NO;
63 static BOOL fetchHeader = YES;
64 static BOOL debugOn = NO;
65 static BOOL debugBodyStructure = NO;
66 static BOOL debugSoParts = NO;
70 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
72 if ((fetchHeader = ([ud boolForKey: @"SOGoDoNotFetchMailHeader"] ? NO : YES)))
73 NSLog(@"Note: fetching full mail header.");
75 NSLog(@"Note: not fetching full mail header: 'SOGoDoNotFetchMailHeader'");
77 /* Note: see SOGoMailManager.m for allowed IMAP4 keys */
78 /* Note: "BODY" actually returns the structure! */
80 coreInfoKeys = [[NSArray alloc] initWithObjects:
81 @"FLAGS", @"ENVELOPE", @"BODYSTRUCTURE",
84 // not yet supported: @"INTERNALDATE",
88 coreInfoKeys = [[NSArray alloc] initWithObjects:
89 @"FLAGS", @"ENVELOPE", @"BODYSTRUCTURE",
91 // not yet supported: @"INTERNALDATE",
95 if (![[ud objectForKey: @"SOGoMailDisableETag"] boolValue]) {
96 mailETag = [[NSString alloc] initWithFormat: @"\"imap4url_%d_%d_%03d\"",
97 UIX_MAILER_MAJOR_VERSION,
98 UIX_MAILER_MINOR_VERSION,
99 UIX_MAILER_SUBMINOR_VERSION];
100 NSLog(@"Note(SOGoMailObject): using constant etag for mail parts: '%@'",
104 NSLog(@"Note(SOGoMailObject): etag caching disabled!");
110 [headerPart release];
117 - (NSString *) relativeImap4Name
119 return [nameInContainer stringByDeletingPathExtension];
124 - (SOGoMailObject *)mailObject {
130 - (NSString *) keyExtensionForPart: (id) _partInfo
134 if (_partInfo == nil)
137 mt = [_partInfo valueForKey: @"type"];
138 st = [[_partInfo valueForKey: @"subtype"] lowercaseString];
139 if ([mt isEqualToString: @"text"]) {
140 if ([st isEqualToString: @"plain"]) return @".txt";
141 if ([st isEqualToString: @"html"]) return @".html";
142 if ([st isEqualToString: @"calendar"]) return @".ics";
143 if ([st isEqualToString: @"x-vcard"]) return @".vcf";
145 else if ([mt isEqualToString: @"image"])
146 return [@"." stringByAppendingString:st];
147 else if ([mt isEqualToString: @"application"]) {
148 if ([st isEqualToString: @"pgp-signature"])
155 - (NSArray *)relationshipKeysWithParts:(BOOL)_withParts {
156 /* should return non-multipart children */
161 parts = [[self bodyStructure] valueForKey: @"parts"];
162 if (![parts isNotNull])
164 if ((count = [parts count]) == 0)
167 for (i = 0, ma = nil; i < count; i++) {
172 part = [parts objectAtIndex:i];
173 hasParts = [part valueForKey: @"parts"] != nil ? YES:NO;
174 if ((hasParts && !_withParts) || (_withParts && !hasParts))
178 ma = [NSMutableArray arrayWithCapacity:count - i];
180 ext = [self keyExtensionForPart:part];
181 key = [[NSString alloc] initWithFormat: @"%d%@", i + 1, ext?ext: @""];
188 - (NSArray *) toOneRelationshipKeys
190 return [self relationshipKeysWithParts:NO];
193 - (NSArray *) toManyRelationshipKeys
195 return [self relationshipKeysWithParts:YES];
200 - (id) fetchParts: (NSArray *) _parts
202 // TODO: explain what it does
204 Called by -fetchPlainTextParts:
206 return [[self imap4Connection] fetchURL: [self imap4URL] parts:_parts];
211 - (BOOL)doesMailExist {
212 static NSArray *existsKey = nil;
215 if (coreInfos != nil) /* if we have coreinfos, we can use them */
216 return [coreInfos isNotNull];
218 /* otherwise fetch something really simple */
220 if (existsKey == nil) /* we use size, other suggestions? */
221 existsKey = [[NSArray alloc] initWithObjects: @"RFC822.SIZE", nil];
223 msgs = [self fetchParts:existsKey]; // returns dict
224 msgs = [msgs valueForKey: @"fetch"];
225 return [msgs count] > 0 ? YES : NO;
228 - (id) fetchCoreInfos
234 msgs = [self fetchParts: coreInfoKeys]; // returns dict
236 [self logWithFormat: @"M: %@", msgs];
237 msgs = [msgs valueForKey: @"fetch"];
238 if ([msgs count] > 0)
239 coreInfos = [msgs objectAtIndex: 0];
250 body = [[self fetchCoreInfos] valueForKey: @"body"];
251 if (debugBodyStructure)
252 [self logWithFormat: @"BODY: %@", body];
257 - (NGImap4Envelope *) envelope
259 return [[self fetchCoreInfos] valueForKey: @"envelope"];
262 - (NSString *) subject
264 return [[self envelope] subject];
267 - (NSString *) decodedSubject
269 return [[self subject] decodedSubject];
272 - (NSCalendarDate *) date
275 NSCalendarDate *date;
277 userTZ = [[context activeUser] timeZone];
278 date = [[self envelope] date];
279 [date setTimeZone: userTZ];
284 - (NSArray *) fromEnvelopeAddresses
286 return [[self envelope] from];
289 - (NSArray *) toEnvelopeAddresses
291 return [[self envelope] to];
294 - (NSArray *) ccEnvelopeAddresses
296 return [[self envelope] cc];
299 - (NSData *) mailHeaderData
301 return [[self fetchCoreInfos] valueForKey: @"header"];
304 - (BOOL) hasMailHeaderInCoreInfos
306 return [[self mailHeaderData] length] > 0 ? YES : NO;
309 - (id) mailHeaderPart
311 NGMimeMessageParser *parser;
314 if (headerPart != nil)
315 return [headerPart isNotNull] ? headerPart : nil;
317 if ([(data = [self mailHeaderData]) length] == 0)
320 // TODO: do we need to set some delegate method which stops parsing the body?
321 parser = [[NGMimeMessageParser alloc] init];
322 headerPart = [[parser parsePartFromData:data] retain];
323 [parser release]; parser = nil;
325 if (headerPart == nil) {
326 headerPart = [[NSNull null] retain];
332 - (NSDictionary *) mailHeaders
335 headers = [[[self mailHeaderPart] headers] copy];
340 - (id) lookupInfoForBodyPart: (id) _path
346 if (![_path isNotNull])
349 if ((info = [self bodyStructure]) == nil) {
350 [self errorWithFormat: @"got no body part structure!"];
354 /* ensure array argument */
356 if ([_path isKindOfClass:[NSString class]]) {
357 if ([_path length] == 0)
360 _path = [_path componentsSeparatedByString: @"."];
364 For each path component, eg 1,1,3
366 Remember that we need special processing for message/rfc822 which maps the
367 namespace of multiparts directly into the main namespace.
369 TODO(hh): no I don't remember, please explain in more detail!
371 pe = [_path objectEnumerator];
372 while ((p = [pe nextObject]) != nil && [info isNotNull]) {
377 [self debugWithFormat: @"check PATH: %@", p];
378 idx = [p intValue] - 1;
380 parts = [info valueForKey: @"parts"];
381 mt = [[info valueForKey: @"type"] lowercaseString];
382 if ([mt isEqualToString: @"message"]) {
383 /* we have special behaviour for message types */
386 if ((body = [info valueForKey: @"body"]) != nil) {
387 mt = [body valueForKey: @"type"];
388 if ([mt isEqualToString: @"multipart"])
389 parts = [body valueForKey: @"parts"];
391 parts = [NSArray arrayWithObject:body];
395 if (idx >= [parts count]) {
396 [self errorWithFormat:
397 @"body part index out of bounds(idx=%d vs count=%d): %@",
398 (idx + 1), [parts count], info];
401 info = [parts objectAtIndex:idx];
403 return [info isNotNull] ? info : nil;
411 id result, fullResult;
413 fullResult = [self fetchParts: [NSArray arrayWithObject: @"RFC822"]];
414 if (fullResult == nil)
417 if ([fullResult isKindOfClass: [NSException class]])
420 /* extract fetch result */
422 result = [fullResult valueForKey: @"fetch"];
423 if (![result isKindOfClass:[NSArray class]]) {
425 @"ERROR: unexpected IMAP4 result (missing 'fetch'): %@",
427 return [NSException exceptionWithHTTPStatus:500 /* server error */
428 reason: @"unexpected IMAP4 result"];
430 if ([result count] == 0)
433 result = [result objectAtIndex:0];
435 /* extract message */
437 if ((content = [result valueForKey: @"message"]) == nil) {
439 @"ERROR: unexpected IMAP4 result (missing 'message'): %@",
441 return [NSException exceptionWithHTTPStatus:500 /* server error */
442 reason: @"unexpected IMAP4 result"];
445 return [[content copy] autorelease];
448 - (NSString *) davContentType
450 return @"message/rfc822";
453 - (NSString *) contentAsString
458 content = [self content];
461 if ([content isKindOfClass: [NSData class]])
463 s = [[NSString alloc] initWithData: content
464 encoding: NSISOLatin1StringEncoding];
469 @"ERROR: could not convert data of length %d to string",
481 /* bulk fetching of plain/text content */
483 // - (BOOL) shouldFetchPartOfType: (NSString *) _type
484 // subtype: (NSString *) _subtype
487 // This method decides which parts are 'prefetched' for display. Those are
488 // usually text parts (the set is currently hardcoded in this method ...).
490 // _type = [_type lowercaseString];
491 // _subtype = [_subtype lowercaseString];
493 // return (([_type isEqualToString: @"text"]
494 // && ([_subtype isEqualToString: @"plain"]
495 // || [_subtype isEqualToString: @"html"]
496 // || [_subtype isEqualToString: @"calendar"]))
497 // || ([_type isEqualToString: @"application"]
498 // && ([_subtype isEqualToString: @"pgp-signature"]
499 // || [_subtype hasPrefix: @"x-vnd.kolab."])));
502 - (void) addRequiredKeysOfStructure: (NSDictionary *) info
504 toArray: (NSMutableArray *) keys
505 acceptedTypes: (NSArray *) types
508 This is used to collect the set of IMAP4 fetch-keys required to fetch
509 the basic parts of the body structure. That is, to fetch all parts which
510 are displayed 'inline' in a single IMAP4 fetch.
512 The method calls itself recursively to walk the body structure.
518 NSString *sp, *mimeType;
521 mimeType = [[NSString stringWithFormat: @"%@/%@",
522 [info valueForKey: @"type"],
523 [info valueForKey: @"subtype"]]
525 if ([types containsObject: mimeType])
528 k = [NSString stringWithFormat: @"body[%@]", p];
532 for some reason we need to add ".TEXT" for plain text stuff on root
534 TODO: check with HTML
538 [keys addObject: [NSDictionary dictionaryWithObjectsAndKeys: k, @"key",
539 mimeType, @"mimeType", nil]];
542 parts = [info objectForKey: @"parts"];
543 count = [parts count];
544 for (i = 0; i < count; i++)
546 sp = (([p length] > 0)
547 ? [p stringByAppendingFormat: @".%d", i + 1]
548 : [NSString stringWithFormat: @"%d", i + 1]);
550 childInfo = [parts objectAtIndex: i];
552 [self addRequiredKeysOfStructure: childInfo
553 path: sp toArray: keys
554 acceptedTypes: types];
558 body = [info objectForKey: @"body"];
561 sp = [[body valueForKey: @"type"] lowercaseString];
562 if ([sp isEqualToString: @"multipart"])
565 sp = [p length] > 0 ? [p stringByAppendingString: @".1"] : @"1";
566 [self addRequiredKeysOfStructure: body
567 path: sp toArray: keys
568 acceptedTypes: types];
572 - (NSArray *) plainTextContentFetchKeys
575 The name is not 100% correct. The method returns all body structure fetch
576 keys which are marked by the -shouldFetchPartOfType:subtype: method.
581 types = [NSArray arrayWithObjects: @"text/plain", @"text/html",
582 @"text/calendar", @"application/pgp-signature", nil];
583 ma = [NSMutableArray arrayWithCapacity: 4];
584 [self addRequiredKeysOfStructure: [self bodyStructure]
585 path: @"" toArray: ma acceptedTypes: types];
590 - (NSDictionary *) fetchPlainTextParts: (NSArray *) _fetchKeys
592 // TODO: is the name correct or does it also fetch other parts?
593 NSMutableDictionary *flatContents;
597 [self debugWithFormat: @"fetch keys: %@", _fetchKeys];
599 result = [self fetchParts: [_fetchKeys objectsForKey: @"key"]];
600 result = [result valueForKey: @"RawResponse"]; // hackish
602 // Note: -valueForKey: doesn't work!
603 result = [(NSDictionary *)result objectForKey: @"fetch"];
605 count = [_fetchKeys count];
606 flatContents = [NSMutableDictionary dictionaryWithCapacity:count];
607 for (i = 0; i < count; i++) {
611 key = [[_fetchKeys objectAtIndex:i] objectForKey: @"key"];
612 data = [(NSDictionary *)[(NSDictionary *)result objectForKey:key]
613 objectForKey: @"data"];
615 if (![data isNotNull]) {
616 [self errorWithFormat: @"got no data for key: %@", key];
620 if ([key isEqualToString: @"body[text]"])
621 key = @""; // see key collector for explanation (TODO: where?)
622 else if ([key hasPrefix: @"body["]) {
625 key = [key substringFromIndex:5];
626 r = [key rangeOfString: @"]"];
628 key = [key substringToIndex:r.location];
630 [flatContents setObject:data forKey:key];
635 - (NSDictionary *) fetchPlainTextParts
637 return [self fetchPlainTextParts: [self plainTextContentFetchKeys]];
640 /* convert parts to strings */
641 - (NSString *) stringForData: (NSData *) _data
642 partInfo: (NSDictionary *) _info
644 NSString *charset, *s;
647 if ([_data isNotNull])
650 = [_data bodyDataFromEncoding: [_info objectForKey: @"encoding"]];
652 charset = [[_info valueForKey: @"parameterList"] valueForKey: @"charset"];
653 if (![charset length])
655 s = [[NSString alloc] initWithData: mailData encoding: NSUTF8StringEncoding];
659 s = [NSString stringWithData: mailData
660 usingEncodingNamed: charset];
668 - (NSDictionary *) stringifyTextParts: (NSDictionary *) _datas
670 NSMutableDictionary *md;
675 md = [NSMutableDictionary dictionaryWithCapacity:4];
676 keys = [_datas keyEnumerator];
677 while ((key = [keys nextObject]))
679 info = [self lookupInfoForBodyPart: key];
680 s = [self stringForData: [_datas objectForKey:key] partInfo: info];
682 [md setObject: s forKey: key];
688 - (NSDictionary *) fetchPlainTextStrings: (NSArray *) _fetchKeys
691 The fetched parts are NSData objects, this method converts them into
692 NSString objects based on the information inside the bodystructure.
694 The fetch-keys are body fetch-keys like: body[text] or body[1.2.3].
695 The keys in the result dictionary are "" for 'text' and 1.2.3 for parts.
699 if ((datas = [self fetchPlainTextParts:_fetchKeys]) == nil)
701 if ([datas isKindOfClass:[NSException class]])
704 return [self stringifyTextParts:datas];
709 - (NSException *) addFlags: (id) _flags
711 return [[self imap4Connection] addFlags:_flags toURL: [self imap4URL]];
714 - (NSException *) removeFlags: (id) _flags
716 return [[self imap4Connection] removeFlags:_flags toURL: [self imap4URL]];
721 - (BOOL) isDeletionAllowed
726 login = [[context activeUser] login];
727 parentAcl = [[self container] aclsForUser: login];
729 return [parentAcl containsObject: SOGoRole_ObjectEraser];
734 - (id) lookupImap4BodyPartKey: (NSString *) _key
737 // TODO: we might want to check for existence prior controller creation
741 NSDictionary *partDesc;
744 parts = [[self bodyStructure] objectForKey: @"parts"];
745 part = [_key intValue] - 1;
746 if (part > -1 && part < [parts count])
748 partDesc = [parts objectAtIndex: part];
749 mimeType = [[partDesc keysWithFormat: @"%{type}/%{subtype}"] lowercaseString];
750 clazz = [SOGoMailBodyPart bodyPartClassForMimeType: mimeType
756 return [clazz objectWithName:_key inContainer: self];
759 - (id) lookupName: (NSString *) _key
761 acquire: (BOOL) _flag
765 /* first check attributes directly bound to the application */
766 if ((obj = [super lookupName:_key inContext:_ctx acquire:NO]) != nil)
769 /* lookup body part */
771 if ([self isBodyPartKey:_key inContext:_ctx]) {
772 if ((obj = [self lookupImap4BodyPartKey:_key inContext:_ctx]) != nil) {
774 [self logWithFormat: @"mail looked up part %@: %@", _key, obj];
779 /* return 404 to stop acquisition */
780 return [NSException exceptionWithHTTPStatus:404 /* Not Found */
781 reason: @"Did not find mail method or part-reference!"];
786 - (BOOL) davIsCollection
788 /* while a mail has child objects, it should appear as a file in WebDAV */
792 - (id) davContentLength
794 return [[self fetchCoreInfos] valueForKey: @"size"];
797 - (NSDate *) davCreationDate
799 // TODO: use INTERNALDATE once NGImap4 supports that
803 - (NSDate *) davLastModified
805 return [self davCreationDate];
808 - (NSException *) davMoveToTargetObject: (id) _target
809 newName: (NSString *) _name
812 [self logWithFormat: @"TODO: should move mail as '%@' to: %@",
814 return [NSException exceptionWithHTTPStatus: 501 /* Not Implemented */
815 reason: @"not implemented"];
818 - (NSException *) davCopyToTargetObject: (id) _target
819 newName: (NSString *) _name
823 Note: this is special because we create SOGoMailObject's even if they do
824 not exist (for performance reasons).
826 Also: we cannot really take a target resource, the ID will be assigned by
828 We even cannot return a 'location' header instead because IMAP4
829 doesn't tell us the new ID.
833 destImap4URL = ([_name length] == 0)
834 ? [[_target container] imap4URL]
835 : [_target imap4URL];
837 return [[self mailManager] copyMailURL:[self imap4URL]
838 toFolderURL:destImap4URL
839 password:[self imap4Password]];
844 - (id) GETAction: (id) _ctx
850 if ((error = [self matchesRequestConditionInContext:_ctx]) != nil) {
851 /* check whether the mail still exists */
852 if (![self doesMailExist]) {
853 return [NSException exceptionWithHTTPStatus:404 /* Not Found */
854 reason: @"mail was deleted"];
856 return error; /* return 304 or 416 */
859 content = [self content];
860 if ([content isKindOfClass:[NSException class]])
862 if (content == nil) {
863 return [NSException exceptionWithHTTPStatus:404 /* Not Found */
864 reason: @"did not find IMAP4 message"];
867 r = [(WOContext *)_ctx response];
868 [r setHeader: @"message/rfc822" forKey: @"content-type"];
869 [r setContent:content];
875 - (NSException *) trashInContext: (id) _ctx
878 Trashing is three actions:
879 a) copy to trash folder
880 b) mark mail as deleted
883 In case b) or c) fails, we can't do anything because IMAP4 doesn't tell us
884 the ID used in the trash folder.
886 SOGoMailFolder *trashFolder;
889 // TODO: check for safe HTTP method
891 trashFolder = [[self mailAccountFolder] trashFolderInContext:_ctx];
892 if ([trashFolder isKindOfClass:[NSException class]])
893 return (NSException *)trashFolder;
894 if (![trashFolder isNotNull]) {
895 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
896 reason: @"Did not find Trash folder!"];
898 [trashFolder flushMailCaches];
902 error = [self davCopyToTargetObject:trashFolder
903 newName: @"fakeNewUnusedByIMAP4" /* autoassigned */
905 if (error != nil) return error;
907 /* b) mark deleted */
909 error = [[self imap4Connection] markURLDeleted: [self imap4URL]];
910 if (error != nil) return error;
912 [container markForExpunge];
914 [self flushMailCaches];
919 - (NSException *) copyToFolderNamed: (NSString *) folderName
922 SOGoMailAccounts *destFolder;
923 NSEnumerator *folders;
924 NSString *currentFolderName, *reason;
926 // TODO: check for safe HTTP method
928 destFolder = [self mailAccountsFolder];
929 folders = [[folderName componentsSeparatedByString: @"/"] objectEnumerator];
930 currentFolderName = [folders nextObject];
931 currentFolderName = [folders nextObject];
933 while (currentFolderName)
935 destFolder = [destFolder lookupName: currentFolderName
938 if ([destFolder isKindOfClass: [NSException class]])
939 return (NSException *) destFolder;
940 currentFolderName = [folders nextObject];
943 if (!([destFolder isKindOfClass: [SOGoMailFolder class]]
944 && [destFolder isNotNull]))
946 reason = [NSString stringWithFormat: @"Did not find folder name '%@'!",
948 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
951 [destFolder flushMailCaches];
955 return [self davCopyToTargetObject: destFolder
956 newName: @"fakeNewUnusedByIMAP4" /* autoassigned */
960 - (NSException *) moveToFolderNamed: (NSString *) folderName
965 if (![self copyToFolderNamed: folderName
968 /* b) mark deleted */
970 error = [[self imap4Connection] markURLDeleted: [self imap4URL]];
971 if (error != nil) return error;
973 [self flushMailCaches];
979 - (NSException *) delete
982 Note: delete is different to DELETEAction: for mails! The 'delete' runs
983 either flags a message as deleted or moves it to the Trash while
984 the DELETEAction: really deletes a message (by flagging it as
985 deleted _AND_ performing an expunge).
987 // TODO: copy to Trash folder
990 // TODO: check for safe HTTP method
992 error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
996 - (id) DELETEAction: (id) _ctx
1000 // TODO: ensure safe HTTP method
1002 error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
1003 if (error != nil) return error;
1005 error = [[self imap4Connection] expungeAtURL:[[self container] imap4URL]];
1006 if (error != nil) return error; // TODO: unflag as deleted?
1008 return [NSNumber numberWithBool:YES]; /* delete was successful */
1011 /* some mail classification */
1013 - (BOOL) isKolabObject
1017 if ((h = [self mailHeaders]) != nil)
1018 return [[h objectForKey: @"x-kolab-type"] isNotEmpty];
1020 // TODO: we could check the body structure?
1025 - (BOOL) isMailingListMail
1029 if ((h = [self mailHeaders]) == nil)
1032 return [[h objectForKey: @"list-id"] isNotEmpty];
1035 - (BOOL) isVirusScanned
1039 if ((h = [self mailHeaders]) == nil)
1042 if (![[h objectForKey: @"x-virus-status"] isNotEmpty]) return NO;
1043 if (![[h objectForKey: @"x-virus-scanned"] isNotEmpty]) return NO;
1047 - (NSString *) scanListHeaderValue: (id) _value
1048 forFieldWithPrefix: (NSString *) _prefix
1050 /* Note: not very tolerant on embedded commands and <> */
1051 // TODO: does not really belong here, should be a header-field-parser
1054 if (![_value isNotEmpty])
1057 if ([_value isKindOfClass:[NSArray class]]) {
1061 e = [_value objectEnumerator];
1062 while ((value = [e nextObject]) != nil) {
1063 value = [self scanListHeaderValue:value forFieldWithPrefix:_prefix];
1064 if (value != nil) return value;
1069 if (![_value isKindOfClass:[NSString class]])
1072 /* check for commas in string values */
1073 r = [_value rangeOfString: @","];
1075 return [self scanListHeaderValue:[_value componentsSeparatedByString: @","]
1076 forFieldWithPrefix:_prefix];
1079 /* value qualifies */
1080 if (![(NSString *)_value hasPrefix:_prefix])
1084 if ([_value characterAtIndex:0] == '<') {
1085 r = [_value rangeOfString: @">"];
1086 _value = (r.length == 0)
1087 ? [_value substringFromIndex:1]
1088 : [_value substringWithRange:NSMakeRange(1, r.location - 2)];
1094 - (NSString *) mailingListArchiveURL
1096 return [self scanListHeaderValue:
1097 [[self mailHeaders] objectForKey: @"list-archive"]
1098 forFieldWithPrefix: @"<http://"];
1101 - (NSString *) mailingListSubscribeURL
1103 return [self scanListHeaderValue:
1104 [[self mailHeaders] objectForKey: @"list-subscribe"]
1105 forFieldWithPrefix: @"<http://"];
1108 - (NSString *) mailingListUnsubscribeURL
1110 return [self scanListHeaderValue:
1111 [[self mailHeaders] objectForKey: @"list-unsubscribe"]
1112 forFieldWithPrefix: @"<http://"];
1120 Note: There is one thing which *can* change for an existing message,
1121 those are the IMAP4 flags (and annotations, which we do not use).
1122 Since we don't render the flags, it should be OK, if this changes
1123 we must embed the flagging into the etag.
1128 - (int) zlGenerationCount
1130 return 0; /* mails never change */
1133 /* Outlook mail tagging */
1135 - (NSString *) outlookMessageClass
1139 if ((type = [[self mailHeaders] objectForKey: @"x-kolab-type"]) != nil) {
1140 if ([type isEqualToString: @"application/x-vnd.kolab.contact"])
1141 return @"IPM.Contact";
1142 if ([type isEqualToString: @"application/x-vnd.kolab.task"])
1144 if ([type isEqualToString: @"application/x-vnd.kolab.event"])
1145 return @"IPM.Appointment";
1146 if ([type isEqualToString: @"application/x-vnd.kolab.note"])
1148 if ([type isEqualToString: @"application/x-vnd.kolab.journal"])
1149 return @"IPM.Journal";
1152 return @"IPM.Message"; /* email, default class */
1155 - (NSArray *) aclsForUser: (NSString *) uid
1157 return [container aclsForUser: uid];
1162 - (BOOL) isDebuggingEnabled
1167 @end /* SOGoMailObject */