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 <NGExtensions/NSString+misc.h>
40 #import <NGImap4/NGImap4Connection.h>
41 #import <NGImap4/NGImap4Envelope.h>
42 #import <NGImap4/NGImap4EnvelopeAddress.h>
43 #import <NGMail/NGMimeMessageParser.h>
45 #import <SoObjects/SOGo/SOGoPermissions.h>
46 #import <SoObjects/SOGo/SOGoUser.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!");
105 [headerPart release];
112 - (NSString *) relativeImap4Name
114 return [nameInContainer stringByDeletingPathExtension];
117 - (NSMutableString *) imap4URLString
119 NSMutableString *urlString;
122 urlString = [container imap4URLString];
123 imap4Name = [[self relativeImap4Name] stringByEscapingURL];
124 [urlString appendFormat: @"%@", imap4Name];
131 - (SOGoMailObject *)mailObject {
137 - (NSString *)keyExtensionForPart:(id)_partInfo {
140 if (_partInfo == nil)
143 mt = [_partInfo valueForKey: @"type"];
144 st = [[_partInfo valueForKey: @"subtype"] lowercaseString];
145 if ([mt isEqualToString: @"text"]) {
146 if ([st isEqualToString: @"plain"]) return @".txt";
147 if ([st isEqualToString: @"html"]) return @".html";
148 if ([st isEqualToString: @"calendar"]) return @".ics";
149 if ([st isEqualToString: @"x-vcard"]) return @".vcf";
151 else if ([mt isEqualToString: @"image"])
152 return [@"." stringByAppendingString:st];
153 else if ([mt isEqualToString: @"application"]) {
154 if ([st isEqualToString: @"pgp-signature"])
161 - (NSArray *)relationshipKeysWithParts:(BOOL)_withParts {
162 /* should return non-multipart children */
167 parts = [[self bodyStructure] valueForKey: @"parts"];
168 if (![parts isNotNull])
170 if ((count = [parts count]) == 0)
173 for (i = 0, ma = nil; i < count; i++) {
178 part = [parts objectAtIndex:i];
179 hasParts = [part valueForKey: @"parts"] != nil ? YES:NO;
180 if ((hasParts && !_withParts) || (_withParts && !hasParts))
184 ma = [NSMutableArray arrayWithCapacity:count - i];
186 ext = [self keyExtensionForPart:part];
187 key = [[NSString alloc] initWithFormat: @"%d%@", i + 1, ext?ext: @""];
194 - (NSArray *)toOneRelationshipKeys {
195 return [self relationshipKeysWithParts:NO];
197 - (NSArray *)toManyRelationshipKeys {
198 return [self relationshipKeysWithParts:YES];
203 - (id)fetchParts:(NSArray *)_parts {
204 // TODO: explain what it does
206 Called by -fetchPlainTextParts:
208 return [[self imap4Connection] fetchURL: [self imap4URL] parts:_parts];
213 - (BOOL)doesMailExist {
214 static NSArray *existsKey = nil;
217 if (coreInfos != nil) /* if we have coreinfos, we can use them */
218 return [coreInfos isNotNull];
220 /* otherwise fetch something really simple */
222 if (existsKey == nil) /* we use size, other suggestions? */
223 existsKey = [[NSArray alloc] initWithObjects: @"RFC822.SIZE", nil];
225 msgs = [self fetchParts:existsKey]; // returns dict
226 msgs = [msgs valueForKey: @"fetch"];
227 return [msgs count] > 0 ? YES : NO;
230 - (id)fetchCoreInfos {
233 if (coreInfos != nil)
234 return [coreInfos isNotNull] ? coreInfos : nil;
236 #if 0 // TODO: old code, why was it using clientObject??
237 msgs = [[self clientObject] fetchParts:coreInfoKeys]; // returns dict
239 msgs = [self fetchParts:coreInfoKeys]; // returns dict
241 if (heavyDebug) [self logWithFormat: @"M: %@", msgs];
242 msgs = [msgs valueForKey: @"fetch"];
243 if ([msgs count] == 0)
246 coreInfos = [[msgs objectAtIndex:0] retain];
250 - (id)bodyStructure {
253 body = [[self fetchCoreInfos] valueForKey: @"body"];
254 if (debugBodyStructure)
255 [self logWithFormat: @"BODY: %@", body];
259 - (NGImap4Envelope *)envelope {
260 return [[self fetchCoreInfos] valueForKey: @"envelope"];
263 - (NSString *) subject
265 return [[self envelope] subject];
268 - (NSCalendarDate *) date
271 NSCalendarDate *date;
273 userTZ = [[context activeUser] timeZone];
274 date = [[self envelope] date];
275 [date setTimeZone: userTZ];
280 - (NSArray *)fromEnvelopeAddresses {
281 return [[self envelope] from];
283 - (NSArray *)toEnvelopeAddresses {
284 return [[self envelope] to];
286 - (NSArray *)ccEnvelopeAddresses {
287 return [[self envelope] cc];
290 - (NSData *)mailHeaderData {
291 return [[self fetchCoreInfos] valueForKey: @"header"];
293 - (BOOL)hasMailHeaderInCoreInfos {
294 return [[self mailHeaderData] length] > 0 ? YES : NO;
297 - (id)mailHeaderPart {
298 NGMimeMessageParser *parser;
301 if (headerPart != nil)
302 return [headerPart isNotNull] ? headerPart : nil;
304 if ([(data = [self mailHeaderData]) length] == 0)
307 // TODO: do we need to set some delegate method which stops parsing the body?
308 parser = [[NGMimeMessageParser alloc] init];
309 headerPart = [[parser parsePartFromData:data] retain];
310 [parser release]; parser = nil;
312 if (headerPart == nil) {
313 headerPart = [[NSNull null] retain];
319 - (NSDictionary *) mailHeaders
322 headers = [[[self mailHeaderPart] headers] copy];
327 - (id)lookupInfoForBodyPart:(id)_path {
332 if (![_path isNotNull])
335 if ((info = [self bodyStructure]) == nil) {
336 [self errorWithFormat: @"got no body part structure!"];
340 /* ensure array argument */
342 if ([_path isKindOfClass:[NSString class]]) {
343 if ([_path length] == 0)
346 _path = [_path componentsSeparatedByString: @"."];
350 For each path component, eg 1,1,3
352 Remember that we need special processing for message/rfc822 which maps the
353 namespace of multiparts directly into the main namespace.
355 TODO(hh): no I don't remember, please explain in more detail!
357 pe = [_path objectEnumerator];
358 while ((p = [pe nextObject]) != nil && [info isNotNull]) {
363 [self debugWithFormat: @"check PATH: %@", p];
364 idx = [p intValue] - 1;
366 parts = [info valueForKey: @"parts"];
367 mt = [[info valueForKey: @"type"] lowercaseString];
368 if ([mt isEqualToString: @"message"]) {
369 /* we have special behaviour for message types */
372 if ((body = [info valueForKey: @"body"]) != nil) {
373 mt = [body valueForKey: @"type"];
374 if ([mt isEqualToString: @"multipart"])
375 parts = [body valueForKey: @"parts"];
377 parts = [NSArray arrayWithObject:body];
381 if (idx >= [parts count]) {
382 [self errorWithFormat:
383 @"body part index out of bounds(idx=%d vs count=%d): %@",
384 (idx + 1), [parts count], info];
387 info = [parts objectAtIndex:idx];
389 return [info isNotNull] ? info : nil;
394 - (NSData *)content {
396 id result, fullResult;
398 fullResult = [self fetchParts:[NSArray arrayWithObject: @"RFC822"]];
399 if (fullResult == nil)
402 if ([fullResult isKindOfClass:[NSException class]])
405 /* extract fetch result */
407 result = [fullResult valueForKey: @"fetch"];
408 if (![result isKindOfClass:[NSArray class]]) {
410 @"ERROR: unexpected IMAP4 result (missing 'fetch'): %@",
412 return [NSException exceptionWithHTTPStatus:500 /* server error */
413 reason: @"unexpected IMAP4 result"];
415 if ([result count] == 0)
418 result = [result objectAtIndex:0];
420 /* extract message */
422 if ((content = [result valueForKey: @"message"]) == nil) {
424 @"ERROR: unexpected IMAP4 result (missing 'message'): %@",
426 return [NSException exceptionWithHTTPStatus:500 /* server error */
427 reason: @"unexpected IMAP4 result"];
430 return [[content copy] autorelease];
433 - (NSString *) davContentType
435 return @"message/rfc822";
438 - (NSString *) contentAsString
443 if ((content = [self content]) == nil)
445 if ([content isKindOfClass:[NSException class]])
448 s = [[NSString alloc] initWithData: content
449 encoding: NSISOLatin1StringEncoding];
452 @"ERROR: could not convert data of length %d to string",
456 return [s autorelease];
459 /* bulk fetching of plain/text content */
461 - (BOOL)shouldFetchPartOfType:(NSString *)_type subtype:(NSString *)_subtype {
463 This method decides which parts are 'prefetched' for display. Those are
464 usually text parts (the set is currently hardcoded in this method ...).
466 _type = [_type lowercaseString];
467 _subtype = [_subtype lowercaseString];
469 return (([_type isEqualToString: @"text"]
470 && ([_subtype isEqualToString: @"plain"]
471 || [_subtype isEqualToString: @"html"]
472 || [_subtype isEqualToString: @"calendar"]))
473 || ([_type isEqualToString: @"application"]
474 && ([_subtype isEqualToString: @"pgp-signature"]
475 || [_subtype hasPrefix: @"x-vnd.kolab."])));
478 - (void)addRequiredKeysOfStructure:(id)_info path:(NSString *)_p
479 toArray:(NSMutableArray *)_keys
480 recurse:(BOOL)_recurse
483 This is used to collect the set of IMAP4 fetch-keys required to fetch
484 the basic parts of the body structure. That is, to fetch all parts which
485 are displayed 'inline' in a single IMAP4 fetch.
487 The method calls itself recursively to walk the body structure.
494 /* Note: if the part itself doesn't qualify, we still check subparts */
495 fetchPart = [self shouldFetchPartOfType:[_info valueForKey: @"type"]
496 subtype:[_info valueForKey: @"subtype"]];
500 if ([_p length] > 0) {
501 k = [[@"body[" stringByAppendingString:_p] stringByAppendingString: @"]"];
505 for some reason we need to add ".TEXT" for plain text stuff on root
507 TODO: check with HTML
519 parts = [(NSDictionary *)_info objectForKey: @"parts"];
520 for (i = 0, count = [parts count]; i < count; i++) {
524 sp = ([_p length] > 0)
525 ? [_p stringByAppendingFormat: @".%d", i + 1]
526 : [NSString stringWithFormat: @"%d", i + 1];
528 childInfo = [parts objectAtIndex:i];
530 [self addRequiredKeysOfStructure:childInfo path:sp toArray:_keys
536 if ((body = [(NSDictionary *)_info objectForKey: @"body"]) != nil) {
539 sp = [[body valueForKey: @"type"] lowercaseString];
540 if ([sp isEqualToString: @"multipart"])
543 sp = [_p length] > 0 ? [_p stringByAppendingString: @".1"] : @"1";
544 [self addRequiredKeysOfStructure:body path:sp toArray:_keys
549 - (NSArray *)plainTextContentFetchKeys {
551 The name is not 100% correct. The method returns all body structure fetch
552 keys which are marked by the -shouldFetchPartOfType:subtype: method.
556 ma = [NSMutableArray arrayWithCapacity:4];
557 [self addRequiredKeysOfStructure:[[self clientObject] bodyStructure]
558 path: @"" toArray:ma recurse:YES];
562 - (NSDictionary *)fetchPlainTextParts:(NSArray *)_fetchKeys {
563 // TODO: is the name correct or does it also fetch other parts?
564 NSMutableDictionary *flatContents;
568 [self debugWithFormat: @"fetch keys: %@", _fetchKeys];
570 result = [self fetchParts:_fetchKeys];
571 result = [result valueForKey: @"RawResponse"]; // hackish
573 // Note: -valueForKey: doesn't work!
574 result = [(NSDictionary *)result objectForKey: @"fetch"];
576 count = [_fetchKeys count];
577 flatContents = [NSMutableDictionary dictionaryWithCapacity:count];
578 for (i = 0; i < count; i++) {
582 key = [_fetchKeys objectAtIndex:i];
583 data = [(NSDictionary *)[(NSDictionary *)result objectForKey:key]
584 objectForKey: @"data"];
586 if (![data isNotNull]) {
587 [self errorWithFormat: @"got no data for key: %@", key];
591 if ([key isEqualToString: @"body[text]"])
592 key = @""; // see key collector for explanation (TODO: where?)
593 else if ([key hasPrefix: @"body["]) {
596 key = [key substringFromIndex:5];
597 r = [key rangeOfString: @"]"];
599 key = [key substringToIndex:r.location];
601 [flatContents setObject:data forKey:key];
606 - (NSDictionary *)fetchPlainTextParts {
607 return [self fetchPlainTextParts:[self plainTextContentFetchKeys]];
610 /* convert parts to strings */
612 - (NSString *)stringForData:(NSData *)_data partInfo:(NSDictionary *)_info
614 NSString *charset, *encoding, *s;
617 if (![_data isNotNull])
622 encoding = [[_info objectForKey: @"encoding"] lowercaseString];
624 if ([encoding isEqualToString: @"7bit"]
625 || [encoding isEqualToString: @"8bit"])
627 else if ([encoding isEqualToString: @"base64"])
628 mailData = [_data dataByDecodingBase64];
629 else if ([encoding isEqualToString: @"quoted-printable"])
630 mailData = [_data dataByDecodingQuotedPrintable];
632 charset = [[_info valueForKey: @"parameterList"] valueForKey: @"charset"];
633 if (![charset length])
635 s = [[NSString alloc] initWithData:mailData encoding:NSUTF8StringEncoding];
639 s = [NSString stringWithData: mailData
640 usingEncodingNamed: charset];
645 - (NSDictionary *)stringifyTextParts:(NSDictionary *)_datas {
646 NSMutableDictionary *md;
650 md = [NSMutableDictionary dictionaryWithCapacity:4];
651 keys = [_datas keyEnumerator];
652 while ((key = [keys nextObject]) != nil) {
656 info = [self lookupInfoForBodyPart:key];
657 if ((s = [self stringForData:[_datas objectForKey:key] partInfo:info]))
658 [md setObject:s forKey:key];
662 - (NSDictionary *)fetchPlainTextStrings:(NSArray *)_fetchKeys {
664 The fetched parts are NSData objects, this method converts them into
665 NSString objects based on the information inside the bodystructure.
667 The fetch-keys are body fetch-keys like: body[text] or body[1.2.3].
668 The keys in the result dictionary are "" for 'text' and 1.2.3 for parts.
672 if ((datas = [self fetchPlainTextParts:_fetchKeys]) == nil)
674 if ([datas isKindOfClass:[NSException class]])
677 return [self stringifyTextParts:datas];
682 - (NSException *) addFlags: (id) _flags
684 return [[self imap4Connection] addFlags:_flags toURL: [self imap4URL]];
687 - (NSException *) removeFlags: (id) _flags
689 return [[self imap4Connection] removeFlags:_flags toURL: [self imap4URL]];
694 - (BOOL) isDeletionAllowed
699 login = [[context activeUser] login];
700 parentAcl = [[self container] aclsForUser: login];
702 return [parentAcl containsObject: SOGoRole_ObjectEraser];
707 - (id) lookupImap4BodyPartKey: (NSString *) _key
710 // TODO: we might want to check for existence prior controller creation
713 clazz = [SOGoMailBodyPart bodyPartClassForKey:_key inContext:_ctx];
715 return [clazz objectWithName:_key inContainer: self];
718 - (id)lookupName:(NSString *)_key inContext:(id)_ctx acquire:(BOOL)_flag {
721 /* first check attributes directly bound to the application */
722 if ((obj = [super lookupName:_key inContext:_ctx acquire:NO]) != nil)
725 /* lookup body part */
727 if ([self isBodyPartKey:_key inContext:_ctx]) {
728 if ((obj = [self lookupImap4BodyPartKey:_key inContext:_ctx]) != nil) {
730 [self logWithFormat: @"mail looked up part %@: %@", _key, obj];
735 /* return 404 to stop acquisition */
736 return [NSException exceptionWithHTTPStatus:404 /* Not Found */
737 reason: @"Did not find mail method or part-reference!"];
742 - (BOOL)davIsCollection {
743 /* while a mail has child objects, it should appear as a file in WebDAV */
747 - (id)davContentLength {
748 return [[self fetchCoreInfos] valueForKey: @"size"];
751 - (NSDate *)davCreationDate {
752 // TODO: use INTERNALDATE once NGImap4 supports that
755 - (NSDate *)davLastModified {
756 return [self davCreationDate];
759 - (NSException *)davMoveToTargetObject:(id)_target newName:(NSString *)_name
762 [self logWithFormat: @"TODO: should move mail as '%@' to: %@",
764 return [NSException exceptionWithHTTPStatus:501 /* Not Implemented */
765 reason: @"not implemented"];
768 - (NSException *)davCopyToTargetObject:(id)_target newName:(NSString *)_name
772 Note: this is special because we create SOGoMailObject's even if they do
773 not exist (for performance reasons).
775 Also: we cannot really take a target resource, the ID will be assigned by
777 We even cannot return a 'location' header instead because IMAP4
778 doesn't tell us the new ID.
782 destImap4URL = ([_name length] == 0)
783 ? [[_target container] imap4URL]
784 : [_target imap4URL];
786 return [[self mailManager] copyMailURL:[self imap4URL]
787 toFolderURL:destImap4URL
788 password:[self imap4Password]];
793 - (id)GETAction:(id)_ctx {
798 if ((error = [self matchesRequestConditionInContext:_ctx]) != nil) {
799 /* check whether the mail still exists */
800 if (![self doesMailExist]) {
801 return [NSException exceptionWithHTTPStatus:404 /* Not Found */
802 reason: @"mail was deleted"];
804 return error; /* return 304 or 416 */
807 content = [self content];
808 if ([content isKindOfClass:[NSException class]])
810 if (content == nil) {
811 return [NSException exceptionWithHTTPStatus:404 /* Not Found */
812 reason: @"did not find IMAP4 message"];
815 r = [(WOContext *)_ctx response];
816 [r setHeader: @"message/rfc822" forKey: @"content-type"];
817 [r setContent:content];
823 - (NSException *)trashInContext:(id)_ctx {
825 Trashing is three actions:
826 a) copy to trash folder
827 b) mark mail as deleted
830 In case b) or c) fails, we can't do anything because IMAP4 doesn't tell us
831 the ID used in the trash folder.
833 SOGoMailFolder *trashFolder;
836 // TODO: check for safe HTTP method
838 trashFolder = [[self mailAccountFolder] trashFolderInContext:_ctx];
839 if ([trashFolder isKindOfClass:[NSException class]])
840 return (NSException *)trashFolder;
841 if (![trashFolder isNotNull]) {
842 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
843 reason: @"Did not find Trash folder!"];
845 [trashFolder flushMailCaches];
849 error = [self davCopyToTargetObject:trashFolder
850 newName: @"fakeNewUnusedByIMAP4" /* autoassigned */
852 if (error != nil) return error;
854 /* b) mark deleted */
856 error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
857 if (error != nil) return error;
861 error = [[self imap4Connection] expungeAtURL:[[self container] imap4URL]];
862 if (error != nil) return error; // TODO: unflag as deleted?
863 [self flushMailCaches];
868 - (NSException *) moveToFolderNamed: (NSString *) folderName
872 Trashing is three actions:
873 a) copy to trash folder
874 b) mark mail as deleted
877 In case b) or c) fails, we can't do anything because IMAP4 doesn't tell us
878 the ID used in the trash folder.
880 SOGoMailAccounts *destFolder;
881 NSEnumerator *folders;
882 NSString *currentFolderName, *reason;
885 // TODO: check for safe HTTP method
887 destFolder = [self mailAccountsFolder];
888 folders = [[folderName componentsSeparatedByString: @"/"] objectEnumerator];
889 currentFolderName = [folders nextObject];
890 currentFolderName = [folders nextObject];
892 while (currentFolderName)
894 destFolder = [destFolder lookupName: currentFolderName
897 if ([destFolder isKindOfClass: [NSException class]])
898 return (NSException *) destFolder;
899 currentFolderName = [folders nextObject];
902 if (!([destFolder isKindOfClass: [SOGoMailFolder class]]
903 && [destFolder isNotNull]))
905 reason = [NSString stringWithFormat: @"Did not find folder name '%@'!",
907 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
910 [destFolder flushMailCaches];
914 error = [self davCopyToTargetObject: destFolder
915 newName: @"fakeNewUnusedByIMAP4" /* autoassigned */
917 if (error != nil) return error;
919 /* b) mark deleted */
921 error = [[self imap4Connection] markURLDeleted: [self imap4URL]];
922 if (error != nil) return error;
926 error = [[self imap4Connection] expungeAtURL:[[self container] imap4URL]];
927 if (error != nil) return error; // TODO: unflag as deleted?
928 [self flushMailCaches];
933 - (NSException *)delete {
935 Note: delete is different to DELETEAction: for mails! The 'delete' runs
936 either flags a message as deleted or moves it to the Trash while
937 the DELETEAction: really deletes a message (by flagging it as
938 deleted _AND_ performing an expunge).
940 // TODO: copy to Trash folder
943 // TODO: check for safe HTTP method
945 error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
948 - (id)DELETEAction:(id)_ctx {
951 // TODO: ensure safe HTTP method
953 error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
954 if (error != nil) return error;
956 error = [[self imap4Connection] expungeAtURL:[[self container] imap4URL]];
957 if (error != nil) return error; // TODO: unflag as deleted?
959 return [NSNumber numberWithBool:YES]; /* delete was successful */
962 /* some mail classification */
964 - (BOOL)isKolabObject {
967 if ((h = [self mailHeaders]) != nil)
968 return [[h objectForKey: @"x-kolab-type"] isNotEmpty];
970 // TODO: we could check the body structure?
975 - (BOOL)isMailingListMail {
978 if ((h = [self mailHeaders]) == nil)
981 return [[h objectForKey: @"list-id"] isNotEmpty];
984 - (BOOL)isVirusScanned {
987 if ((h = [self mailHeaders]) == nil)
990 if (![[h objectForKey: @"x-virus-status"] isNotEmpty]) return NO;
991 if (![[h objectForKey: @"x-virus-scanned"] isNotEmpty]) return NO;
995 - (NSString *)scanListHeaderValue:(id)_value
996 forFieldWithPrefix:(NSString *)_prefix
998 /* Note: not very tolerant on embedded commands and <> */
999 // TODO: does not really belong here, should be a header-field-parser
1002 if (![_value isNotEmpty])
1005 if ([_value isKindOfClass:[NSArray class]]) {
1009 e = [_value objectEnumerator];
1010 while ((value = [e nextObject]) != nil) {
1011 value = [self scanListHeaderValue:value forFieldWithPrefix:_prefix];
1012 if (value != nil) return value;
1017 if (![_value isKindOfClass:[NSString class]])
1020 /* check for commas in string values */
1021 r = [_value rangeOfString: @","];
1023 return [self scanListHeaderValue:[_value componentsSeparatedByString: @","]
1024 forFieldWithPrefix:_prefix];
1027 /* value qualifies */
1028 if (![(NSString *)_value hasPrefix:_prefix])
1032 if ([_value characterAtIndex:0] == '<') {
1033 r = [_value rangeOfString: @">"];
1034 _value = (r.length == 0)
1035 ? [_value substringFromIndex:1]
1036 : [_value substringWithRange:NSMakeRange(1, r.location - 2)];
1042 - (NSString *)mailingListArchiveURL {
1043 return [self scanListHeaderValue:
1044 [[self mailHeaders] objectForKey: @"list-archive"]
1045 forFieldWithPrefix: @"<http://"];
1047 - (NSString *)mailingListSubscribeURL {
1048 return [self scanListHeaderValue:
1049 [[self mailHeaders] objectForKey: @"list-subscribe"]
1050 forFieldWithPrefix: @"<http://"];
1052 - (NSString *)mailingListUnsubscribeURL {
1053 return [self scanListHeaderValue:
1054 [[self mailHeaders] objectForKey: @"list-unsubscribe"]
1055 forFieldWithPrefix: @"<http://"];
1060 - (id)davEntityTag {
1062 Note: There is one thing which *can* change for an existing message,
1063 those are the IMAP4 flags (and annotations, which we do not use).
1064 Since we don't render the flags, it should be OK, if this changes
1065 we must embed the flagging into the etag.
1069 - (int)zlGenerationCount {
1070 return 0; /* mails never change */
1073 /* Outlook mail tagging */
1075 - (NSString *)outlookMessageClass {
1078 if ((type = [[self mailHeaders] objectForKey: @"x-kolab-type"]) != nil) {
1079 if ([type isEqualToString: @"application/x-vnd.kolab.contact"])
1080 return @"IPM.Contact";
1081 if ([type isEqualToString: @"application/x-vnd.kolab.task"])
1083 if ([type isEqualToString: @"application/x-vnd.kolab.event"])
1084 return @"IPM.Appointment";
1085 if ([type isEqualToString: @"application/x-vnd.kolab.note"])
1087 if ([type isEqualToString: @"application/x-vnd.kolab.journal"])
1088 return @"IPM.Journal";
1091 return @"IPM.Message"; /* email, default class */
1094 - (NSArray *) aclsForUser: (NSString *) uid
1096 return [container aclsForUser: uid];
1101 - (BOOL)isDebuggingEnabled {
1105 @end /* SOGoMailObject */