/* Copyright (C) 2004-2005 SKYRIX Software AG This file is part of OpenGroupware.org. OGo is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. OGo is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with OGo; see the file COPYING. If not, write to the Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import "SOGoMailObject.h" #import "SOGoMailManager.h" #import "SOGoMailBodyPart.h" @implementation SOGoMailBodyPart static NSString *mailETag = nil; static BOOL debugOn = NO; + (void) initialize { if (!mailETag) { /* The following disabled code should not be needed, except if we use annotations (see davEntityTag below) */ // if (![[ud objectForKey: @"SOGoMailDisableETag"] boolValue]) { mailETag = [[NSString alloc] initWithFormat:@"\"imap4url_%d_%d_%03d\"", UIX_MAILER_MAJOR_VERSION, UIX_MAILER_MINOR_VERSION, UIX_MAILER_SUBMINOR_VERSION]; } } - (id) init { if ((self = [super init])) asAttachment = NO; return self; } - (void) dealloc { [partInfo release]; [identifier release]; [pathToPart release]; [super dealloc]; } - (void) setAsAttachment { asAttachment = YES; } /* hierarchy */ - (SOGoMailObject *) mailObject { return [[self container] mailObject]; } /* IMAP4 */ - (NSString *) bodyPartName { NSString *s; NSRange r; s = [self nameInContainer]; r = [s rangeOfString:@"."]; /* strip extensions */ if (r.length == 0) return s; return [s substringToIndex:r.location]; } - (NSArray *) bodyPartPath { NSMutableArray *p; id obj; if (pathToPart != nil) return ([pathToPart isNotNull] ? (id)pathToPart : nil); #warning partToPart should be populated directly p = [[NSMutableArray alloc] initWithCapacity:8]; for (obj = self; [obj isKindOfClass:[SOGoMailBodyPart class]]; obj = [obj container]) { [p insertObject:[obj bodyPartName] atIndex:0]; } pathToPart = [p copy]; [p release]; return pathToPart; } - (NSString *) bodyPartIdentifier { if (identifier != nil) return ([identifier isNotNull] ? (id)identifier : nil); identifier = [[[self bodyPartPath] componentsJoinedByString:@"."] copy]; return identifier; } - (NSURL *) imap4URL { /* reuse URL of message */ return [[self mailObject] imap4URL]; } /* part info */ - (id) partInfo { if (!partInfo) { partInfo = [[self mailObject] lookupInfoForBodyPart: [self bodyPartPath]]; [partInfo retain]; } return partInfo; } /* name lookup */ - (id) lookupImap4BodyPartKey: (NSString *) key inContext: (WOContext *) localContext { // TODO: we might want to check for existence prior controller creation Class clazz; NSArray *subParts; unsigned int nbr; id obj; NSDictionary *subPart, *infos; nbr = [key intValue]; infos = [self partInfo]; subParts = [infos objectForKey: @"parts"]; if (!subParts) subParts = [[infos objectForKey: @"body"] objectForKey: @"parts"]; if (nbr > 0 && nbr < ([subParts count] + 1)) { subPart = [subParts objectAtIndex: nbr - 1]; clazz = [[self class] bodyPartClassForMimeType: [subPart keysWithFormat: @"%{type}/%{subtype}"] inContext: localContext]; obj = [clazz objectWithName: key inContainer: self]; } else obj = self; return obj; // clazz = [SOGoMailBodyPart bodyPartClassForKey: _key // inContext: _ctx]; // return [clazz objectWithName: _key inContainer: self]; } - (NSString *) filename { NSString *filename; NSDictionary *parameters; [self partInfo]; filename = [[partInfo objectForKey: @"parameterList"] objectForKey: @"name"]; if (!filename) { parameters = [[partInfo objectForKey: @"disposition"] objectForKey: @"parameterList"]; filename = [parameters objectForKey: @"filename"]; } return filename; } /* We overwrite the super's class method in order to make sure we aren't dealing with our actual filename as the _key. That could lead to problems if we weren't doing this as our filename could start with a digit, leading to a wrong assumption in the super class */ // - (BOOL)isBodyPartKey:(NSString *)_key inContext:(id)_ctx // { // NSString *s; // s = [self filename]; // if (s && [s isEqualToString: _key]) return NO; // return [super isBodyPartKey: _key inContext: _ctx]; // } - (id) lookupName: (NSString *) _key inContext: (id) _ctx acquire: (BOOL) _flag { id obj; /* first check attributes directly bound to the application */ obj = [super lookupName:_key inContext:_ctx acquire:NO]; if (!obj) { /* lookup body part */ if ([self isBodyPartKey: _key]) obj = [self lookupImap4BodyPartKey: _key inContext: _ctx]; else if ([_key isEqualToString: @"asAttachment"]) [self setAsAttachment]; /* should check whether such a filename exist in the attached names */ if (!obj) obj = self; } return obj; } /* fetch */ - (NSData *) fetchBLOB { // HEADER, HEADER.FIELDS, HEADER.FIELDS.NOT, MIME, TEXT NSString *enc; NSData *data; data = [[self imap4Connection] fetchContentOfBodyPart: [self bodyPartIdentifier] atURL:[self imap4URL]]; if (data == nil) return nil; /* check for content encodings */ enc = [[self partInfo] valueForKey: @"encoding"]; /* if we haven't found one, check out the main message's encoding as we could be trying to fetch the message's content as a part */ if (!enc) enc = [[[[self mailObject] fetchCoreInfos] valueForKey: @"body"] valueForKey: @"encoding"]; if (enc) { enc = [enc lowercaseString]; if ([enc isEqualToString: @"base64"]) data = [data dataByDecodingBase64]; else if ([enc isEqualToString: @"quoted-printable"]) data = [data dataByDecodingQuotedPrintableTransferEncoding]; else if ([enc isEqualToString: @"7bit"] || [enc isEqualToString: @"8bit"]) ; /* keep data as is */ // TODO: do we need to change encodings? else { data = nil; [self errorWithFormat: @"unsupported encoding: %@", enc]; } } return data; } /* WebDAV */ - (NSString *)contentTypeForBodyPartInfo:(id)_info { NSMutableString *type; NSString *mt, *st; NSDictionary *parameters; NSEnumerator *ke; NSString *pn; if (![_info isNotNull]) return nil; mt = [_info valueForKey:@"type"]; if (![mt isNotNull]) return nil; st = [_info valueForKey:@"subtype"]; if (![st isNotNull]) return nil; type = [NSMutableString stringWithCapacity:16]; [type appendString:[mt lowercaseString]]; [type appendString:@"/"]; [type appendString:[st lowercaseString]]; parameters = [_info valueForKey:@"parameterList"]; ke = [parameters keyEnumerator]; while ((pn = [ke nextObject]) != nil) { [type appendString:@"; "]; [type appendString:pn]; [type appendString:@"=\""]; [type appendString:[[parameters objectForKey:pn] stringValue]]; [type appendString:@"\""]; } return type; } - (NSString *) contentTypeForPathExtension: (NSString *) pe { if ([pe length] == 0) return @"application/octet-stream"; /* TODO: add some map */ if ([pe isEqualToString:@"gif"]) return @"image/gif"; if ([pe isEqualToString:@"png"]) return @"image/png"; if ([pe isEqualToString:@"jpg"]) return @"image/jpeg"; if ([pe isEqualToString:@"txt"]) return @"text/plain"; return @"application/octet-stream"; } - (NSString *) davContentType { // TODO: what about the content-type and other headers? // => we could pass them in as the extension? (eg generate 1.gif!) NSString *parts, *contentType, *extension; /* try type from body structure info */ if (asAttachment) contentType = @"application/octet-stream"; else { parts = [self contentTypeForBodyPartInfo: [self partInfo]]; contentType = [[parts componentsSeparatedByString: @";"] objectAtIndex: 0]; if (![contentType length]) { extension = [[self nameInContainer] pathExtension]; contentType = [self contentTypeForPathExtension: extension]; } } return contentType; } /* actions */ - (id) GETAction: (WOContext *) localContext { NSException *error; NSData *data; NSString *etag, *mimeType, *fileName; id response; error = [self matchesRequestConditionInContext: localContext]; if (error) { response = error; /* return 304 or 416 */ } else { // [self debugWithFormat: @"should fetch body part: %@", // [self bodyPartIdentifier]]; data = [self fetchBLOB]; if (data) { // [self debugWithFormat:@" fetched %d bytes: %@", [data length], // [self partInfo]]; // TODO: wrong, could be encoded response = [localContext response]; mimeType = [self davContentType]; if ([mimeType isEqualToString: @"application/x-xpinstall"]) mimeType = @"application/octet-stream"; [response setHeader: mimeType forKey: @"content-type"]; [response setHeader: [NSString stringWithFormat:@"%d", [data length]] forKey: @"content-length"]; if (asAttachment) { fileName = [self filename]; if ([fileName length]) [response setHeader: [NSString stringWithFormat: @"attachment; filename*=\"utf-8''%@\"", [fileName stringByEscapingURL]] forKey: @"content-disposition"]; } etag = [self davEntityTag]; if (etag) [response setHeader: etag forKey: @"etag"]; [response setContent: data]; } else response = [NSException exceptionWithHTTPStatus: 404 /* not found */ reason: @"did not find body part"]; } return response; } /* factory */ + (Class) bodyPartClassForKey: (NSString *) _key inContext: (id) _ctx { NSString *pe; pe = [_key pathExtension]; if (![pe isNotNull] || [pe length] == 0) return self; /* hard coded for now */ switch ([pe length]) { case 3: if ([pe isEqualToString:@"gif"] || [pe isEqualToString:@"png"] || [pe isEqualToString:@"jpg"]) return NSClassFromString(@"SOGoImageMailBodyPart"); if ([pe isEqualToString:@"ics"]) return NSClassFromString(@"SOGoCalendarMailBodyPart"); if ([pe isEqualToString:@"vcf"]) return NSClassFromString(@"SOGoVCardMailBodyPart"); break; case 4: if ([pe isEqualToString:@"eml"]) return NSClassFromString(@"SOGoMessageMailBodyPart"); break; default: return self; } return self; } + (Class) bodyPartClassForMimeType: (NSString *) mimeType inContext: (id) _ctx { NSString *classString; Class klazz; if ([mimeType isEqualToString: @"image/gif"] || [mimeType isEqualToString: @"image/png"] || [mimeType isEqualToString: @"image/jpg"] || [mimeType isEqualToString: @"image/jpeg"]) classString = @"SOGoImageMailBodyPart"; else if ([mimeType isEqualToString: @"text/calendar"] || [mimeType isEqualToString: @"application/ics"]) classString = @"SOGoCalendarMailBodyPart"; else if ([mimeType isEqualToString: @"text/html"]) classString = @"SOGoHTMLMailBodyPart"; else if ([mimeType isEqualToString: @"text/x-vcard"]) classString = @"SOGoVCardMailBodyPart"; else if ([mimeType isEqualToString: @"message/rfc822"]) classString = @"SOGoMessageMailBodyPart"; else { classString = @"SOGoMailBodyPart"; // NSLog (@"unhandled mime type: '%@'", mimeType); } klazz = NSClassFromString (classString); return klazz; } - (BOOL) isFolderish { return NO; } /* etag support */ - (id)davEntityTag { return mailETag; } /* debugging */ - (BOOL)isDebuggingEnabled { return debugOn; } @end /* SOGoMailBodyPart */