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>
31 @implementation SOGoMailObject
33 static NSArray *coreInfoKeys = nil;
34 static BOOL heavyDebug = NO;
35 static BOOL debugOn = NO;
36 static BOOL debugBodyStructure = NO;
39 /* Note: see SOGoMailManager.m for allowed IMAP4 keys */
40 /* Note: "BODY" actually returns the structure! */
41 coreInfoKeys = [[NSArray alloc] initWithObjects:
42 @"FLAGS", @"ENVELOPE", @"BODY",
44 // not yet supported: @"INTERNALDATE",
49 [self->coreInfos release];
55 - (NSString *)relativeImap4Name {
56 return [[self nameInContainer] stringByDeletingPathExtension];
61 - (SOGoMailObject *)mailObject {
67 - (NSString *)keyExtensionForPart:(id)_partInfo {
73 mt = [_partInfo valueForKey:@"type"];
74 st = [[_partInfo valueForKey:@"subtype"] lowercaseString];
75 if ([mt isEqualToString:@"text"]) {
76 if ([st isEqualToString:@"plain"])
78 if ([st isEqualToString:@"html"])
80 if ([st isEqualToString:@"calendar"])
83 else if ([mt isEqualToString:@"image"])
84 return [@"." stringByAppendingString:st];
85 else if ([mt isEqualToString:@"application"]) {
86 if ([st isEqualToString:@"pgp-signature"])
93 - (NSArray *)relationshipKeysWithParts:(BOOL)_withParts {
94 /* should return non-multipart children */
99 parts = [[self bodyStructure] valueForKey:@"parts"];
100 if (![parts isNotNull])
102 if ((count = [parts count]) == 0)
105 for (i = 0, ma = nil; i < count; i++) {
110 part = [parts objectAtIndex:i];
111 hasParts = [part valueForKey:@"parts"] != nil ? YES:NO;
112 if ((hasParts && !_withParts) || (_withParts && !hasParts))
116 ma = [NSMutableArray arrayWithCapacity:count - i];
118 ext = [self keyExtensionForPart:part];
119 key = [[NSString alloc] initWithFormat:@"%d%@", i + 1, ext?ext:@""];
126 - (NSArray *)toOneRelationshipKeys {
127 return [self relationshipKeysWithParts:NO];
129 - (NSArray *)toManyRelationshipKeys {
130 return [self relationshipKeysWithParts:YES];
135 - (id)fetchParts:(NSArray *)_parts {
136 return [[self mailManager] fetchURL:[self imap4URL] parts:_parts
137 password:[self imap4Password]];
142 - (id)fetchCoreInfos {
145 if (self->coreInfos != nil)
146 return [self->coreInfos isNotNull] ? self->coreInfos : nil;
148 msgs = [[self clientObject] fetchParts:coreInfoKeys]; // returns dict
149 if (heavyDebug) [self logWithFormat:@"M: %@", msgs];
150 msgs = [msgs valueForKey:@"fetch"];
151 if ([msgs count] == 0)
154 self->coreInfos = [[msgs objectAtIndex:0] retain];
155 return self->coreInfos;
158 - (id)bodyStructure {
161 body = [[self fetchCoreInfos] valueForKey:@"body"];
162 if (debugBodyStructure)
163 [self logWithFormat:@"BODY: %@", body];
167 - (NGImap4Envelope *)envelope {
168 return [[self fetchCoreInfos] valueForKey:@"envelope"];
170 - (NSString *)subject {
171 return [[self envelope] subject];
173 - (NSCalendarDate *)date {
174 return [[self envelope] date];
176 - (NSArray *)fromEnvelopeAddresses {
177 return [[self envelope] from];
179 - (NSArray *)toEnvelopeAddresses {
180 return [[self envelope] to];
182 - (NSArray *)ccEnvelopeAddresses {
183 return [[self envelope] cc];
186 - (id)lookupInfoForBodyPart:(id)_path {
191 if (![_path isNotNull])
194 if ((info = [self bodyStructure]) == nil) {
195 [self errorWithFormat:@"got no body part structure!"];
199 /* ensure array argument */
201 if ([_path isKindOfClass:[NSString class]]) {
202 if ([_path length] == 0)
205 _path = [_path componentsSeparatedByString:@"."];
209 For each path component, eg 1,1,3
211 Remember that we need special processing for message/rfc822 which maps the
212 namespace of multiparts directly into the main namespace.
214 TODO(hh): no I don't remember, please explain in more detail!
216 pe = [_path objectEnumerator];
217 while ((p = [pe nextObject]) != nil && [info isNotNull]) {
222 [self debugWithFormat:@"check PATH: %@", p];
223 idx = [p intValue] - 1;
225 parts = [info valueForKey:@"parts"];
226 mt = [[info valueForKey:@"type"] lowercaseString];
227 if ([mt isEqualToString:@"message"]) {
228 /* we have special behaviour for message types */
231 if ((body = [info valueForKey:@"body"]) != nil) {
232 mt = [body valueForKey:@"type"];
233 if ([mt isEqualToString:@"multipart"])
234 parts = [body valueForKey:@"parts"];
236 parts = [NSArray arrayWithObject:body];
240 if (idx >= [parts count]) {
241 [self errorWithFormat:
242 @"body part index out of bounds(idx=%d vs count=%d): %@",
243 (idx + 1), [parts count], info];
246 info = [parts objectAtIndex:idx];
248 return [info isNotNull] ? info : nil;
253 - (NSData *)content {
255 id result, fullResult;
257 fullResult = [self fetchParts:[NSArray arrayWithObject:@"RFC822"]];
258 if (fullResult == nil)
261 if ([fullResult isKindOfClass:[NSException class]])
264 /* extract fetch result */
266 result = [fullResult valueForKey:@"fetch"];
267 if (![result isKindOfClass:[NSArray class]]) {
269 @"ERROR: unexpected IMAP4 result (missing 'fetch'): %@",
271 return [NSException exceptionWithHTTPStatus:500 /* server error */
272 reason:@"unexpected IMAP4 result"];
274 if ([result count] == 0)
277 result = [result objectAtIndex:0];
279 /* extract message */
281 if ((content = [result valueForKey:@"message"]) == nil) {
283 @"ERROR: unexpected IMAP4 result (missing 'message'): %@",
285 return [NSException exceptionWithHTTPStatus:500 /* server error */
286 reason:@"unexpected IMAP4 result"];
289 return [[content copy] autorelease];
292 - (NSString *)contentAsString {
296 if ((content = [self content]) == nil)
298 if ([content isKindOfClass:[NSException class]])
301 s = [[NSString alloc] initWithData:content
302 encoding:NSISOLatin1StringEncoding];
305 @"ERROR: could not convert data of length %d to string",
309 return [s autorelease];
312 /* bulk fetching of plain/text content */
314 - (BOOL)shouldFetchPartOfType:(NSString *)_type subtype:(NSString *)_subtype {
315 _type = [_type lowercaseString];
316 _subtype = [_subtype lowercaseString];
318 if ([_type isEqualToString:@"text"]) {
319 if ([_subtype isEqualToString:@"plain"])
322 if ([_subtype isEqualToString:@"calendar"]) /* we also fetch calendars */
326 if ([_type isEqualToString:@"application"]) {
327 if ([_subtype isEqualToString:@"pgp-signature"])
334 - (void)addRequiredKeysOfStructure:(id)_info path:(NSString *)_p
335 toArray:(NSMutableArray *)_keys
336 recurse:(BOOL)_recurse
343 fetchPart = [self shouldFetchPartOfType:[_info valueForKey:@"type"]
344 subtype:[_info valueForKey:@"subtype"]];
348 if ([_p length] > 0) {
349 k = [[@"body[" stringByAppendingString:_p] stringByAppendingString:@"]"];
353 for some reason we need to add ".TEXT" for plain text stuff on root
355 TODO: check with HTML
367 parts = [_info objectForKey:@"parts"];
368 for (i = 0, count = [parts count]; i < count; i++) {
373 ? [_p stringByAppendingFormat:@".%d", i + 1]
374 : [NSString stringWithFormat:@"%d", i + 1];
376 childInfo = [parts objectAtIndex:i];
378 [self addRequiredKeysOfStructure:childInfo path:sp toArray:_keys
384 if ((body = [_info objectForKey:@"body"]) != nil) {
387 sp = [[body valueForKey:@"type"] lowercaseString];
388 if ([sp isEqualToString:@"multipart"])
391 sp = [_p length] > 0 ? [_p stringByAppendingString:@".1"] : @"1";
392 [self addRequiredKeysOfStructure:body path:sp toArray:_keys
397 - (NSArray *)plainTextContentFetchKeys {
400 ma = [NSMutableArray arrayWithCapacity:4];
401 [self addRequiredKeysOfStructure:[[self clientObject] bodyStructure]
402 path:@"" toArray:ma recurse:YES];
406 - (NSDictionary *)fetchPlainTextParts:(NSArray *)_fetchKeys {
407 NSMutableDictionary *flatContents;
411 [self debugWithFormat:@"fetch keys: %@", _fetchKeys];
413 result = [self fetchParts:_fetchKeys];
414 result = [result valueForKey:@"RawResponse"]; // hackish
415 result = [result objectForKey:@"fetch"]; // Note: -valueForKey: doesn't work!
417 count = [_fetchKeys count];
418 flatContents = [NSMutableDictionary dictionaryWithCapacity:count];
419 for (i = 0; i < count; i++) {
423 key = [_fetchKeys objectAtIndex:i];
424 data = [[result objectForKey:key] objectForKey:@"data"];
426 if (![data isNotNull]) {
427 [self debugWithFormat:@"got no data fork key: %@", key];
431 if ([key isEqualToString:@"body[text]"])
432 key = @""; // see key collector
433 else if ([key hasPrefix:@"body["]) {
436 key = [key substringFromIndex:5];
437 r = [key rangeOfString:@"]"];
439 key = [key substringToIndex:r.location];
441 [flatContents setObject:data forKey:key];
446 - (NSDictionary *)fetchPlainTextParts {
447 return [self fetchPlainTextParts:[self plainTextContentFetchKeys]];
450 /* convert parts to strings */
452 - (NSString *)stringForData:(NSData *)_data partInfo:(NSDictionary *)_info {
456 if (![_data isNotNull])
461 charset = [[_info valueForKey:@"parameterList"] valueForKey:@"charset"];
462 if ([charset isNotNull] && [charset length] > 0)
463 s = [NSString stringWithData:_data usingEncodingNamed:charset];
465 if (s == nil) { /* no charset provided, fall back to UTF-8 */
466 s = [[NSString alloc] initWithData:_data encoding:NSUTF8StringEncoding];
473 - (NSDictionary *)stringifyTextParts:(NSDictionary *)_datas {
474 NSMutableDictionary *md;
478 md = [NSMutableDictionary dictionaryWithCapacity:4];
479 keys = [_datas keyEnumerator];
480 while ((key = [keys nextObject]) != nil) {
484 info = [self lookupInfoForBodyPart:key];
485 if ((s = [self stringForData:[_datas objectForKey:key] partInfo:info]))
486 [md setObject:s forKey:key];
490 - (NSDictionary *)fetchPlainTextStrings:(NSArray *)_fetchKeys {
492 The fetched parts are NSData objects, this method converts them into
493 NSString objects based on the information inside the bodystructure.
495 The fetch-keys are body fetch-keys like: body[text] or body[1.2.3].
496 The keys in the result dictionary are "" for 'text' and 1.2.3 for parts.
500 if ((datas = [self fetchPlainTextParts:_fetchKeys]) == nil)
502 if ([datas isKindOfClass:[NSException class]])
505 return [self stringifyTextParts:datas];
510 - (NSException *)addFlags:(id)_flags {
511 return [[self mailManager] addFlags:_flags toURL:[self imap4URL]
512 password:[self imap4Password]];
514 - (NSException *)removeFlags:(id)_flags {
515 return [[self mailManager] removeFlags:_flags toURL:[self imap4URL]
516 password:[self imap4Password]];
521 - (BOOL)isDeletionAllowed {
522 return [[self container] isDeleteAndExpungeAllowed];
527 - (id)lookupImap4BodyPartKey:(NSString *)_key inContext:(id)_ctx {
528 // TODO: we might want to check for existence prior controller creation
531 clazz = [SOGoMailBodyPart bodyPartClassForKey:_key inContext:_ctx];
532 return [[[clazz alloc] initWithName:_key inContainer:self] autorelease];
535 - (id)lookupName:(NSString *)_key inContext:(id)_ctx acquire:(BOOL)_flag {
538 /* first check attributes directly bound to the application */
539 if ((obj = [super lookupName:_key inContext:_ctx acquire:NO]) != nil)
542 /* lookup body part */
544 if ([self isBodyPartKey:_key inContext:_ctx]) {
545 if ((obj = [self lookupImap4BodyPartKey:_key inContext:_ctx]) != nil)
549 /* return 404 to stop acquisition */
550 return [NSException exceptionWithHTTPStatus:404 /* Not Found */];
555 - (BOOL)davIsCollection {
556 /* while a mail has child objects, it should appear as a file in WebDAV */
560 - (id)davContentLength {
561 return [[self fetchCoreInfos] valueForKey:@"size"];
564 - (NSDate *)davCreationDate {
565 // TODO: use INTERNALDATE once NGImap4 supports that
568 - (NSDate *)davLastModified {
569 return [self davCreationDate];
572 - (NSException *)davMoveToTargetObject:(id)_target newName:(NSString *)_name
575 [self logWithFormat:@"TODO: should move mail as '%@' to: %@",
577 return [NSException exceptionWithHTTPStatus:501 /* Not Implemented */
578 reason:@"not implemented"];
581 - (NSException *)davCopyToTargetObject:(id)_target newName:(NSString *)_name
585 Note: this is special because we create SOGoMailObject's even if they do
586 not exist (for performance reasons).
588 Also: we cannot really take a target resource, the ID will be assigned by
590 We even cannot return a 'location' header instead because IMAP4
591 doesn't tell us the new ID.
595 destImap4URL = ([_name length] == 0)
596 ? [[_target container] imap4URL]
597 : [_target imap4URL];
599 return [[self mailManager] copyMailURL:[self imap4URL]
600 toFolderURL:destImap4URL
601 password:[self imap4Password]];
606 - (id)GETAction:(WOContext *)_ctx {
610 content = [self content];
611 if ([content isKindOfClass:[NSException class]])
613 if (content == nil) {
614 return [NSException exceptionWithHTTPStatus:404 /* Not Found */
615 reason:@"did not find IMAP4 message"];
619 [r setHeader:@"message/rfc822" forKey:@"content-type"];
620 [r setContent:content];
626 - (NSException *)trashInContext:(id)_ctx {
628 Trashing is three actions:
629 a) copy to trash folder
630 b) mark mail as deleted
633 In case b) or c) fails, we can't do anything because IMAP4 doesn't tell us
634 the ID used in the trash folder.
636 SOGoMailFolder *trashFolder;
639 // TODO: check for safe HTTP method
641 trashFolder = [[self mailAccountFolder] trashFolderInContext:_ctx];
642 if ([trashFolder isKindOfClass:[NSException class]])
643 return (NSException *)trashFolder;
644 if (![trashFolder isNotNull]) {
645 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
646 reason:@"Did not find Trash folder!"];
648 [trashFolder flushMailCaches];
652 error = [self davCopyToTargetObject:trashFolder
653 newName:@"fakeNewUnusedByIMAP4" /* autoassigned */
655 if (error != nil) return error;
657 /* b) mark deleted */
659 error = [[self mailManager] markURLDeleted:[self imap4URL]
660 password:[self imap4Password]];
661 if (error != nil) return error;
665 error = [[self mailManager] expungeAtURL:[[self container] imap4URL]
666 password:[self imap4Password]];
667 if (error != nil) return error; // TODO: unflag as deleted?
668 [self flushMailCaches];
673 - (NSException *)delete {
675 Note: delete is different to DELETEAction: for mails! The 'delete' runs
676 either flags a message as deleted or moves it to the Trash while
677 the DELETEAction: really deletes a message (by flagging it as
678 deleted _AND_ performing an expunge).
680 // TODO: copy to Trash folder
683 // TODO: check for safe HTTP method
685 error = [[self mailManager] markURLDeleted:[self imap4URL]
686 password:[self imap4Password]];
689 - (id)DELETEAction:(id)_ctx {
692 // TODO: ensure safe HTTP method
694 error = [[self mailManager] markURLDeleted:[self imap4URL]
695 password:[self imap4Password]];
696 if (error != nil) return error;
698 error = [[self mailManager] expungeAtURL:[[self container] imap4URL]
699 password:[self imap4Password]];
700 if (error != nil) return error; // TODO: unflag as deleted?
702 return [NSNumber numberWithBool:YES]; /* delete was successful */
707 - (BOOL)isDebuggingEnabled {
711 @end /* SOGoMailObject */