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 #include "SOGoMailObject.h"
23 #include "SOGoMailFolder.h"
24 #include "SOGoMailAccount.h"
25 #include "SOGoMailManager.h"
26 #include "SOGoMailBodyPart.h"
27 #include <NGImap4/NGImap4Envelope.h>
28 #include <NGImap4/NGImap4EnvelopeAddress.h>
29 #include <NGMail/NGMimeMessageParser.h>
32 @implementation SOGoMailObject
34 static NSArray *coreInfoKeys = nil;
35 static NSString *mailETag = nil;
36 static BOOL heavyDebug = NO;
37 static BOOL fetchHeader = YES;
38 static BOOL debugOn = NO;
39 static BOOL debugBodyStructure = NO;
40 static BOOL debugSoParts = NO;
43 return [super version] + 0 /* v1 */;
47 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
49 NSAssert2([super version] == 1,
50 @"invalid superclass (%@) version %i !",
51 NSStringFromClass([self superclass]), [super version]);
53 if ((fetchHeader = ([ud boolForKey:@"SOGoDoNotFetchMailHeader"] ? NO : YES)))
54 NSLog(@"Note: fetching full mail header.");
56 NSLog(@"Note: not fetching full mail header: 'SOGoDoNotFetchMailHeader'");
58 /* Note: see SOGoMailManager.m for allowed IMAP4 keys */
59 /* Note: "BODY" actually returns the structure! */
61 coreInfoKeys = [[NSArray alloc] initWithObjects:
62 @"FLAGS", @"ENVELOPE", @"BODY",
65 // not yet supported: @"INTERNALDATE",
69 coreInfoKeys = [[NSArray alloc] initWithObjects:
70 @"FLAGS", @"ENVELOPE", @"BODY",
72 // not yet supported: @"INTERNALDATE",
76 if (![[ud objectForKey:@"SOGoMailDisableETag"] boolValue]) {
77 mailETag = [[NSString alloc] initWithFormat:@"\"imap4url_%d_%d_%03d\"",
78 UIX_MAILER_MAJOR_VERSION,
79 UIX_MAILER_MINOR_VERSION,
80 UIX_MAILER_SUBMINOR_VERSION];
81 NSLog(@"Note(SOGoMailObject): using constant etag for mail parts: '%@'",
85 NSLog(@"Note(SOGoMailObject): etag caching disabled!");
89 [self->headers release];
90 [self->headerPart release];
91 [self->coreInfos release];
97 - (NSString *)relativeImap4Name {
98 return [[self nameInContainer] stringByDeletingPathExtension];
103 - (SOGoMailObject *)mailObject {
109 - (NSString *)keyExtensionForPart:(id)_partInfo {
112 if (_partInfo == nil)
115 mt = [_partInfo valueForKey:@"type"];
116 st = [[_partInfo valueForKey:@"subtype"] lowercaseString];
117 if ([mt isEqualToString:@"text"]) {
118 if ([st isEqualToString:@"plain"]) return @".txt";
119 if ([st isEqualToString:@"html"]) return @".html";
120 if ([st isEqualToString:@"calendar"]) return @".ics";
121 if ([st isEqualToString:@"x-vcard"]) return @".vcf";
123 else if ([mt isEqualToString:@"image"])
124 return [@"." stringByAppendingString:st];
125 else if ([mt isEqualToString:@"application"]) {
126 if ([st isEqualToString:@"pgp-signature"])
133 - (NSArray *)relationshipKeysWithParts:(BOOL)_withParts {
134 /* should return non-multipart children */
139 parts = [[self bodyStructure] valueForKey:@"parts"];
140 if (![parts isNotNull])
142 if ((count = [parts count]) == 0)
145 for (i = 0, ma = nil; i < count; i++) {
150 part = [parts objectAtIndex:i];
151 hasParts = [part valueForKey:@"parts"] != nil ? YES:NO;
152 if ((hasParts && !_withParts) || (_withParts && !hasParts))
156 ma = [NSMutableArray arrayWithCapacity:count - i];
158 ext = [self keyExtensionForPart:part];
159 key = [[NSString alloc] initWithFormat:@"%d%@", i + 1, ext?ext:@""];
166 - (NSArray *)toOneRelationshipKeys {
167 return [self relationshipKeysWithParts:NO];
169 - (NSArray *)toManyRelationshipKeys {
170 return [self relationshipKeysWithParts:YES];
175 - (id)fetchParts:(NSArray *)_parts {
176 // TODO: explain what it does
178 Called by -fetchPlainTextParts:
180 return [[self imap4Connection] fetchURL:[self imap4URL] parts:_parts];
185 - (BOOL)doesMailExist {
186 static NSArray *existsKey = nil;
189 if (self->coreInfos != nil) /* if we have coreinfos, we can use them */
190 return [self->coreInfos isNotNull];
192 /* otherwise fetch something really simple */
194 if (existsKey == nil) /* we use size, other suggestions? */
195 existsKey = [[NSArray alloc] initWithObjects:@"RFC822.SIZE", nil];
197 msgs = [self fetchParts:existsKey]; // returns dict
198 msgs = [msgs valueForKey:@"fetch"];
199 return [msgs count] > 0 ? YES : NO;
202 - (id)fetchCoreInfos {
205 if (self->coreInfos != nil)
206 return [self->coreInfos isNotNull] ? self->coreInfos : nil;
208 #if 0 // TODO: old code, why was it using clientObject??
209 msgs = [[self clientObject] fetchParts:coreInfoKeys]; // returns dict
211 msgs = [self fetchParts:coreInfoKeys]; // returns dict
213 if (heavyDebug) [self logWithFormat:@"M: %@", msgs];
214 msgs = [msgs valueForKey:@"fetch"];
215 if ([msgs count] == 0)
218 self->coreInfos = [[msgs objectAtIndex:0] retain];
219 return self->coreInfos;
222 - (id)bodyStructure {
225 body = [[self fetchCoreInfos] valueForKey:@"body"];
226 if (debugBodyStructure)
227 [self logWithFormat:@"BODY: %@", body];
231 - (NGImap4Envelope *)envelope {
232 return [[self fetchCoreInfos] valueForKey:@"envelope"];
234 - (NSString *)subject {
235 return [[self envelope] subject];
237 - (NSCalendarDate *)date {
238 return [[self envelope] date];
240 - (NSArray *)fromEnvelopeAddresses {
241 return [[self envelope] from];
243 - (NSArray *)toEnvelopeAddresses {
244 return [[self envelope] to];
246 - (NSArray *)ccEnvelopeAddresses {
247 return [[self envelope] cc];
250 - (NSData *)mailHeaderData {
251 return [[self fetchCoreInfos] valueForKey:@"header"];
253 - (BOOL)hasMailHeaderInCoreInfos {
254 return [[self mailHeaderData] length] > 0 ? YES : NO;
257 - (id)mailHeaderPart {
258 NGMimeMessageParser *parser;
261 if (self->headerPart != nil)
262 return [self->headerPart isNotNull] ? self->headerPart : nil;
264 if ([(data = [self mailHeaderData]) length] == 0)
267 // TODO: do we need to set some delegate method which stops parsing the body?
268 parser = [[NGMimeMessageParser alloc] init];
269 self->headerPart = [[parser parsePartFromData:data] retain];
270 [parser release]; parser = nil;
272 if (self->headerPart == nil) {
273 self->headerPart = [[NSNull null] retain];
276 return self->headerPart;
278 - (NSDictionary *)mailHeaders {
279 if (self->headers == nil)
280 self->headers = [[[self mailHeaderPart] headers] copy];
281 return self->headers;
284 - (id)lookupInfoForBodyPart:(id)_path {
289 if (![_path isNotNull])
292 if ((info = [self bodyStructure]) == nil) {
293 [self errorWithFormat:@"got no body part structure!"];
297 /* ensure array argument */
299 if ([_path isKindOfClass:[NSString class]]) {
300 if ([_path length] == 0)
303 _path = [_path componentsSeparatedByString:@"."];
307 For each path component, eg 1,1,3
309 Remember that we need special processing for message/rfc822 which maps the
310 namespace of multiparts directly into the main namespace.
312 TODO(hh): no I don't remember, please explain in more detail!
314 pe = [_path objectEnumerator];
315 while ((p = [pe nextObject]) != nil && [info isNotNull]) {
320 [self debugWithFormat:@"check PATH: %@", p];
321 idx = [p intValue] - 1;
323 parts = [info valueForKey:@"parts"];
324 mt = [[info valueForKey:@"type"] lowercaseString];
325 if ([mt isEqualToString:@"message"]) {
326 /* we have special behaviour for message types */
329 if ((body = [info valueForKey:@"body"]) != nil) {
330 mt = [body valueForKey:@"type"];
331 if ([mt isEqualToString:@"multipart"])
332 parts = [body valueForKey:@"parts"];
334 parts = [NSArray arrayWithObject:body];
338 if (idx >= [parts count]) {
339 [self errorWithFormat:
340 @"body part index out of bounds(idx=%d vs count=%d): %@",
341 (idx + 1), [parts count], info];
344 info = [parts objectAtIndex:idx];
346 return [info isNotNull] ? info : nil;
351 - (NSData *)content {
353 id result, fullResult;
355 fullResult = [self fetchParts:[NSArray arrayWithObject:@"RFC822"]];
356 if (fullResult == nil)
359 if ([fullResult isKindOfClass:[NSException class]])
362 /* extract fetch result */
364 result = [fullResult valueForKey:@"fetch"];
365 if (![result isKindOfClass:[NSArray class]]) {
367 @"ERROR: unexpected IMAP4 result (missing 'fetch'): %@",
369 return [NSException exceptionWithHTTPStatus:500 /* server error */
370 reason:@"unexpected IMAP4 result"];
372 if ([result count] == 0)
375 result = [result objectAtIndex:0];
377 /* extract message */
379 if ((content = [result valueForKey:@"message"]) == nil) {
381 @"ERROR: unexpected IMAP4 result (missing 'message'): %@",
383 return [NSException exceptionWithHTTPStatus:500 /* server error */
384 reason:@"unexpected IMAP4 result"];
387 return [[content copy] autorelease];
390 - (NSString *)contentAsString {
394 if ((content = [self content]) == nil)
396 if ([content isKindOfClass:[NSException class]])
399 s = [[NSString alloc] initWithData:content
400 encoding:NSISOLatin1StringEncoding];
403 @"ERROR: could not convert data of length %d to string",
407 return [s autorelease];
410 /* bulk fetching of plain/text content */
412 - (BOOL)shouldFetchPartOfType:(NSString *)_type subtype:(NSString *)_subtype {
414 This method decides which parts are 'prefetched' for display. Those are
415 usually text parts (the set is currently hardcoded in this method ...).
417 _type = [_type lowercaseString];
418 _subtype = [_subtype lowercaseString];
420 if ([_type isEqualToString:@"text"]) {
421 if ([_subtype isEqualToString:@"plain"]
422 || [_subtype isEqualToString:@"html"])
425 if ([_subtype isEqualToString:@"calendar"]) /* we also fetch calendars */
429 if ([_type isEqualToString:@"application"]) {
430 if ([_subtype isEqualToString:@"pgp-signature"])
432 if ([_subtype hasPrefix:@"x-vnd.kolab."])
439 - (void)addRequiredKeysOfStructure:(id)_info path:(NSString *)_p
440 toArray:(NSMutableArray *)_keys
441 recurse:(BOOL)_recurse
444 This is used to collect the set of IMAP4 fetch-keys required to fetch
445 the basic parts of the body structure. That is, to fetch all parts which
446 are displayed 'inline' in a single IMAP4 fetch.
448 The method calls itself recursively to walk the body structure.
455 /* Note: if the part itself doesn't qualify, we still check subparts */
456 fetchPart = [self shouldFetchPartOfType:[_info valueForKey:@"type"]
457 subtype:[_info valueForKey:@"subtype"]];
461 if ([_p length] > 0) {
462 k = [[@"body[" stringByAppendingString:_p] stringByAppendingString:@"]"];
466 for some reason we need to add ".TEXT" for plain text stuff on root
468 TODO: check with HTML
480 parts = [(NSDictionary *)_info objectForKey:@"parts"];
481 for (i = 0, count = [parts count]; i < count; i++) {
485 sp = ([_p length] > 0)
486 ? [_p stringByAppendingFormat:@".%d", i + 1]
487 : [NSString stringWithFormat:@"%d", i + 1];
489 childInfo = [parts objectAtIndex:i];
491 [self addRequiredKeysOfStructure:childInfo path:sp toArray:_keys
497 if ((body = [(NSDictionary *)_info objectForKey:@"body"]) != nil) {
500 sp = [[body valueForKey:@"type"] lowercaseString];
501 if ([sp isEqualToString:@"multipart"])
504 sp = [_p length] > 0 ? [_p stringByAppendingString:@".1"] : @"1";
505 [self addRequiredKeysOfStructure:body path:sp toArray:_keys
510 - (NSArray *)plainTextContentFetchKeys {
512 The name is not 100% correct. The method returns all body structure fetch
513 keys which are marked by the -shouldFetchPartOfType:subtype: method.
517 ma = [NSMutableArray arrayWithCapacity:4];
518 [self addRequiredKeysOfStructure:[[self clientObject] bodyStructure]
519 path:@"" toArray:ma recurse:YES];
523 - (NSDictionary *)fetchPlainTextParts:(NSArray *)_fetchKeys {
524 // TODO: is the name correct or does it also fetch other parts?
525 NSMutableDictionary *flatContents;
529 [self debugWithFormat:@"fetch keys: %@", _fetchKeys];
531 result = [self fetchParts:_fetchKeys];
532 result = [result valueForKey:@"RawResponse"]; // hackish
534 // Note: -valueForKey: doesn't work!
535 result = [(NSDictionary *)result objectForKey:@"fetch"];
537 count = [_fetchKeys count];
538 flatContents = [NSMutableDictionary dictionaryWithCapacity:count];
539 for (i = 0; i < count; i++) {
543 key = [_fetchKeys objectAtIndex:i];
544 data = [(NSDictionary *)[(NSDictionary *)result objectForKey:key]
545 objectForKey:@"data"];
547 if (![data isNotNull]) {
548 [self errorWithFormat:@"got no data for key: %@", key];
552 if ([key isEqualToString:@"body[text]"])
553 key = @""; // see key collector for explanation (TODO: where?)
554 else if ([key hasPrefix:@"body["]) {
557 key = [key substringFromIndex:5];
558 r = [key rangeOfString:@"]"];
560 key = [key substringToIndex:r.location];
562 [flatContents setObject:data forKey:key];
567 - (NSDictionary *)fetchPlainTextParts {
568 return [self fetchPlainTextParts:[self plainTextContentFetchKeys]];
571 /* convert parts to strings */
573 - (NSString *)stringForData:(NSData *)_data partInfo:(NSDictionary *)_info
575 NSString *charset, *encoding, *s;
578 if (![_data isNotNull])
583 encoding = [[_info objectForKey:@"encoding"] lowercaseString];
585 if ([encoding isEqualToString: @"7bit"]
586 || [encoding isEqualToString: @"8bit"])
588 else if ([encoding isEqualToString: @"base64"])
589 mailData = [_data dataByDecodingBase64];
590 else if ([encoding isEqualToString: @"quoted-printable"])
591 mailData = [_data dataByDecodingQuotedPrintable];
593 charset = [[_info valueForKey:@"parameterList"] valueForKey: @"charset"];
594 if (![charset length])
596 s = [[NSString alloc] initWithData:mailData encoding:NSUTF8StringEncoding];
600 s = [NSString stringWithData: mailData
601 usingEncodingNamed: charset];
606 - (NSDictionary *)stringifyTextParts:(NSDictionary *)_datas {
607 NSMutableDictionary *md;
611 md = [NSMutableDictionary dictionaryWithCapacity:4];
612 keys = [_datas keyEnumerator];
613 while ((key = [keys nextObject]) != nil) {
617 info = [self lookupInfoForBodyPart:key];
618 if ((s = [self stringForData:[_datas objectForKey:key] partInfo:info]))
619 [md setObject:s forKey:key];
623 - (NSDictionary *)fetchPlainTextStrings:(NSArray *)_fetchKeys {
625 The fetched parts are NSData objects, this method converts them into
626 NSString objects based on the information inside the bodystructure.
628 The fetch-keys are body fetch-keys like: body[text] or body[1.2.3].
629 The keys in the result dictionary are "" for 'text' and 1.2.3 for parts.
633 if ((datas = [self fetchPlainTextParts:_fetchKeys]) == nil)
635 if ([datas isKindOfClass:[NSException class]])
638 return [self stringifyTextParts:datas];
643 - (NSException *)addFlags:(id)_flags {
644 return [[self imap4Connection] addFlags:_flags toURL:[self imap4URL]];
646 - (NSException *)removeFlags:(id)_flags {
647 return [[self imap4Connection] removeFlags:_flags toURL:[self imap4URL]];
652 - (BOOL)isDeletionAllowed {
653 return [[self container] isDeleteAndExpungeAllowed];
658 - (id)lookupImap4BodyPartKey:(NSString *)_key inContext:(id)_ctx {
659 // TODO: we might want to check for existence prior controller creation
662 clazz = [SOGoMailBodyPart bodyPartClassForKey:_key inContext:_ctx];
663 return [[[clazz alloc] initWithName:_key inContainer:self] autorelease];
666 - (id)lookupName:(NSString *)_key inContext:(id)_ctx acquire:(BOOL)_flag {
669 /* first check attributes directly bound to the application */
670 if ((obj = [super lookupName:_key inContext:_ctx acquire:NO]) != nil)
673 /* lookup body part */
675 if ([self isBodyPartKey:_key inContext:_ctx]) {
676 if ((obj = [self lookupImap4BodyPartKey:_key inContext:_ctx]) != nil) {
678 [self logWithFormat:@"mail looked up part %@: %@", _key, obj];
683 /* return 404 to stop acquisition */
684 return [NSException exceptionWithHTTPStatus:404 /* Not Found */
685 reason:@"Did not find mail method or part-reference!"];
690 - (BOOL)davIsCollection {
691 /* while a mail has child objects, it should appear as a file in WebDAV */
695 - (id)davContentLength {
696 return [[self fetchCoreInfos] valueForKey:@"size"];
699 - (NSDate *)davCreationDate {
700 // TODO: use INTERNALDATE once NGImap4 supports that
703 - (NSDate *)davLastModified {
704 return [self davCreationDate];
707 - (NSException *)davMoveToTargetObject:(id)_target newName:(NSString *)_name
710 [self logWithFormat:@"TODO: should move mail as '%@' to: %@",
712 return [NSException exceptionWithHTTPStatus:501 /* Not Implemented */
713 reason:@"not implemented"];
716 - (NSException *)davCopyToTargetObject:(id)_target newName:(NSString *)_name
720 Note: this is special because we create SOGoMailObject's even if they do
721 not exist (for performance reasons).
723 Also: we cannot really take a target resource, the ID will be assigned by
725 We even cannot return a 'location' header instead because IMAP4
726 doesn't tell us the new ID.
730 destImap4URL = ([_name length] == 0)
731 ? [[_target container] imap4URL]
732 : [_target imap4URL];
734 return [[self mailManager] copyMailURL:[self imap4URL]
735 toFolderURL:destImap4URL
736 password:[self imap4Password]];
741 - (id)GETAction:(id)_ctx {
746 if ((error = [self matchesRequestConditionInContext:_ctx]) != nil) {
747 /* check whether the mail still exists */
748 if (![self doesMailExist]) {
749 return [NSException exceptionWithHTTPStatus:404 /* Not Found */
750 reason:@"mail was deleted"];
752 return error; /* return 304 or 416 */
755 content = [self content];
756 if ([content isKindOfClass:[NSException class]])
758 if (content == nil) {
759 return [NSException exceptionWithHTTPStatus:404 /* Not Found */
760 reason:@"did not find IMAP4 message"];
763 r = [(WOContext *)_ctx response];
764 [r setHeader:@"message/rfc822" forKey:@"content-type"];
765 [r setContent:content];
771 - (NSException *)trashInContext:(id)_ctx {
773 Trashing is three actions:
774 a) copy to trash folder
775 b) mark mail as deleted
778 In case b) or c) fails, we can't do anything because IMAP4 doesn't tell us
779 the ID used in the trash folder.
781 SOGoMailFolder *trashFolder;
784 // TODO: check for safe HTTP method
786 trashFolder = [[self mailAccountFolder] trashFolderInContext:_ctx];
787 if ([trashFolder isKindOfClass:[NSException class]])
788 return (NSException *)trashFolder;
789 if (![trashFolder isNotNull]) {
790 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
791 reason:@"Did not find Trash folder!"];
793 [trashFolder flushMailCaches];
797 error = [self davCopyToTargetObject:trashFolder
798 newName:@"fakeNewUnusedByIMAP4" /* autoassigned */
800 if (error != nil) return error;
802 /* b) mark deleted */
804 error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
805 if (error != nil) return error;
809 error = [[self imap4Connection] expungeAtURL:[[self container] imap4URL]];
810 if (error != nil) return error; // TODO: unflag as deleted?
811 [self flushMailCaches];
816 - (NSException *) moveToFolderNamed: (NSString *) folderName
820 Trashing is three actions:
821 a) copy to trash folder
822 b) mark mail as deleted
825 In case b) or c) fails, we can't do anything because IMAP4 doesn't tell us
826 the ID used in the trash folder.
828 SOGoMailFolder *destFolder;
829 NSEnumerator *folders;
830 NSString *currentFolderName, *reason;
833 // TODO: check for safe HTTP method
835 destFolder = [self mailAccountsFolder];
836 folders = [[folderName componentsSeparatedByString: @"/"] objectEnumerator];
837 currentFolderName = [folders nextObject];
838 currentFolderName = [folders nextObject];
840 while (currentFolderName)
842 destFolder = [destFolder lookupName: currentFolderName
845 if ([destFolder isKindOfClass: [NSException class]])
846 return (NSException *) destFolder;
847 currentFolderName = [folders nextObject];
850 if (!([destFolder isKindOfClass: [SOGoMailFolder class]]
851 && [destFolder isNotNull]))
853 reason = [NSString stringWithFormat: @"Did not find folder name '%@'!",
855 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
858 [destFolder flushMailCaches];
862 error = [self davCopyToTargetObject: destFolder
863 newName:@"fakeNewUnusedByIMAP4" /* autoassigned */
865 if (error != nil) return error;
867 /* b) mark deleted */
869 error = [[self imap4Connection] markURLDeleted: [self imap4URL]];
870 if (error != nil) return error;
874 error = [[self imap4Connection] expungeAtURL:[[self container] imap4URL]];
875 if (error != nil) return error; // TODO: unflag as deleted?
876 [self flushMailCaches];
881 - (NSException *)delete {
883 Note: delete is different to DELETEAction: for mails! The 'delete' runs
884 either flags a message as deleted or moves it to the Trash while
885 the DELETEAction: really deletes a message (by flagging it as
886 deleted _AND_ performing an expunge).
888 // TODO: copy to Trash folder
891 // TODO: check for safe HTTP method
893 error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
896 - (id)DELETEAction:(id)_ctx {
899 // TODO: ensure safe HTTP method
901 error = [[self imap4Connection] markURLDeleted:[self imap4URL]];
902 if (error != nil) return error;
904 error = [[self imap4Connection] expungeAtURL:[[self container] imap4URL]];
905 if (error != nil) return error; // TODO: unflag as deleted?
907 return [NSNumber numberWithBool:YES]; /* delete was successful */
910 /* some mail classification */
912 - (BOOL)isKolabObject {
915 if ((h = [self mailHeaders]) != nil)
916 return [[h objectForKey:@"x-kolab-type"] isNotEmpty];
918 // TODO: we could check the body structure?
923 - (BOOL)isMailingListMail {
926 if ((h = [self mailHeaders]) == nil)
929 return [[h objectForKey:@"list-id"] isNotEmpty];
932 - (BOOL)isVirusScanned {
935 if ((h = [self mailHeaders]) == nil)
938 if (![[h objectForKey:@"x-virus-status"] isNotEmpty]) return NO;
939 if (![[h objectForKey:@"x-virus-scanned"] isNotEmpty]) return NO;
943 - (NSString *)scanListHeaderValue:(id)_value
944 forFieldWithPrefix:(NSString *)_prefix
946 /* Note: not very tolerant on embedded commands and <> */
947 // TODO: does not really belong here, should be a header-field-parser
950 if (![_value isNotEmpty])
953 if ([_value isKindOfClass:[NSArray class]]) {
957 e = [_value objectEnumerator];
958 while ((value = [e nextObject]) != nil) {
959 value = [self scanListHeaderValue:value forFieldWithPrefix:_prefix];
960 if (value != nil) return value;
965 if (![_value isKindOfClass:[NSString class]])
968 /* check for commas in string values */
969 r = [_value rangeOfString:@","];
971 return [self scanListHeaderValue:[_value componentsSeparatedByString:@","]
972 forFieldWithPrefix:_prefix];
975 /* value qualifies */
976 if (![(NSString *)_value hasPrefix:_prefix])
980 if ([_value characterAtIndex:0] == '<') {
981 r = [_value rangeOfString:@">"];
982 _value = (r.length == 0)
983 ? [_value substringFromIndex:1]
984 : [_value substringWithRange:NSMakeRange(1, r.location - 2)];
990 - (NSString *)mailingListArchiveURL {
991 return [self scanListHeaderValue:
992 [[self mailHeaders] objectForKey:@"list-archive"]
993 forFieldWithPrefix:@"<http://"];
995 - (NSString *)mailingListSubscribeURL {
996 return [self scanListHeaderValue:
997 [[self mailHeaders] objectForKey:@"list-subscribe"]
998 forFieldWithPrefix:@"<http://"];
1000 - (NSString *)mailingListUnsubscribeURL {
1001 return [self scanListHeaderValue:
1002 [[self mailHeaders] objectForKey:@"list-unsubscribe"]
1003 forFieldWithPrefix:@"<http://"];
1008 - (id)davEntityTag {
1010 Note: There is one thing which *can* change for an existing message,
1011 those are the IMAP4 flags (and annotations, which we do not use).
1012 Since we don't render the flags, it should be OK, if this changes
1013 we must embed the flagging into the etag.
1017 - (int)zlGenerationCount {
1018 return 0; /* mails never change */
1021 /* Outlook mail tagging */
1023 - (NSString *)outlookMessageClass {
1026 if ((type = [[self mailHeaders] objectForKey:@"x-kolab-type"]) != nil) {
1027 if ([type isEqualToString:@"application/x-vnd.kolab.contact"])
1028 return @"IPM.Contact";
1029 if ([type isEqualToString:@"application/x-vnd.kolab.task"])
1031 if ([type isEqualToString:@"application/x-vnd.kolab.event"])
1032 return @"IPM.Appointment";
1033 if ([type isEqualToString:@"application/x-vnd.kolab.note"])
1035 if ([type isEqualToString:@"application/x-vnd.kolab.journal"])
1036 return @"IPM.Journal";
1039 return @"IPM.Message"; /* email, default class */
1044 - (BOOL)isDebuggingEnabled {
1048 @end /* SOGoMailObject */