sogo/SoObjects/Mailer/SOGoMailBodyPart.m

508 lines
12 KiB
Objective-C

/*
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 <Foundation/NSArray.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSEnumerator.h>
#import <Foundation/NSString.h>
#import <Foundation/NSURL.h>
#import <NGObjWeb/NSException+HTTP.h>
#import <NGObjWeb/SoObject+SoDAV.h>
#import <NGObjWeb/WOContext.h>
#import <NGObjWeb/WOResponse.h>
#import <NGExtensions/NSNull+misc.h>
#import <NGExtensions/NSObject+Logs.h>
#import <NGExtensions/NSString+misc.h>
#import <NGExtensions/NGBase64Coding.h>
#import <NGExtensions/NGQuotedPrintableCoding.h>
#import <NGImap4/NGImap4Connection.h>
#import <SoObjects/SOGo/NSDictionary+Utilities.h>
#import "NSDictionary+Mail.h"
#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_%@_%@_%@\"",
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 */
if (!imap4URL)
{
imap4URL = [[self mailObject] imap4URL];
[imap4URL retain];
}
return 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
{
[self partInfo];
return [partInfo 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 *) fetchBLOBWithPeek: (BOOL) withPeek
{
// HEADER, HEADER.FIELDS, HEADER.FIELDS.NOT, MIME, TEXT
NSString *enc;
NSData *data;
data = [[self imap4Connection] fetchContentOfBodyPart:
[self bodyPartIdentifier]
atURL:[self imap4URL]
withPeek: withPeek];
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;
}
- (NSData *) fetchBLOB
{
return [self fetchBLOBWithPeek: NO];
}
/* 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 */