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/NGBase64Coding.h>
35 #import <NGExtensions/NSNull+misc.h>
36 #import <NGExtensions/NSObject+Logs.h>
37 #import <NGExtensions/NGQuotedPrintableCoding.h>
38 #import <NGExtensions/NSString+Encoding.h>
39 #import <NGImap4/NGImap4Connection.h>
40 #import <NGImap4/NGImap4Envelope.h>
41 #import <NGImap4/NGImap4EnvelopeAddress.h>
42 #import <NGMail/NGMimeMessageParser.h>
44 #import <SoObjects/SOGo/SOGoPermissions.h>
45 #import <SoObjects/SOGo/SOGoUser.h>
46 #import "SOGoMailFolder.h"
47 #import "SOGoMailAccount.h"
48 #import "SOGoMailManager.h"
49 #import "SOGoMailBodyPart.h"
51 #import "SOGoMailObject.h"
53 @implementation SOGoMailObject
55 static NSArray *coreInfoKeys = nil;
56 static NSString *mailETag = nil;
57 static BOOL heavyDebug = NO;
58 static BOOL fetchHeader = YES;
59 static BOOL debugOn = NO;
60 static BOOL debugBodyStructure = NO;
61 static BOOL debugSoParts = NO;
64 return [super version] + 0 /* v1 */;
68 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
70 NSAssert2([super version] == 1,
71 @"invalid superclass (%@) version %i !",
72 NSStringFromClass([self superclass]), [super version]);
74 if ((fetchHeader = ([ud boolForKey:@"SOGoDoNotFetchMailHeader"] ? NO : YES)))
75 NSLog(@"Note: fetching full mail header.");
77 NSLog(@"Note: not fetching full mail header: 'SOGoDoNotFetchMailHeader'");
79 /* Note: see SOGoMailManager.m for allowed IMAP4 keys */
80 /* Note: "BODY" actually returns the structure! */
82 coreInfoKeys = [[NSArray alloc] initWithObjects:
83 @"FLAGS", @"ENVELOPE", @"BODY",
86 // not yet supported: @"INTERNALDATE",
90 coreInfoKeys = [[NSArray alloc] initWithObjects:
91 @"FLAGS", @"ENVELOPE", @"BODY",
93 // not yet supported: @"INTERNALDATE",
97 if (![[ud objectForKey:@"SOGoMailDisableETag"] boolValue]) {
98 mailETag = [[NSString alloc] initWithFormat:@"\"imap4url_%d_%d_%03d\"",
99 UIX_MAILER_MAJOR_VERSION,
100 UIX_MAILER_MINOR_VERSION,
101 UIX_MAILER_SUBMINOR_VERSION];
102 NSLog(@"Note(SOGoMailObject): using constant etag for mail parts: '%@'",
106 NSLog(@"Note(SOGoMailObject): etag caching disabled!");
110 [self->headers release];
111 [self->headerPart release];
112 [self->coreInfos release];
118 - (NSString *)relativeImap4Name {
119 return [[self nameInContainer] stringByDeletingPathExtension];
124 - (SOGoMailObject *)mailObject {
130 - (NSString *)keyExtensionForPart:(id)_partInfo {
133 if (_partInfo == nil)
136 mt = [_partInfo valueForKey:@"type"];
137 st = [[_partInfo valueForKey:@"subtype"] lowercaseString];
138 if ([mt isEqualToString:@"text"]) {
139 if ([st isEqualToString:@"plain"]) return @".txt";
140 if ([st isEqualToString:@"html"]) return @".html";
141 if ([st isEqualToString:@"calendar"]) return @".ics";
142 if ([st isEqualToString:@"x-vcard"]) return @".vcf";
144 else if ([mt isEqualToString:@"image"])
145 return [@"." stringByAppendingString:st];
146 else if ([mt isEqualToString:@"application"]) {
147 if ([st isEqualToString:@"pgp-signature"])
154 - (NSArray *)relationshipKeysWithParts:(BOOL)_withParts {
155 /* should return non-multipart children */
160 parts = [[self bodyStructure] valueForKey:@"parts"];
161 if (![parts isNotNull])
163 if ((count = [parts count]) == 0)
166 for (i = 0, ma = nil; i < count; i++) {
171 part = [parts objectAtIndex:i];
172 hasParts = [part valueForKey:@"parts"] != nil ? YES:NO;
173 if ((hasParts && !_withParts) || (_withParts && !hasParts))
177 ma = [NSMutableArray arrayWithCapacity:count - i];
179 ext = [self keyExtensionForPart:part];
180 key = [[NSString alloc] initWithFormat:@"%d%@", i + 1, ext?ext:@""];
187 - (NSArray *)toOneRelationshipKeys {
188 return [self relationshipKeysWithParts:NO];
190 - (NSArray *)toManyRelationshipKeys {
191 return [self relationshipKeysWithParts:YES];
196 - (id)fetchParts:(NSArray *)_parts {
197 // TODO: explain what it does
199 Called by -fetchPlainTextParts:
201 return [[self imap4Connection] fetchURL:[self imap4URL] parts:_parts];
206 - (BOOL)doesMailExist {
207 static NSArray *existsKey = nil;
210 if (self->coreInfos != nil) /* if we have coreinfos, we can use them */
211 return [self->coreInfos isNotNull];
213 /* otherwise fetch something really simple */
215 if (existsKey == nil) /* we use size, other suggestions? */
216 existsKey = [[NSArray alloc] initWithObjects:@"RFC822.SIZE", nil];
218 msgs = [self fetchParts:existsKey]; // returns dict
219 msgs = [msgs valueForKey:@"fetch"];
220 return [msgs count] > 0 ? YES : NO;
223 - (id)fetchCoreInfos {
226 if (self->coreInfos != nil)
227 return [self->coreInfos isNotNull] ? self->coreInfos : nil;
229 #if 0 // TODO: old code, why was it using clientObject??
230 msgs = [[self clientObject] fetchParts:coreInfoKeys]; // returns dict
232 msgs = [self fetchParts:coreInfoKeys]; // returns dict
234 if (heavyDebug) [self logWithFormat:@"M: %@", msgs];
235 msgs = [msgs valueForKey:@"fetch"];
236 if ([msgs count] == 0)
239 self->coreInfos = [[msgs objectAtIndex:0] retain];
240 return self->coreInfos;
243 - (id)bodyStructure {
246 body = [[self fetchCoreInfos] valueForKey:@"body"];
247 if (debugBodyStructure)
248 [self logWithFormat:@"BODY: %@", body];
252 - (NGImap4Envelope *)envelope {
253 return [[self fetchCoreInfos] valueForKey:@"envelope"];
256 - (NSString *) subject
258 return [[self envelope] subject];
261 - (NSCalendarDate *) date
264 NSCalendarDate *date;
266 userTZ = [[context activeUser] timeZone];
267 date = [[self envelope] date];
268 [date setTimeZone: userTZ];
273 - (NSArray *)fromEnvelopeAddresses {
274 return [[self envelope] from];
276 - (NSArray *)toEnvelopeAddresses {
277 return [[self envelope] to];
279 - (NSArray *)ccEnvelopeAddresses {
280 return [[self envelope] cc];
283 - (NSData *)mailHeaderData {
284 return [[self fetchCoreInfos] valueForKey:@"header"];
286 - (BOOL)hasMailHeaderInCoreInfos {
287 return [[self mailHeaderData] length] > 0 ? YES : NO;
290 - (id)mailHeaderPart {
291 NGMimeMessageParser *parser;
294 if (self->headerPart != nil)
295 return [self->headerPart isNotNull] ? self->headerPart : nil;
297 if ([(data = [self mailHeaderData]) length] == 0)
300 // TODO: do we need to set some delegate method which stops parsing the body?
301 parser = [[NGMimeMessageParser alloc] init];
302 self->headerPart = [[parser parsePartFromData:data] retain];
303 [parser release]; parser = nil;
305 if (self->headerPart == nil) {
306 self->headerPart = [[NSNull null] retain];
309 return self->headerPart;
311 - (NSDictionary *)mailHeaders {
312 if (self->headers == nil)
313 self->headers = [[[self mailHeaderPart] headers] copy];
314 return self->headers;
317 - (id)lookupInfoForBodyPart:(id)_path {
322 if (![_path isNotNull])
325 if ((info = [self bodyStructure]) == nil) {
326 [self errorWithFormat:@"got no body part structure!"];
330 /* ensure array argument */
332 if ([_path isKindOfClass:[NSString class]]) {
333 if ([_path length] == 0)
336 _path = [_path componentsSeparatedByString:@"."];
340 For each path component, eg 1,1,3
342 Remember that we need special processing for message/rfc822 which maps the
343 namespace of multiparts directly into the main namespace.
345 TODO(hh): no I don't remember, please explain in more detail!
347 pe = [_path objectEnumerator];
348 while ((p = [pe nextObject]) != nil && [info isNotNull]) {
353 [self debugWithFormat:@"check PATH: %@", p];
354 idx = [p intValue] - 1;
356 parts = [info valueForKey:@"parts"];
357 mt = [[info valueForKey:@"type"] lowercaseString];
358 if ([mt isEqualToString:@"message"]) {
359 /* we have special behaviour for message types */
362 if ((body = [info valueForKey:@"body"]) != nil) {
363 mt = [body valueForKey:@"type"];
364 if ([mt isEqualToString:@"multipart"])
365 parts = [body valueForKey:@"parts"];
367 parts = [NSArray arrayWithObject:body];
371 if (idx >= [parts count]) {
372 [self errorWithFormat:
373 @"body part index out of bounds(idx=%d vs count=%d): %@",
374 (idx + 1), [parts count], info];
377 info = [parts objectAtIndex:idx];
379 return [info isNotNull] ? info : nil;
384 - (NSData *)content {
386 id result, fullResult;
388 fullResult = [self fetchParts:[NSArray arrayWithObject:@"RFC822"]];
389 if (fullResult == nil)
392 if ([fullResult isKindOfClass:[NSException class]])
395 /* extract fetch result */
397 result = [fullResult valueForKey:@"fetch"];
398 if (![result isKindOfClass:[NSArray class]]) {
400 @"ERROR: unexpected IMAP4 result (missing 'fetch'): %@",
402 return [NSException exceptionWithHTTPStatus:500 /* server error */
403 reason:@"unexpected IMAP4 result"];
405 if ([result count] == 0)
408 result = [result objectAtIndex:0];
410 /* extract message */
412 if ((content = [result valueForKey:@"message"]) == nil) {
414 @"ERROR: unexpected IMAP4 result (missing 'message'): %@",
416 return [NSException exceptionWithHTTPStatus:500 /* server error */
417 reason:@"unexpected IMAP4 result"];
420 return [[content copy] autorelease];
423 - (NSString *) davContentType
425 return @"message/rfc822";
428 - (NSString *) contentAsString
433 if ((content = [self content]) == nil)
435 if ([content isKindOfClass:[NSException class]])
438 s = [[NSString alloc] initWithData: content
439 encoding: NSISOLatin1StringEncoding];
442 @"ERROR: could not convert data of length %d to string",
446 return [s autorelease];
449 /* bulk fetching of plain/text content */
451 - (BOOL)shouldFetchPartOfType:(NSString *)_type subtype:(NSString *)_subtype {
453 This method decides which parts are 'prefetched' for display. Those are
454 usually text parts (the set is currently hardcoded in this method ...).
456 _type = [_type lowercaseString];
457 _subtype = [_subtype lowercaseString];
459 return (([_type isEqualToString:@"text"]
460 && ([_subtype isEqualToString:@"plain"]
461 || [_subtype isEqualToString:@"html"]
462 || [_subtype isEqualToString:@"calendar"]))
463 || ([_type isEqualToString:@"application"]
464 && ([_subtype isEqualToString:@"pgp-signature"]
465 || [_subtype hasPrefix:@"x-vnd.kolab."])));
468 - (void)addRequiredKeysOfStructure:(id)_info path:(NSString *)_p
469 toArray:(NSMutableArray *)_keys
470 recurse:(BOOL)_recurse
473 This is used to collect the set of IMAP4 fetch-keys required to fetch
474 the basic parts of the body structure. That is, to fetch all parts which
475 are displayed 'inline' in a single IMAP4 fetch.
477 The method calls itself recursively to walk the body structure.
484 /* Note: if the part itself doesn't qualify, we still check subparts */
485 fetchPart = [self shouldFetchPartOfType:[_info valueForKey:@"type"]
486 subtype:[_info valueForKey:@"subtype"]];
490 if ([_p length] > 0) {
491 k = [[@"body[" stringByAppendingString:_p] stringByAppendingString:@"]"];
495 for some reason we need to add ".TEXT" for plain text stuff on root
497 TODO: check with HTML
509 parts = [(NSDictionary *)_info objectForKey:@"parts"];
510 for (i = 0, count = [parts count]; i < count; i++) {
514 sp = ([_p length] > 0)
515 ? [_p stringByAppendingFormat:@".%d", i + 1]
516 : [NSString stringWithFormat:@"%d", i + 1];
518 childInfo = [parts objectAtIndex:i];
520 [self addRequiredKeysOfStructure:childInfo path:sp toArray:_keys
526 if ((body = [(NSDictionary *)_info objectForKey:@"body"]) != nil) {
529 sp = [[body valueForKey:@"type"] lowercaseString];
530 if ([sp isEqualToString:@"multipart"])
533 sp = [_p length] > 0 ? [_p stringByAppendingString:@".1"] : @"1";
534 [self addRequiredKeysOfStructure:body path:sp toArray:_keys
539 - (NSArray *)plainTextContentFetchKeys {
541 The name is not 100% correct. The method returns all body structure fetch
542 keys which are marked by the -shouldFetchPartOfType:subtype: method.
546 ma = [NSMutableArray arrayWithCapacity:4];
547 [self addRequiredKeysOfStructure:[[self clientObject] bodyStructure]
548 path:@"" toArray:ma recurse:YES];
552 - (NSDictionary *)fetchPlainTextParts:(NSArray *)_fetchKeys {
553 // TODO: is the name correct or does it also fetch other parts?
554 NSMutableDictionary *flatContents;
558 [self debugWithFormat:@"fetch keys: %@", _fetchKeys];
560 result = [self fetchParts:_fetchKeys];
561 result = [result valueForKey:@"RawResponse"]; // hackish
563 // Note: -valueForKey: doesn't work!
564 result = [(NSDictionary *)result objectForKey:@"fetch"];
566 count = [_fetchKeys count];
567 flatContents = [NSMutableDictionary dictionaryWithCapacity:count];
568 for (i = 0; i < count; i++) {
572 key = [_fetchKeys objectAtIndex:i];
573 data = [(NSDictionary *)[(NSDictionary *)result objectForKey:key]
574 objectForKey:@"data"];
576 if (![data isNotNull]) {
577 [self errorWithFormat:@"got no data for key: %@", key];
581 if ([key isEqualToString:@"body[text]"])
582 key = @""; // see key collector for explanation (TODO: where?)
583 else if ([key hasPrefix:@"body["]) {
586 key = [key substringFromIndex:5];
587 r = [key rangeOfString:@"]"];
589 key = [key substringToIndex:r.location];
591 [flatContents setObject:data forKey:key];
596 - (NSDictionary *)fetchPlainTextParts {
597 return [self fetchPlainTextParts:[self plainTextContentFetchKeys]];
600 /* convert parts to strings */
602 - (NSString *)stringForData:(NSData *)_data partInfo:(NSDictionary *)_info
604 NSString *charset, *encoding, *s;
607 if (![_data isNotNull])
612 encoding = [[_info objectForKey:@"encoding"] lowercaseString];
614 if ([encoding isEqualToString: @"7bit"]
615 || [encoding isEqualToString: @"8bit"])
617 else if ([encoding isEqualToString: @"base64"])
618 mailData = [_data dataByDecodingBase64];
619 else if ([encoding isEqualToString: @"quoted-printable"])
620 mailData = [_data dataByDecodingQuotedPrintable];
622 charset = [[_info valueForKey:@"parameterList"] valueForKey: @"charset"];
623 if (![charset length])
625 s = [[NSString alloc] initWithData:mailData encoding:NSUTF8StringEncoding];
629 s = [NSString stringWithData: mailData
630 usingEncodingNamed: charset];
635 - (NSDictionary *)stringifyTextParts:(NSDictionary *)_datas {
636 NSMutableDictionary *md;
640 md = [NSMutableDictionary dictionaryWithCapacity:4];
641 keys = [_datas keyEnumerator];
642 while ((key = [keys nextObject]) != nil) {
646 info = [self lookupInfoForBodyPart:key];
647 if ((s = [self stringForData:[_datas objectForKey:key] partInfo:info]))
648 [md setObject:s forKey:key];
652 - (NSDictionary *)fetchPlainTextStrings:(NSArray *)_fetchKeys {
654 The fetched parts are NSData objects, this method converts them into
655 NSString objects based on the information inside the bodystructure.
657 The fetch-keys are body fetch-keys like: body[text] or body[1.2.3].
658 The keys in the result dictionary are "" for 'text' and 1.2.3 for parts.
662 if ((datas = [self fetchPlainTextParts:_fetchKeys]) == nil)
664 if ([datas isKindOfClass:[NSException class]])
667 return [self stringifyTextParts:datas];
672 - (NSException *) addFlags: (id) _flags
674 return [[self imap4Connection] addFlags:_flags toURL:[self imap4URL]];
677 - (NSException *) removeFlags: (id) _flags
679 return [[self imap4Connection] removeFlags:_flags toURL:[self imap4URL]];
684 - (BOOL) isDeletionAllowed
689 login = [[context activeUser] login];
690 parentAcl = [[self container] aclsForUser: login];
692 return [parentAcl containsObject: SOGoRole_ObjectEraser];
697 - (id) lookupImap4BodyPartKey: (NSString *) _key
700 // TODO: we might want to check for existence prior controller creation
703 clazz = [SOGoMailBodyPart bodyPartClassForKey:_key inContext:_ctx];
705 return [clazz objectWithName:_key inContainer: self];
708 - (id)lookupName:(NSString *)_key inContext:(id)_ctx acquire:(BOOL)_flag {
711 /* first check attributes directly bound to the application */
712 if ((obj = [super lookupName:_key inContext:_ctx acquire:NO]) != nil)
715 /* lookup body part */
717 if ([self isBodyPartKey:_key inContext:_ctx]) {
718 if ((obj = [self lookupImap4BodyPartKey:_key inContext:_ctx]) != nil) {
720 [self logWithFormat:@"mail looked up part %@: %@", _key, obj];
725 /* return 404 to stop acquisition */
726 return [NSException exceptionWithHTTPStatus:404 /* Not Found */
727 reason:@"Did not find mail method or part-reference!"];
732 - (BOOL)davIsCollection {
733 /* while a mail has child objects, it should appear as a file in WebDAV */
737 - (id)davContentLength {
738 return [[self fetchCoreInfos] valueForKey:@"size"];
741 - (NSDate *)davCreationDate {
742 // TODO: use INTERNALDATE once NGImap4 supports that
745 - (NSDate *)davLastModified {
746 return [self davCreationDate];
749 - (NSException *)davMoveToTargetObject:(id)_target newName:(NSString *)_name
752 [self logWithFormat:@"TODO: should move mail as '%@' to: %@",
754 return [NSException exceptionWithHTTPStatus:501 /* Not Implemented */
755 reason:@"not implemented"];
758 - (NSException *)davCopyToTargetObject:(id)_target newName:(NSString *)_name
762 Note: this is special because we create SOGoMailObject's even if they do
763 not exist (for performance reasons).
765 Also: we cannot really take a target resource, the ID will be assigned by
767 We even cannot return a 'location' header instead because IMAP4
768 doesn't tell us the new ID.
772 destImap4URL = ([_name length] == 0)
773 ? [[_target container] imap4URL]
774 : [_target imap4URL];
776 return [[self mailManager] copyMailURL:[self imap4URL]
777 toFolderURL:destImap4URL
778 password:[self imap4Password]];
783 - (id)GETAction:(id)_ctx {
788 if ((error = [self matchesRequestConditionInContext:_ctx]) != nil) {
789 /* check whether the mail still exists */
790 if (![self doesMailExist]) {
791 return [NSException exceptionWithHTTPStatus:404 /* Not Found */
792 reason:@"mail was deleted"];
794 return error; /* return 304 or 416 */
797 content = [self content];
798 if ([content isKindOfClass:[NSException class]])
800 if (content == nil) {
801 return [NSException exceptionWithHTTPStatus:404 /* Not Found */
802 reason:@"did not find IMAP4 message"];
805 r = [(WOContext *)_ctx response];
806 [r setHeader:@"message/rfc822" forKey:@"content-type"];
807 [r setContent:content];
813 - (NSException *)trashInContext:(id)_ctx {
815 Trashing is three actions:
816 a) copy to trash folder
817 b) mark mail as deleted
820 In case b) or c) fails, we can't do anything because IMAP4 doesn't tell us
821 the ID used in the trash folder.
823 SOGoMailFolder *trashFolder;
826 // TODO: check for safe HTTP method
828 trashFolder = [[self mailAccountFolder] trashFolderInContext:_ctx];
829 if ([trashFolder isKindOfClass:[NSException class]])
830 return (NSException *)trashFolder;
831 if (![trashFolder isNotNull]) {
832 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
833 reason:@"Did not find Trash folder!"];
835 [trashFolder flushMailCaches];
839 error = [self davCopyToTargetObject:trashFolder
840 newName:@"fakeNewUnusedByIMAP4" /* autoassigned */
842 if (error != nil) return error;
844 /* b) mark deleted */
846 error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
847 if (error != nil) return error;
851 error = [[self imap4Connection] expungeAtURL:[[self container] imap4URL]];
852 if (error != nil) return error; // TODO: unflag as deleted?
853 [self flushMailCaches];
858 - (NSException *) moveToFolderNamed: (NSString *) folderName
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 SOGoMailAccounts *destFolder;
871 NSEnumerator *folders;
872 NSString *currentFolderName, *reason;
875 // TODO: check for safe HTTP method
877 destFolder = [self mailAccountsFolder];
878 folders = [[folderName componentsSeparatedByString: @"/"] objectEnumerator];
879 currentFolderName = [folders nextObject];
880 currentFolderName = [folders nextObject];
882 while (currentFolderName)
884 destFolder = [destFolder lookupName: currentFolderName
887 if ([destFolder isKindOfClass: [NSException class]])
888 return (NSException *) destFolder;
889 currentFolderName = [folders nextObject];
892 if (!([destFolder isKindOfClass: [SOGoMailFolder class]]
893 && [destFolder isNotNull]))
895 reason = [NSString stringWithFormat: @"Did not find folder name '%@'!",
897 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
900 [destFolder flushMailCaches];
904 error = [self davCopyToTargetObject: destFolder
905 newName:@"fakeNewUnusedByIMAP4" /* autoassigned */
907 if (error != nil) return error;
909 /* b) mark deleted */
911 error = [[self imap4Connection] markURLDeleted: [self imap4URL]];
912 if (error != nil) return error;
916 error = [[self imap4Connection] expungeAtURL:[[self container] imap4URL]];
917 if (error != nil) return error; // TODO: unflag as deleted?
918 [self flushMailCaches];
923 - (NSException *)delete {
925 Note: delete is different to DELETEAction: for mails! The 'delete' runs
926 either flags a message as deleted or moves it to the Trash while
927 the DELETEAction: really deletes a message (by flagging it as
928 deleted _AND_ performing an expunge).
930 // TODO: copy to Trash folder
933 // TODO: check for safe HTTP method
935 error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
938 - (id)DELETEAction:(id)_ctx {
941 // TODO: ensure safe HTTP method
943 error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
944 if (error != nil) return error;
946 error = [[self imap4Connection] expungeAtURL:[[self container] imap4URL]];
947 if (error != nil) return error; // TODO: unflag as deleted?
949 return [NSNumber numberWithBool:YES]; /* delete was successful */
952 /* some mail classification */
954 - (BOOL)isKolabObject {
957 if ((h = [self mailHeaders]) != nil)
958 return [[h objectForKey:@"x-kolab-type"] isNotEmpty];
960 // TODO: we could check the body structure?
965 - (BOOL)isMailingListMail {
968 if ((h = [self mailHeaders]) == nil)
971 return [[h objectForKey:@"list-id"] isNotEmpty];
974 - (BOOL)isVirusScanned {
977 if ((h = [self mailHeaders]) == nil)
980 if (![[h objectForKey:@"x-virus-status"] isNotEmpty]) return NO;
981 if (![[h objectForKey:@"x-virus-scanned"] isNotEmpty]) return NO;
985 - (NSString *)scanListHeaderValue:(id)_value
986 forFieldWithPrefix:(NSString *)_prefix
988 /* Note: not very tolerant on embedded commands and <> */
989 // TODO: does not really belong here, should be a header-field-parser
992 if (![_value isNotEmpty])
995 if ([_value isKindOfClass:[NSArray class]]) {
999 e = [_value objectEnumerator];
1000 while ((value = [e nextObject]) != nil) {
1001 value = [self scanListHeaderValue:value forFieldWithPrefix:_prefix];
1002 if (value != nil) return value;
1007 if (![_value isKindOfClass:[NSString class]])
1010 /* check for commas in string values */
1011 r = [_value rangeOfString:@","];
1013 return [self scanListHeaderValue:[_value componentsSeparatedByString:@","]
1014 forFieldWithPrefix:_prefix];
1017 /* value qualifies */
1018 if (![(NSString *)_value hasPrefix:_prefix])
1022 if ([_value characterAtIndex:0] == '<') {
1023 r = [_value rangeOfString:@">"];
1024 _value = (r.length == 0)
1025 ? [_value substringFromIndex:1]
1026 : [_value substringWithRange:NSMakeRange(1, r.location - 2)];
1032 - (NSString *)mailingListArchiveURL {
1033 return [self scanListHeaderValue:
1034 [[self mailHeaders] objectForKey:@"list-archive"]
1035 forFieldWithPrefix:@"<http://"];
1037 - (NSString *)mailingListSubscribeURL {
1038 return [self scanListHeaderValue:
1039 [[self mailHeaders] objectForKey:@"list-subscribe"]
1040 forFieldWithPrefix:@"<http://"];
1042 - (NSString *)mailingListUnsubscribeURL {
1043 return [self scanListHeaderValue:
1044 [[self mailHeaders] objectForKey:@"list-unsubscribe"]
1045 forFieldWithPrefix:@"<http://"];
1050 - (id)davEntityTag {
1052 Note: There is one thing which *can* change for an existing message,
1053 those are the IMAP4 flags (and annotations, which we do not use).
1054 Since we don't render the flags, it should be OK, if this changes
1055 we must embed the flagging into the etag.
1059 - (int)zlGenerationCount {
1060 return 0; /* mails never change */
1063 /* Outlook mail tagging */
1065 - (NSString *)outlookMessageClass {
1068 if ((type = [[self mailHeaders] objectForKey:@"x-kolab-type"]) != nil) {
1069 if ([type isEqualToString:@"application/x-vnd.kolab.contact"])
1070 return @"IPM.Contact";
1071 if ([type isEqualToString:@"application/x-vnd.kolab.task"])
1073 if ([type isEqualToString:@"application/x-vnd.kolab.event"])
1074 return @"IPM.Appointment";
1075 if ([type isEqualToString:@"application/x-vnd.kolab.note"])
1077 if ([type isEqualToString:@"application/x-vnd.kolab.journal"])
1078 return @"IPM.Journal";
1081 return @"IPM.Message"; /* email, default class */
1084 - (NSArray *) aclsForUser: (NSString *) uid
1086 return [container aclsForUser: uid];
1091 - (BOOL)isDebuggingEnabled {
1095 @end /* SOGoMailObject */