/* 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. */ #include "SOGoDraftObject.h" #include #include #include #include #include #include #include #include #include #include #include "common.h" @implementation SOGoDraftObject static NGMimeType *TextPlainType = nil; static NGMimeType *MultiMixedType = nil; static NSString *userAgent = @"SOGoMail 1.0"; static BOOL draftDeleteDisabled = NO; // for debugging static BOOL debugOn = NO; + (void)initialize { NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; if ((draftDeleteDisabled = [ud boolForKey:@"SOGoNoDraftDeleteAfterSend"])) NSLog(@"WARNING: draft delete is disabled! (SOGoNoDraftDeleteAfterSend)"); TextPlainType = [[NGMimeType mimeType:@"text" subType:@"plain"] copy]; MultiMixedType = [[NGMimeType mimeType:@"multipart" subType:@"mixed"] copy]; } - (void)dealloc { [self->envelope release]; [self->info release]; [self->path release]; [super dealloc]; } /* draft folder functionality */ - (NSFileManager *)spoolFileManager { return [[self container] spoolFileManager]; } - (NSString *)userSpoolFolderPath { return [[self container] userSpoolFolderPath]; } - (BOOL)_ensureUserSpoolFolderPath { return [[self container] _ensureUserSpoolFolderPath]; } /* draft object functionality */ - (NSString *)draftFolderPath { if (self->path != nil) return self->path; self->path = [[[self userSpoolFolderPath] stringByAppendingPathComponent: [self nameInContainer]] copy]; return self->path; } - (BOOL)_ensureDraftFolderPath { NSFileManager *fm; if (![self _ensureUserSpoolFolderPath]) return NO; if ((fm = [self spoolFileManager]) == nil) { [self errorWithFormat:@"missing spool file manager!"]; return NO; } return [fm createDirectoriesAtPath:[self draftFolderPath] attributes:nil]; } - (NSString *)infoPath { return [[self draftFolderPath] stringByAppendingPathComponent:@".info.plist"]; } /* contents */ - (NSException *)storeInfo:(NSDictionary *)_info { if (_info == nil) { return [NSException exceptionWithHTTPStatus:500 /* server error */ reason:@"got no info to write for draft!"]; } if (![self _ensureDraftFolderPath]) { [self errorWithFormat:@"could not create folder for draft: '%@'", [self draftFolderPath]]; return [NSException exceptionWithHTTPStatus:500 /* server error */ reason:@"could not create folder for draft!"]; } if (![_info writeToFile:[self infoPath] atomically:YES]) { [self errorWithFormat:@"could not write info: '%@'", [self infoPath]]; return [NSException exceptionWithHTTPStatus:500 /* server error */ reason:@"could not write draft info!"]; } /* reset info cache */ [self->info release]; self->info = nil; return nil /* everything is excellent */; } - (NSDictionary *)fetchInfo { NSString *p; if (self->info != nil) return self->info; p = [self infoPath]; if (![[self spoolFileManager] fileExistsAtPath:p]) { [self debugWithFormat:@"Note: info object does not yet exist: %@", p]; return nil; } self->info = [[NSDictionary alloc] initWithContentsOfFile:p]; if (self->info == nil) [self errorWithFormat:@"draft info dictionary broken at path: %@", p]; return self->info; } /* accessors */ - (NSString *)sender { id tmp; if ((tmp = [[self fetchInfo] objectForKey:@"from"]) == nil) return nil; if ([tmp isKindOfClass:[NSArray class]]) return [tmp count] > 0 ? [tmp objectAtIndex:0] : nil; return tmp; } /* attachments */ - (NSArray *)fetchAttachmentNames { NSMutableArray *ma; NSFileManager *fm; NSArray *files; unsigned i, count; fm = [self spoolFileManager]; if ((files = [fm directoryContentsAtPath:[self draftFolderPath]]) == nil) return nil; count = [files count]; ma = [NSMutableArray arrayWithCapacity:count]; for (i = 0; i < count; i++) { NSString *filename; filename = [files objectAtIndex:i]; if ([filename hasPrefix:@"."]) continue; [ma addObject:filename]; } return ma; } - (BOOL)isValidAttachmentName:(NSString *)_name { static NSString *sescape[] = { @"/", @"..", @"~", @"\"", @"'", @" ", nil }; unsigned i; NSRange r; if (![_name isNotNull]) return NO; if ([_name length] == 0) return NO; if ([_name hasPrefix:@"."]) return NO; for (i = 0; sescape[i] != nil; i++) { r = [_name rangeOfString:sescape[i]]; if (r.length > 0) return NO; } return YES; } - (NSString *)pathToAttachmentWithName:(NSString *)_name { if ([_name length] == 0) return nil; return [[self draftFolderPath] stringByAppendingPathComponent:_name]; } - (NSException *)invalidAttachmentNameError:(NSString *)_name { return [NSException exceptionWithHTTPStatus:400 /* Bad Request */ reason:@"Invalid attachment name!"]; } - (NSException *)saveAttachment:(NSData *)_attach withName:(NSString *)_name { NSString *p; if (![_attach isNotNull]) { return [NSException exceptionWithHTTPStatus:400 /* Bad Request */ reason:@"Missing attachment content!"]; } if (![self _ensureDraftFolderPath]) { return [NSException exceptionWithHTTPStatus:500 /* Server Error */ reason:@"Could not create folder for draft!"]; } if (![self isValidAttachmentName:_name]) return [self invalidAttachmentNameError:_name]; p = [self pathToAttachmentWithName:_name]; if (![_attach writeToFile:p atomically:YES]) { return [NSException exceptionWithHTTPStatus:500 /* Server Error */ reason:@"Could not write attachment to draft!"]; } return nil; /* everything OK */ } - (NSException *)deleteAttachmentWithName:(NSString *)_name { NSFileManager *fm; NSString *p; if (![self isValidAttachmentName:_name]) return [self invalidAttachmentNameError:_name]; fm = [self spoolFileManager]; p = [self pathToAttachmentWithName:_name]; if (![fm fileExistsAtPath:p]) return nil; /* well, doesn't exist, so its deleted ;-) */ if (![fm removeFileAtPath:p handler:nil]) { [self logWithFormat:@"ERROR: failed to delete file: %@", p]; return [NSException exceptionWithHTTPStatus:500 /* Server Error */ reason:@"Could not delete attachment from draft!"]; } return nil; /* everything OK */ } /* NGMime representations */ - (NGMimeBodyPart *)bodyPartForText { NGMutableHashMap *map; NGMimeBodyPart *bodyPart; NSDictionary *lInfo; id body; if ((lInfo = [self fetchInfo]) == nil) return nil; /* prepare header of body part */ map = [[[NGMutableHashMap alloc] initWithCapacity:2] autorelease]; // TODO: set charset in header! [map setObject:@"text/plain" forKey:@"content-type"]; if ((body = [lInfo objectForKey:@"text"]) != nil) { if ([body isKindOfClass:[NSString class]]) { [map setObject:@"text/plain; charset=utf-8" forKey:@"content-type"]; body = [body dataUsingEncoding:NSUTF8StringEncoding]; } } /* prepare body content */ bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease]; [bodyPart setBody:body]; return bodyPart; } - (NGMimeMessage *)mimeMessageForContentWithHeaderMap:(NGMutableHashMap *)map { NSDictionary *lInfo; NGMimeMessage *message; id body; if ((lInfo = [self fetchInfo]) == nil) return nil; [map setObject:@"text/plain" forKey:@"content-type"]; if ((body = [lInfo objectForKey:@"text"]) != nil) { if ([body isKindOfClass:[NSString class]]) { /* Note: just 'utf8' is displayed wrong in Mail.app */ [map setObject:@"text/plain; charset=utf-8" forKey:@"content-type"]; body = [body dataUsingEncoding:NSUTF8StringEncoding]; } } message = [[[NGMimeMessage alloc] initWithHeader:map] autorelease]; [message setBody:body]; return message; } - (NSString *)mimeTypeForExtension:(NSString *)_ext { // TODO: make configurable // TODO: use /etc/mime-types if ([_ext isEqualToString:@"txt"]) return @"text/plain"; if ([_ext isEqualToString:@"html"]) return @"text/html"; if ([_ext isEqualToString:@"htm"]) return @"text/html"; if ([_ext isEqualToString:@"gif"]) return @"image/gif"; if ([_ext isEqualToString:@"jpg"]) return @"image/jpeg"; if ([_ext isEqualToString:@"jpeg"]) return @"image/jpeg"; if ([_ext isEqualToString:@"mail"]) return @"message/rfc822"; return @"application/octet-stream"; } - (NSString *)contentTypeForAttachmentWithName:(NSString *)_name { NSString *s; s = [self mimeTypeForExtension:[_name pathExtension]]; if ([_name length] > 0) { s = [s stringByAppendingString:@"; name=\""]; s = [s stringByAppendingString:_name]; s = [s stringByAppendingString:@"\""]; } return s; } - (NSString *)contentDispositionForAttachmentWithName:(NSString *)_name { NSString *type; NSString *cdtype; NSString *cd; type = [self contentTypeForAttachmentWithName:_name]; if ([type hasPrefix:@"text/"]) cdtype = @"inline"; else if ([type hasPrefix:@"image/"] || [type hasPrefix:@"message"]) cdtype = @"inline"; else cdtype = @"attachment"; cd = [cdtype stringByAppendingString:@"; filename=\""]; cd = [cd stringByAppendingString:_name]; cd = [cd stringByAppendingString:@"\""]; // TODO: add size parameter (useful addition, RFC 2183) return cd; } - (NGMimeBodyPart *)bodyPartForAttachmentWithName:(NSString *)_name { NSFileManager *fm; NGMutableHashMap *map; NGMimeBodyPart *bodyPart; NSString *s; NSData *content; BOOL attachAsString, is7bit; NSString *p; id body; if (_name == nil) return nil; /* check attachment */ fm = [self spoolFileManager]; p = [self pathToAttachmentWithName:_name]; if (![fm isReadableFileAtPath:p]) { [self errorWithFormat:@"did not find attachment: '%@'", _name]; return nil; } attachAsString = NO; is7bit = NO; /* prepare header of body part */ map = [[[NGMutableHashMap alloc] initWithCapacity:4] autorelease]; if ((s = [self contentTypeForAttachmentWithName:_name]) != nil) { [map setObject:s forKey:@"content-type"]; if ([s hasPrefix:@"text/"]) attachAsString = YES; else if ([s hasPrefix:@"message/rfc822"]) is7bit = YES; } if ((s = [self contentDispositionForAttachmentWithName:_name])) [map setObject:s forKey:@"content-disposition"]; /* prepare body content */ if (attachAsString) { // TODO: is this really necessary? NSString *s; content = [[NSData alloc] initWithContentsOfMappedFile:p]; s = [[NSString alloc] initWithData:content encoding:[NSString defaultCStringEncoding]]; if (s != nil) { body = s; [content release]; content = nil; } else { [self warnWithFormat: @"could not get text attachment as string: '%@'", _name]; body = content; content = nil; } } else if (is7bit) { /* Note: Apparently NGMimeFileData objects are not processed by the MIME generator! */ body = [[NGMimeFileData alloc] initWithPath:p removeFile:NO]; [map setObject:@"7bit" forKey:@"content-transfer-encoding"]; [map setObject:[NSNumber numberWithInt:[body length]] forKey:@"content-length"]; } else { /* Note: in OGo this is done in LSWImapMailEditor.m:2477. Apparently NGMimeFileData objects are not processed by the MIME generator! */ NSData *encoded; content = [[NSData alloc] initWithContentsOfMappedFile:p]; encoded = [content dataByEncodingBase64]; [content release]; content = nil; [map setObject:@"base64" forKey:@"content-transfer-encoding"]; [map setObject:[NSNumber numberWithInt:[encoded length]] forKey:@"content-length"]; /* Note: the -init method will create a temporary file! */ body = [[NGMimeFileData alloc] initWithBytes:[encoded bytes] length:[encoded length]]; } bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease]; [bodyPart setBody:body]; [body release]; body = nil; return bodyPart; } - (NSArray *)bodyPartsForAllAttachments { /* returns nil on error */ NSMutableArray *bodyParts; NSArray *names; unsigned i, count; names = [self fetchAttachmentNames]; if ((count = [names count]) == 0) return [NSArray array]; bodyParts = [NSMutableArray arrayWithCapacity:count]; for (i = 0; i < count; i++) { NGMimeBodyPart *bodyPart; bodyPart = [self bodyPartForAttachmentWithName:[names objectAtIndex:i]]; if (bodyPart == nil) return nil; [bodyParts addObject:bodyPart]; } return bodyParts; } - (NGMimeMessage *)mimeMultiPartMessageWithHeaderMap:(NGMutableHashMap *)map andBodyParts:(NSArray *)_bodyParts { NGMimeMessage *message; NGMimeMultipartBody *mBody; NGMimeBodyPart *part; NSEnumerator *e; [map addObject:MultiMixedType forKey:@"content-type"]; message = [[[NGMimeMessage alloc] initWithHeader:map] autorelease]; mBody = [[NGMimeMultipartBody alloc] initWithPart:message]; part = [self bodyPartForText]; [mBody addBodyPart:part]; e = [_bodyParts objectEnumerator]; while ((part = [e nextObject]) != nil) [mBody addBodyPart:part]; [message setBody:mBody]; [mBody release]; mBody = nil; return message; } - (void)_addHeaders:(NSDictionary *)_h toHeaderMap:(NGMutableHashMap *)_map { NSEnumerator *names; NSString *name; if ([_h count] == 0) return; names = [_h keyEnumerator]; while ((name = [names nextObject]) != nil) { id value; value = [_h objectForKey:name]; [_map addObject:value forKey:name]; } } - (BOOL)isEmptyValue:(id)_value { if (![_value isNotNull]) return YES; if ([_value isKindOfClass:[NSArray class]]) return [_value count] == 0 ? YES : NO; if ([_value isKindOfClass:[NSString class]]) return [_value length] == 0 ? YES : NO; return NO; } - (NGMutableHashMap *)mimeHeaderMapWithHeaders:(NSDictionary *)_headers { NGMutableHashMap *map; NSDictionary *lInfo; // TODO: this should be some kind of object? NSArray *emails; NSString *s; id from, replyTo; if ((lInfo = [self fetchInfo]) == nil) return nil; map = [[[NGMutableHashMap alloc] initWithCapacity:16] autorelease]; /* add recipients */ if ((emails = [lInfo objectForKey:@"to"]) != nil) { if ([emails count] == 0) { [self errorWithFormat:@"missing 'to' recipient in email!"]; return nil; } [map setObjects:emails forKey:@"to"]; } if ((emails = [lInfo objectForKey:@"cc"]) != nil) [map setObjects:emails forKey:@"cc"]; if ((emails = [lInfo objectForKey:@"bcc"]) != nil) [map setObjects:emails forKey:@"bcc"]; /* add senders */ from = [lInfo objectForKey:@"from"]; replyTo = [lInfo objectForKey:@"replyTo"]; if (![self isEmptyValue:from]) { if ([from isKindOfClass:[NSArray class]]) [map setObjects:from forKey:@"from"]; else [map setObject:from forKey:@"from"]; } if (![self isEmptyValue:replyTo]) { if ([from isKindOfClass:[NSArray class]]) [map setObjects:from forKey:@"reply-to"]; else [map setObject:from forKey:@"reply-to"]; } else if (![self isEmptyValue:from]) [map setObjects:[map objectsForKey:@"from"] forKey:@"reply-to"]; /* add subject */ if ([(s = [lInfo objectForKey:@"subject"]) length] > 0) [map setObject:s forKey:@"subject"]; /* add standard headers */ [map addObject:[NSCalendarDate date] forKey:@"date"]; [map addObject:@"1.0" forKey:@"MIME-Version"]; [map addObject:userAgent forKey:@"X-Mailer"]; /* add custom headers */ [self _addHeaders:[lInfo objectForKey:@"headers"] toHeaderMap:map]; [self _addHeaders:_headers toHeaderMap:map]; return map; } - (NGMimeMessage *)mimeMessageWithHeaders:(NSDictionary *)_headers { NSAutoreleasePool *pool; NGMutableHashMap *map; NSArray *bodyParts; NGMimeMessage *message; pool = [[NSAutoreleasePool alloc] init]; if ([self fetchInfo] == nil) { [self errorWithFormat:@"could not locate draft fetch info!"]; return nil; } if ((map = [self mimeHeaderMapWithHeaders:_headers]) == nil) return nil; [self debugWithFormat:@"MIME Envelope: %@", map]; if ((bodyParts = [self bodyPartsForAllAttachments]) == nil) { [self errorWithFormat: @"could not create body parts for attachments!"]; return nil; // TODO: improve error handling, return exception } [self debugWithFormat:@"attachments: %@", bodyParts]; if ([bodyParts count] == 0) { /* no attachments */ message = [self mimeMessageForContentWithHeaderMap:map]; } else { /* attachments, create multipart/mixed */ message = [self mimeMultiPartMessageWithHeaderMap:map andBodyParts:bodyParts]; } [self debugWithFormat:@"message: %@", message]; message = [message retain]; [pool release]; return [message autorelease]; } - (NGMimeMessage *)mimeMessage { return [self mimeMessageWithHeaders:nil]; } - (NSString *)saveMimeMessageToTemporaryFileWithHeaders:(NSDictionary *)_h { NGMimeMessageGenerator *gen; NSAutoreleasePool *pool; NGMimeMessage *message; NSString *tmpPath; pool = [[NSAutoreleasePool alloc] init]; message = [self mimeMessageWithHeaders:_h]; if (![message isNotNull]) return nil; if ([message isKindOfClass:[NSException class]]) { [self errorWithFormat:@"error: %@", message]; return nil; } gen = [[NGMimeMessageGenerator alloc] init]; tmpPath = [[gen generateMimeFromPartToFile:message] copy]; [gen release]; gen = nil; [pool release]; return [tmpPath autorelease]; } - (NSString *)saveMimeMessageToTemporaryFile { return [self saveMimeMessageToTemporaryFileWithHeaders:nil]; } - (void)deleteTemporaryMessageFile:(NSString *)_path { NSFileManager *fm; if (![_path isNotNull]) return; fm = [NSFileManager defaultManager]; if (![fm fileExistsAtPath:_path]) return; [fm removeFileAtPath:_path handler:nil]; } - (NSArray *)allRecipients { NSDictionary *lInfo; NSMutableArray *ma; NSArray *tmp; if ((lInfo = [self fetchInfo]) == nil) return nil; ma = [NSMutableArray arrayWithCapacity:16]; if ((tmp = [lInfo objectForKey:@"to"]) != nil) [ma addObjectsFromArray:tmp]; if ((tmp = [lInfo objectForKey:@"cc"]) != nil) [ma addObjectsFromArray:tmp]; if ((tmp = [lInfo objectForKey:@"bcc"]) != nil) [ma addObjectsFromArray:tmp]; return ma; } - (NSException *)sendMimeMessageAtPath:(NSString *)_path { static NGSendMail *mailer = nil; NSArray *recipients; NSString *from; /* validate */ recipients = [self allRecipients]; from = [self sender]; if ([recipients count] == 0) { return [NSException exceptionWithHTTPStatus:500 /* server error */ reason:@"draft has no recipients set!"]; } if ([from length] == 0) { return [NSException exceptionWithHTTPStatus:500 /* server error */ reason:@"draft has no sender (from) set!"]; } /* setup mailer object */ if (mailer == nil) mailer = [[NGSendMail sharedSendMail] retain]; if (![mailer isSendMailAvailable]) { [self errorWithFormat:@"missing sendmail binary!"]; return [NSException exceptionWithHTTPStatus:500 /* server error */ reason:@"did not find sendmail binary!"]; } /* send mail */ return [mailer sendMailAtPath:_path toRecipients:recipients sender:from]; } - (NSException *)sendMail { NSException *error; NSString *tmpPath; /* save MIME mail to file */ tmpPath = [self saveMimeMessageToTemporaryFile]; if (![tmpPath isNotNull]) { return [NSException exceptionWithHTTPStatus:500 /* server error */ reason:@"could not save MIME message for draft!"]; } /* send mail */ error = [self sendMimeMessageAtPath:tmpPath]; /* delete temporary file */ [self deleteTemporaryMessageFile:tmpPath]; return error; } /* operations */ - (NSException *)delete { NSFileManager *fm; NSString *p, *sp; NSEnumerator *e; if ((fm = [self spoolFileManager]) == nil) { [self errorWithFormat:@"missing spool file manager!"]; return [NSException exceptionWithHTTPStatus:500 /* server error */ reason:@"missing spool file manager!"]; } p = [self draftFolderPath]; if (![fm fileExistsAtPath:p]) { return [NSException exceptionWithHTTPStatus:404 /* not found */ reason:@"did not find draft!"]; } e = [[fm directoryContentsAtPath:p] objectEnumerator]; while ((sp = [e nextObject])) { sp = [p stringByAppendingPathComponent:sp]; if (draftDeleteDisabled) { [self logWithFormat:@"should delete draft file %@ ...", sp]; continue; } if (![fm removeFileAtPath:sp handler:nil]) { return [NSException exceptionWithHTTPStatus:500 /* server error */ reason:@"failed to delete draft!"]; } } if (draftDeleteDisabled) { [self logWithFormat:@"should delete draft directory: %@", p]; } else { if (![fm removeFileAtPath:p handler:nil]) { return [NSException exceptionWithHTTPStatus:500 /* server error */ reason:@"failed to delete draft directory!"]; } } return nil; } - (NSData *)content { /* Note: does not cache, expensive operation */ NSData *data; NSString *p; if ((p = [self saveMimeMessageToTemporaryFile]) == nil) return nil; data = [NSData dataWithContentsOfMappedFile:p]; /* delete temporary file */ [self deleteTemporaryMessageFile:p]; return data; } - (NSString *)contentAsString { NSString *str; NSData *data; if ((data = [self content]) == nil) return nil; str = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding]; if (str == nil) { [self errorWithFormat:@"could not load draft as ASCII (data size=%d)", [data length]]; return nil; } return [str autorelease]; } /* actions */ - (id)DELETEAction:(id)_ctx { NSException *error; if ((error = [self delete]) != nil) return error; return [NSNumber numberWithBool:YES]; /* delete worked out ... */ } - (id)GETAction:(WOContext *)_ctx { /* Override, because SOGoObject's GETAction uses the less efficient -contentAsString method. */ WORequest *rq; rq = [_ctx request]; if ([rq isSoWebDAVRequest]) { WOResponse *r; NSData *content; if ((content = [self content]) == nil) { return [NSException exceptionWithHTTPStatus:500 reason:@"Could not generate MIME content!"]; } r = [_ctx response]; [r setHeader:@"message/rfc822" forKey:@"content-type"]; [r setContent:content]; return r; } return [super GETAction:_ctx]; } /* fake being a SOGoMailObject */ - (id)fetchParts:(NSArray *)_parts { return [NSDictionary dictionaryWithObject:self forKey:@"fetch"]; } - (NSString *)uid { return [self nameInContainer]; } - (NSArray *)flags { static NSArray *seenFlags = nil; seenFlags = [[NSArray alloc] initWithObjects:@"seen", nil]; return seenFlags; } - (unsigned)size { // TODO: size, hard to support, we would need to generate MIME? return 0; } - (NSArray *)imap4EnvelopeAddressesForStrings:(NSArray *)_emails { NSMutableArray *ma; unsigned i, count; if (_emails == nil) return nil; if ((count = [_emails count]) == 0) return [NSArray array]; ma = [NSMutableArray arrayWithCapacity:count]; for (i = 0; i < count; i++) { NGImap4EnvelopeAddress *envaddr; envaddr = [[NGImap4EnvelopeAddress alloc] initWithString:[_emails objectAtIndex:i]]; if ([envaddr isNotNull]) [ma addObject:envaddr]; [envaddr release]; } return ma; } - (NGImap4Envelope *)envelope { NSDictionary *lInfo; id from, replyTo; if (self->envelope != nil) return self->envelope; if ((lInfo = [self fetchInfo]) == nil) return nil; if ((from = [self sender]) != nil) from = [NSArray arrayWithObjects:&from count:1]; if ((replyTo = [lInfo objectForKey:@"replyTo"]) != nil) { if (![replyTo isKindOfClass:[NSArray class]]) replyTo = [NSArray arrayWithObjects:&replyTo count:1]; } self->envelope = [[NGImap4Envelope alloc] initWithMessageID:[self nameInContainer] subject:[lInfo objectForKey:@"subject"] from:from replyTo:replyTo to:[lInfo objectForKey:@"to"] cc:[lInfo objectForKey:@"cc"] bcc:[lInfo objectForKey:@"bcc"]]; return self->envelope; } /* debugging */ - (BOOL)isDebuggingEnabled { return debugOn; } @end /* SOGoDraftObject */