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 - (NSArray *) replyToEnvelopeAddresses
301 return [[self envelope] replyTo];
304 - (NSData *) mailHeaderData
306 return [[self fetchCoreInfos] valueForKey: @"header"];
309 - (BOOL) hasMailHeaderInCoreInfos
311 return [[self mailHeaderData] length] > 0 ? YES : NO;
314 - (id) mailHeaderPart
316 NGMimeMessageParser *parser;
319 if (headerPart != nil)
320 return [headerPart isNotNull] ? headerPart : nil;
322 if ([(data = [self mailHeaderData]) length] == 0)
325 // TODO: do we need to set some delegate method which stops parsing the body?
326 parser = [[NGMimeMessageParser alloc] init];
327 headerPart = [[parser parsePartFromData:data] retain];
328 [parser release]; parser = nil;
330 if (headerPart == nil) {
331 headerPart = [[NSNull null] retain];
337 - (NSDictionary *) mailHeaders
340 headers = [[[self mailHeaderPart] headers] copy];
345 - (id) lookupInfoForBodyPart: (id) _path
351 if (![_path isNotNull])
354 if ((info = [self bodyStructure]) == nil) {
355 [self errorWithFormat: @"got no body part structure!"];
359 /* ensure array argument */
361 if ([_path isKindOfClass:[NSString class]]) {
362 if ([_path length] == 0)
365 _path = [_path componentsSeparatedByString: @"."];
369 For each path component, eg 1,1,3
371 Remember that we need special processing for message/rfc822 which maps the
372 namespace of multiparts directly into the main namespace.
374 TODO(hh): no I don't remember, please explain in more detail!
376 pe = [_path objectEnumerator];
377 while ((p = [pe nextObject]) != nil && [info isNotNull]) {
382 [self debugWithFormat: @"check PATH: %@", p];
383 idx = [p intValue] - 1;
385 parts = [info valueForKey: @"parts"];
386 mt = [[info valueForKey: @"type"] lowercaseString];
387 if ([mt isEqualToString: @"message"]) {
388 /* we have special behaviour for message types */
391 if ((body = [info valueForKey: @"body"]) != nil) {
392 mt = [body valueForKey: @"type"];
393 if ([mt isEqualToString: @"multipart"])
394 parts = [body valueForKey: @"parts"];
396 parts = [NSArray arrayWithObject:body];
400 if (idx >= [parts count]) {
401 [self errorWithFormat:
402 @"body part index out of bounds(idx=%d vs count=%d): %@",
403 (idx + 1), [parts count], info];
406 info = [parts objectAtIndex:idx];
408 return [info isNotNull] ? info : nil;
416 id result, fullResult;
418 fullResult = [self fetchParts: [NSArray arrayWithObject: @"RFC822"]];
419 if (fullResult == nil)
422 if ([fullResult isKindOfClass: [NSException class]])
425 /* extract fetch result */
427 result = [fullResult valueForKey: @"fetch"];
428 if (![result isKindOfClass:[NSArray class]]) {
430 @"ERROR: unexpected IMAP4 result (missing 'fetch'): %@",
432 return [NSException exceptionWithHTTPStatus:500 /* server error */
433 reason: @"unexpected IMAP4 result"];
435 if ([result count] == 0)
438 result = [result objectAtIndex:0];
440 /* extract message */
442 if ((content = [result valueForKey: @"message"]) == nil) {
444 @"ERROR: unexpected IMAP4 result (missing 'message'): %@",
446 return [NSException exceptionWithHTTPStatus:500 /* server error */
447 reason: @"unexpected IMAP4 result"];
450 return [[content copy] autorelease];
453 - (NSString *) davContentType
455 return @"message/rfc822";
458 - (NSString *) contentAsString
463 content = [self content];
466 if ([content isKindOfClass: [NSData class]])
468 #warning we ignore the charset here?
469 s = [[NSString alloc] initWithData: content
470 encoding: NSISOLatin1StringEncoding];
475 @"ERROR: could not convert data of length %d to string",
487 /* bulk fetching of plain/text content */
489 // - (BOOL) shouldFetchPartOfType: (NSString *) _type
490 // subtype: (NSString *) _subtype
493 // This method decides which parts are 'prefetched' for display. Those are
494 // usually text parts (the set is currently hardcoded in this method ...).
496 // _type = [_type lowercaseString];
497 // _subtype = [_subtype lowercaseString];
499 // return (([_type isEqualToString: @"text"]
500 // && ([_subtype isEqualToString: @"plain"]
501 // || [_subtype isEqualToString: @"html"]
502 // || [_subtype isEqualToString: @"calendar"]))
503 // || ([_type isEqualToString: @"application"]
504 // && ([_subtype isEqualToString: @"pgp-signature"]
505 // || [_subtype hasPrefix: @"x-vnd.kolab."])));
508 - (void) addRequiredKeysOfStructure: (NSDictionary *) info
510 toArray: (NSMutableArray *) keys
511 acceptedTypes: (NSArray *) types
514 This is used to collect the set of IMAP4 fetch-keys required to fetch
515 the basic parts of the body structure. That is, to fetch all parts which
516 are displayed 'inline' in a single IMAP4 fetch.
518 The method calls itself recursively to walk the body structure.
524 NSString *sp, *mimeType;
527 mimeType = [[NSString stringWithFormat: @"%@/%@",
528 [info valueForKey: @"type"],
529 [info valueForKey: @"subtype"]]
531 if ([types containsObject: mimeType])
534 k = [NSString stringWithFormat: @"body[%@]", p];
538 for some reason we need to add ".TEXT" for plain text stuff on root
540 TODO: check with HTML
544 [keys addObject: [NSDictionary dictionaryWithObjectsAndKeys: k, @"key",
545 mimeType, @"mimeType", nil]];
548 parts = [info objectForKey: @"parts"];
549 count = [parts count];
550 for (i = 0; i < count; i++)
552 sp = (([p length] > 0)
553 ? [p stringByAppendingFormat: @".%d", i + 1]
554 : [NSString stringWithFormat: @"%d", i + 1]);
556 childInfo = [parts objectAtIndex: i];
558 [self addRequiredKeysOfStructure: childInfo
559 path: sp toArray: keys
560 acceptedTypes: types];
564 body = [info objectForKey: @"body"];
567 sp = [[body valueForKey: @"type"] lowercaseString];
568 if ([sp isEqualToString: @"multipart"])
571 sp = [p length] > 0 ? [p stringByAppendingString: @".1"] : @"1";
572 [self addRequiredKeysOfStructure: body
573 path: sp toArray: keys
574 acceptedTypes: types];
578 - (NSArray *) plainTextContentFetchKeys
581 The name is not 100% correct. The method returns all body structure fetch
582 keys which are marked by the -shouldFetchPartOfType:subtype: method.
587 types = [NSArray arrayWithObjects: @"text/plain", @"text/html",
588 @"text/calendar", @"application/ics",
589 @"application/pgp-signature", nil];
590 ma = [NSMutableArray arrayWithCapacity: 4];
591 [self addRequiredKeysOfStructure: [self bodyStructure]
592 path: @"" toArray: ma acceptedTypes: types];
597 - (NSDictionary *) fetchPlainTextParts: (NSArray *) _fetchKeys
599 // TODO: is the name correct or does it also fetch other parts?
600 NSMutableDictionary *flatContents;
604 [self debugWithFormat: @"fetch keys: %@", _fetchKeys];
606 result = [self fetchParts: [_fetchKeys objectsForKey: @"key"]];
607 result = [result valueForKey: @"RawResponse"]; // hackish
609 // Note: -valueForKey: doesn't work!
610 result = [(NSDictionary *)result objectForKey: @"fetch"];
612 count = [_fetchKeys count];
613 flatContents = [NSMutableDictionary dictionaryWithCapacity:count];
614 for (i = 0; i < count; i++) {
618 key = [[_fetchKeys objectAtIndex:i] objectForKey: @"key"];
619 data = [(NSDictionary *)[(NSDictionary *)result objectForKey:key]
620 objectForKey: @"data"];
622 if (![data isNotNull]) {
623 [self errorWithFormat: @"got no data for key: %@", key];
627 if ([key isEqualToString: @"body[text]"])
628 key = @""; // see key collector for explanation (TODO: where?)
629 else if ([key hasPrefix: @"body["]) {
632 key = [key substringFromIndex:5];
633 r = [key rangeOfString: @"]"];
635 key = [key substringToIndex:r.location];
637 [flatContents setObject:data forKey:key];
642 - (NSDictionary *) fetchPlainTextParts
644 return [self fetchPlainTextParts: [self plainTextContentFetchKeys]];
647 /* convert parts to strings */
648 - (NSString *) stringForData: (NSData *) _data
649 partInfo: (NSDictionary *) _info
651 NSString *charset, *s;
654 if ([_data isNotNull])
657 = [_data bodyDataFromEncoding: [_info objectForKey: @"encoding"]];
659 charset = [[_info valueForKey: @"parameterList"] valueForKey: @"charset"];
660 if (![charset length])
666 s = [NSString stringWithData: mailData usingEncodingNamed: charset];
669 // If it has failed, we try at least using UTF-8. Normally, this can NOT fail.
670 // Unfortunately, it seems to fail under GNUstep so we try latin1 if that's
673 s = [[[NSString alloc] initWithData: mailData encoding: NSUTF8StringEncoding] autorelease];
676 s = [[[NSString alloc] initWithData: mailData encoding: NSISOLatin1StringEncoding] autorelease];
684 - (NSDictionary *) stringifyTextParts: (NSDictionary *) _datas
686 NSMutableDictionary *md;
691 md = [NSMutableDictionary dictionaryWithCapacity:4];
692 keys = [_datas keyEnumerator];
693 while ((key = [keys nextObject]))
695 info = [self lookupInfoForBodyPart: key];
696 s = [self stringForData: [_datas objectForKey:key] partInfo: info];
698 [md setObject: s forKey: key];
704 - (NSDictionary *) fetchPlainTextStrings: (NSArray *) _fetchKeys
707 The fetched parts are NSData objects, this method converts them into
708 NSString objects based on the information inside the bodystructure.
710 The fetch-keys are body fetch-keys like: body[text] or body[1.2.3].
711 The keys in the result dictionary are "" for 'text' and 1.2.3 for parts.
715 if ((datas = [self fetchPlainTextParts:_fetchKeys]) == nil)
717 if ([datas isKindOfClass:[NSException class]])
720 return [self stringifyTextParts:datas];
725 - (NSException *) addFlags: (id) _flags
727 return [[self imap4Connection] addFlags:_flags toURL: [self imap4URL]];
730 - (NSException *) removeFlags: (id) _flags
732 return [[self imap4Connection] removeFlags:_flags toURL: [self imap4URL]];
737 - (BOOL) isDeletionAllowed
742 login = [[context activeUser] login];
743 parentAcl = [[self container] aclsForUser: login];
745 return [parentAcl containsObject: SOGoRole_ObjectEraser];
750 - (id) lookupImap4BodyPartKey: (NSString *) _key
753 // TODO: we might want to check for existence prior controller creation
757 NSDictionary *partDesc;
760 parts = [[self bodyStructure] objectForKey: @"parts"];
762 /* We don't have parts here but we're trying to download the message's
763 content that could be an image/jpeg, as an example */
764 if ([parts count] == 0 && ![_key intValue])
766 partDesc = [self bodyStructure];
771 part = [_key intValue] - 1;
772 if (part > -1 && part < [parts count])
773 partDesc = [parts objectAtIndex: part];
780 mimeType = [[partDesc keysWithFormat: @"%{type}/%{subtype}"] lowercaseString];
781 clazz = [SOGoMailBodyPart bodyPartClassForMimeType: mimeType
787 return [clazz objectWithName:_key inContainer: self];
790 - (id) lookupName: (NSString *) _key
792 acquire: (BOOL) _flag
796 /* first check attributes directly bound to the application */
797 if ((obj = [super lookupName:_key inContext:_ctx acquire:NO]) != nil)
800 /* lookup body part */
802 if ([self isBodyPartKey:_key inContext:_ctx]) {
803 if ((obj = [self lookupImap4BodyPartKey:_key inContext:_ctx]) != nil) {
805 [self logWithFormat: @"mail looked up part %@: %@", _key, obj];
810 /* return 404 to stop acquisition */
811 return [NSException exceptionWithHTTPStatus:404 /* Not Found */
812 reason: @"Did not find mail method or part-reference!"];
817 - (BOOL) davIsCollection
819 /* while a mail has child objects, it should appear as a file in WebDAV */
823 - (id) davContentLength
825 return [[self fetchCoreInfos] valueForKey: @"size"];
828 - (NSDate *) davCreationDate
830 // TODO: use INTERNALDATE once NGImap4 supports that
834 - (NSDate *) davLastModified
836 return [self davCreationDate];
839 - (NSException *) davMoveToTargetObject: (id) _target
840 newName: (NSString *) _name
843 [self logWithFormat: @"TODO: should move mail as '%@' to: %@",
845 return [NSException exceptionWithHTTPStatus: 501 /* Not Implemented */
846 reason: @"not implemented"];
849 - (NSException *) davCopyToTargetObject: (id) _target
850 newName: (NSString *) _name
854 Note: this is special because we create SOGoMailObject's even if they do
855 not exist (for performance reasons).
857 Also: we cannot really take a target resource, the ID will be assigned by
859 We even cannot return a 'location' header instead because IMAP4
860 doesn't tell us the new ID.
864 destImap4URL = ([_name length] == 0)
865 ? [[_target container] imap4URL]
866 : [_target imap4URL];
868 return [[self mailManager] copyMailURL:[self imap4URL]
869 toFolderURL:destImap4URL
870 password:[self imap4Password]];
875 - (id) GETAction: (id) _ctx
881 if ((error = [self matchesRequestConditionInContext:_ctx]) != nil) {
882 /* check whether the mail still exists */
883 if (![self doesMailExist]) {
884 return [NSException exceptionWithHTTPStatus:404 /* Not Found */
885 reason: @"mail was deleted"];
887 return error; /* return 304 or 416 */
890 content = [self content];
891 if ([content isKindOfClass:[NSException class]])
893 if (content == nil) {
894 return [NSException exceptionWithHTTPStatus:404 /* Not Found */
895 reason: @"did not find IMAP4 message"];
898 r = [(WOContext *)_ctx response];
899 [r setHeader: @"message/rfc822" forKey: @"content-type"];
900 [r setContent:content];
906 - (NSException *) trashInContext: (id) _ctx
909 Trashing is three actions:
910 a) copy to trash folder
911 b) mark mail as deleted
914 In case b) or c) fails, we can't do anything because IMAP4 doesn't tell us
915 the ID used in the trash folder.
917 SOGoMailFolder *trashFolder;
920 // TODO: check for safe HTTP method
922 trashFolder = [[self mailAccountFolder] trashFolderInContext:_ctx];
923 if ([trashFolder isKindOfClass:[NSException class]])
924 return (NSException *)trashFolder;
925 if (![trashFolder isNotNull]) {
926 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
927 reason: @"Did not find Trash folder!"];
929 [trashFolder flushMailCaches];
933 error = [self davCopyToTargetObject:trashFolder
934 newName: @"fakeNewUnusedByIMAP4" /* autoassigned */
936 if (error != nil) return error;
938 /* b) mark deleted */
940 error = [[self imap4Connection] markURLDeleted: [self imap4URL]];
941 if (error != nil) return error;
943 [container markForExpunge];
945 [self flushMailCaches];
950 - (NSException *) copyToFolderNamed: (NSString *) folderName
953 SOGoMailFolder *destFolder;
954 NSEnumerator *folders;
955 NSString *currentFolderName, *reason;
957 // TODO: check for safe HTTP method
959 destFolder = (SOGoMailFolder *) [self mailAccountsFolder];
960 folders = [[folderName componentsSeparatedByString: @"/"] objectEnumerator];
961 currentFolderName = [folders nextObject];
962 currentFolderName = [folders nextObject];
964 while (currentFolderName)
966 destFolder = [destFolder lookupName: currentFolderName
969 if ([destFolder isKindOfClass: [NSException class]])
970 return (NSException *) destFolder;
971 currentFolderName = [folders nextObject];
974 if (!([destFolder isKindOfClass: [SOGoMailFolder class]]
975 && [destFolder isNotNull]))
977 reason = [NSString stringWithFormat: @"Did not find folder name '%@'!",
979 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
982 [destFolder flushMailCaches];
986 return [self davCopyToTargetObject: destFolder
987 newName: @"fakeNewUnusedByIMAP4" /* autoassigned */
991 - (NSException *) moveToFolderNamed: (NSString *) folderName
996 if (![self copyToFolderNamed: folderName
999 /* b) mark deleted */
1001 error = [[self imap4Connection] markURLDeleted: [self imap4URL]];
1002 if (error != nil) return error;
1004 [self flushMailCaches];
1010 - (NSException *) delete
1013 Note: delete is different to DELETEAction: for mails! The 'delete' runs
1014 either flags a message as deleted or moves it to the Trash while
1015 the DELETEAction: really deletes a message (by flagging it as
1016 deleted _AND_ performing an expunge).
1018 // TODO: copy to Trash folder
1021 // TODO: check for safe HTTP method
1023 error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
1027 - (id) DELETEAction: (id) _ctx
1031 // TODO: ensure safe HTTP method
1033 error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
1034 if (error != nil) return error;
1036 error = [[self imap4Connection] expungeAtURL:[[self container] imap4URL]];
1037 if (error != nil) return error; // TODO: unflag as deleted?
1039 return [NSNumber numberWithBool:YES]; /* delete was successful */
1042 /* some mail classification */
1044 - (BOOL) isKolabObject
1048 if ((h = [self mailHeaders]) != nil)
1049 return [[h objectForKey: @"x-kolab-type"] isNotEmpty];
1051 // TODO: we could check the body structure?
1056 - (BOOL) isMailingListMail
1060 if ((h = [self mailHeaders]) == nil)
1063 return [[h objectForKey: @"list-id"] isNotEmpty];
1066 - (BOOL) isVirusScanned
1070 if ((h = [self mailHeaders]) == nil)
1073 if (![[h objectForKey: @"x-virus-status"] isNotEmpty]) return NO;
1074 if (![[h objectForKey: @"x-virus-scanned"] isNotEmpty]) return NO;
1078 - (NSString *) scanListHeaderValue: (id) _value
1079 forFieldWithPrefix: (NSString *) _prefix
1081 /* Note: not very tolerant on embedded commands and <> */
1082 // TODO: does not really belong here, should be a header-field-parser
1085 if (![_value isNotEmpty])
1088 if ([_value isKindOfClass:[NSArray class]]) {
1092 e = [_value objectEnumerator];
1093 while ((value = [e nextObject]) != nil) {
1094 value = [self scanListHeaderValue:value forFieldWithPrefix:_prefix];
1095 if (value != nil) return value;
1100 if (![_value isKindOfClass:[NSString class]])
1103 /* check for commas in string values */
1104 r = [_value rangeOfString: @","];
1106 return [self scanListHeaderValue:[_value componentsSeparatedByString: @","]
1107 forFieldWithPrefix:_prefix];
1110 /* value qualifies */
1111 if (![(NSString *)_value hasPrefix:_prefix])
1115 if ([_value characterAtIndex:0] == '<') {
1116 r = [_value rangeOfString: @">"];
1117 _value = (r.length == 0)
1118 ? [_value substringFromIndex:1]
1119 : [_value substringWithRange:NSMakeRange(1, r.location - 2)];
1125 - (NSString *) mailingListArchiveURL
1127 return [self scanListHeaderValue:
1128 [[self mailHeaders] objectForKey: @"list-archive"]
1129 forFieldWithPrefix: @"<http://"];
1132 - (NSString *) mailingListSubscribeURL
1134 return [self scanListHeaderValue:
1135 [[self mailHeaders] objectForKey: @"list-subscribe"]
1136 forFieldWithPrefix: @"<http://"];
1139 - (NSString *) mailingListUnsubscribeURL
1141 return [self scanListHeaderValue:
1142 [[self mailHeaders] objectForKey: @"list-unsubscribe"]
1143 forFieldWithPrefix: @"<http://"];
1151 Note: There is one thing which *can* change for an existing message,
1152 those are the IMAP4 flags (and annotations, which we do not use).
1153 Since we don't render the flags, it should be OK, if this changes
1154 we must embed the flagging into the etag.
1159 - (int) zlGenerationCount
1161 return 0; /* mails never change */
1164 /* Outlook mail tagging */
1166 - (NSString *) outlookMessageClass
1170 if ((type = [[self mailHeaders] objectForKey: @"x-kolab-type"]) != nil) {
1171 if ([type isEqualToString: @"application/x-vnd.kolab.contact"])
1172 return @"IPM.Contact";
1173 if ([type isEqualToString: @"application/x-vnd.kolab.task"])
1175 if ([type isEqualToString: @"application/x-vnd.kolab.event"])
1176 return @"IPM.Appointment";
1177 if ([type isEqualToString: @"application/x-vnd.kolab.note"])
1179 if ([type isEqualToString: @"application/x-vnd.kolab.journal"])
1180 return @"IPM.Journal";
1183 return @"IPM.Message"; /* email, default class */
1186 - (NSArray *) aclsForUser: (NSString *) uid
1188 return [container aclsForUser: uid];
1193 - (BOOL) isDebuggingEnabled
1198 @end /* SOGoMailObject */