From 305257b7e3205547aa7d2291293c00a6403ae774 Mon Sep 17 00:00:00 2001 From: helge Date: Wed, 27 Oct 2004 01:23:05 +0000 Subject: [PATCH] added ability to create NGMime objects git-svn-id: http://svn.opengroupware.org/SOGo/trunk@428 d1b88da0-ebda-0310-925b-ed51d893ca5b --- SOGo/SoObjects/Mailer/ChangeLog | 5 + SOGo/SoObjects/Mailer/SOGoDraftObject.h | 8 +- SOGo/SoObjects/Mailer/SOGoDraftObject.m | 307 +++++++++++++++++++++++- SOGo/SoObjects/Mailer/Version | 2 +- SOGo/UI/Mailer/ChangeLog | 4 + SOGo/UI/Mailer/UIxMailEditor.m | 109 +++++++-- SOGo/UI/Mailer/Version | 2 +- 7 files changed, 414 insertions(+), 23 deletions(-) diff --git a/SOGo/SoObjects/Mailer/ChangeLog b/SOGo/SoObjects/Mailer/ChangeLog index 04f91206..f89955bf 100644 --- a/SOGo/SoObjects/Mailer/ChangeLog +++ b/SOGo/SoObjects/Mailer/ChangeLog @@ -1,3 +1,8 @@ +2004-10-27 Helge Hess + + * SOGoDraftObject.m: added ability to create NGMime objects from draft + (v0.9.40) + 2004-10-26 Helge Hess * SOGoDraftObject.[hm]: added method to delete attachments (v0.9.39) diff --git a/SOGo/SoObjects/Mailer/SOGoDraftObject.h b/SOGo/SoObjects/Mailer/SOGoDraftObject.h index 9a0aedc4..65d6f200 100644 --- a/SOGo/SoObjects/Mailer/SOGoDraftObject.h +++ b/SOGo/SoObjects/Mailer/SOGoDraftObject.h @@ -35,10 +35,12 @@ */ @class NSString, NSArray, NSDictionary, NSData; +@class NGMimeMessage; @interface SOGoDraftObject : SOGoMailBaseObject { - NSString *path; + NSString *path; + NSDictionary *info; /* stores the envelope information */ } /* contents */ @@ -51,6 +53,10 @@ - (BOOL)saveAttachment:(NSData *)_attachment withName:(NSString *)_name; - (BOOL)deleteAttachmentWithName:(NSString *)_name; +/* NGMime representations */ + +- (NGMimeMessage *)mimeMessage; + @end #endif /* __Mailer_SOGoDraftObject_H__ */ diff --git a/SOGo/SoObjects/Mailer/SOGoDraftObject.m b/SOGo/SoObjects/Mailer/SOGoDraftObject.m index 722ceaa5..16c893ea 100644 --- a/SOGo/SoObjects/Mailer/SOGoDraftObject.m +++ b/SOGo/SoObjects/Mailer/SOGoDraftObject.m @@ -20,12 +20,26 @@ */ #include "SOGoDraftObject.h" +#include +#include +#include +#include +#include #include #include "common.h" @implementation SOGoDraftObject +static NGMimeType *TextPlainType = nil; +static NGMimeType *MultiMixedType = nil; + ++ (void)initialize { + TextPlainType = [[NGMimeType mimeType:@"text" subType:@"plain"] copy]; + MultiMixedType = [[NGMimeType mimeType:@"multipart" subType:@"mixed"] copy]; +} + - (void)dealloc { + [self->info release]; [self->path release]; [super dealloc]; } @@ -88,8 +102,10 @@ return YES; } - (NSDictionary *)fetchInfo { - NSDictionary *info; NSString *p; + + if (self->info != nil) + return self->info; p = [self infoPath]; if (![[self spoolFileManager] fileExistsAtPath:p]) { @@ -97,10 +113,11 @@ return nil; } - if ((info = [NSDictionary dictionaryWithContentsOfFile:p]) == nil) - [self logWithFormat:@"ERROR: dictionary broken at path: %@", p]; + self->info = [[NSDictionary alloc] initWithContentsOfFile:p]; + if (self->info == nil) + [self logWithFormat:@"ERROR: draft info dictionary broken at path: %@", p]; - return info; + return self->info; } - (NSArray *)fetchAttachmentNames { @@ -140,10 +157,19 @@ if (r.length > 0) return NO; r = [_name rangeOfString:@"~"]; if (r.length > 0) return NO; + r = [_name rangeOfString:@"\""]; + if (r.length > 0) return NO; return YES; } +- (NSString *)pathToAttachmentWithName:(NSString *)_name { + if ([_name length] == 0) + return nil; + + return [[self draftFolderPath] stringByAppendingPathComponent:_name]; +} + - (BOOL)saveAttachment:(NSData *)_attachment withName:(NSString *)_name { NSString *p; @@ -157,7 +183,7 @@ if (![self isValidAttachmentName:_name]) return NO; - p = [[self draftFolderPath] stringByAppendingPathComponent:_name]; + p = [self pathToAttachmentWithName:_name]; return [_attachment writeToFile:p atomically:YES]; } @@ -169,11 +195,280 @@ return NO; fm = [self spoolFileManager]; - p = [[self draftFolderPath] stringByAppendingPathComponent:_name]; + p = [self pathToAttachmentWithName:_name]; if (![fm fileExistsAtPath:p]) return YES; /* well, doesn't exist, so its deleted ;-) */ return [fm removeFileAtPath:p handler:nil]; } +/* NGMime representations */ + +- (NGMimeBodyPart *)bodyPartForText { + NGMutableHashMap *map; + NGMimeBodyPart *bodyPart; + NSDictionary *lInfo; + + if ((lInfo = [self fetchInfo]) == nil) + return nil; + + /* prepare header of body part */ + + map = [[[NGMutableHashMap alloc] initWithCapacity:2] autorelease]; + [map setObject:@"text/plain" forKey:@"content-type"]; + + /* prepare body content */ + + bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease]; + [bodyPart setBody:[lInfo objectForKey:@"text"]]; + return bodyPart; +} + +- (NGMimeMessage *)mimeMessageForContentWithHeaderMap:(NGMutableHashMap *)map { + NSDictionary *lInfo; + NGMimeMessage *message; + + if ((lInfo = [self fetchInfo]) == nil) + return nil; + + [map setObject:@"text/plain" forKey:@"content-type"]; + message = [[[NGMimeMessage alloc] initWithHeader:map] autorelease]; + [message setBody:[lInfo objectForKey:@"text"]]; + return message; +} + +- (NSString *)mimeTypeForExtension:(NSString *)_ext { + // TODO: make configurable + 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"; + return @"application/octet-stream"; +} + +- (NSString *)contentTypeForAttachmentWithName:(NSString *)_name { + return [self mimeTypeForExtension:[_name pathExtension]]; +} +- (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/"]) + 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; + NSString *p; + + if (_name == nil) return nil; + + /* check attachment */ + + fm = [self spoolFileManager]; + p = [self pathToAttachmentWithName:_name]; + if (![fm isReadableFileAtPath:p]) { + [self logWithFormat:@"ERROR: did not find attachment: '%@'", _name]; + return nil; + } + attachAsString = NO; + + /* prepare header of body part */ + + map = [[[NGMutableHashMap alloc] initWithCapacity:4] autorelease]; + + if ((s = [self contentTypeForAttachmentWithName:_name])) { + [map setObject:s forKey:@"content-type"]; + if ([s hasPrefix:@"text/"]) + attachAsString = YES; + } + if ((s = [self contentDispositionForAttachmentWithName:_name])) + [map setObject:s forKey:@"content-disposition"]; + + bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease]; + + /* 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) { + [bodyPart setBody:s]; + [s release]; s = nil; + } + else { + [self logWithFormat: + @"WARNING: could not get text attachment as string: '%@'",_name]; + [bodyPart setBody:content]; + } + } + else { + content = [[NGMimeFileData alloc] initWithPath:p removeFile:NO]; + [bodyPart setBody:content]; + } + + [content release]; content = 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; +} + +- (NGMutableHashMap *)mimeHeaderMap { + NGMutableHashMap *map; + NSDictionary *lInfo; + NSArray *emails; + NSString *s; + + 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 logWithFormat:@"ERROR: 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 */ + + if ((s = [lInfo objectForKey:@"from"]) != nil) + [map setObject:s forKey:@"from"]; + if ((s = [lInfo objectForKey:@"replyTo"]) != nil) + [map setObject:s forKey:@"reply-to"]; + + /* add subject */ + + if ((s = [lInfo objectForKey:@"subject"]) != nil) + [map setObject:s forKey:@"subject"]; + + /* add standard headers */ + + [map addObject:[NSCalendarDate date] forKey:@"date"]; + [map addObject:@"1.0" forKey:@"MIME-Version"]; + [map addObject:@"SOGoMail 1.0" forKey:@"X-Mailer"]; + + return map; +} + +- (NGMimeMessage *)mimeMessage { + NSAutoreleasePool *pool; + NGMutableHashMap *map; + NSArray *bodyParts; + NGMimeMessage *message; + + pool = [[NSAutoreleasePool alloc] init]; + + if ([self fetchInfo] == nil) { + [self logWithFormat:@"ERROR: could not locate draft fetch info!"]; + return nil; + } + + if ((map = [self mimeHeaderMap]) == nil) + return nil; + [self logWithFormat:@"MIME Envelope: %@", map]; + + if ((bodyParts = [self bodyPartsForAllAttachments]) == nil) { + [self logWithFormat: + @"ERROR: could not create body parts for attachments!"]; + return nil; // TODO: improve error handling, return exception + } + [self logWithFormat:@"attachments: %@", bodyParts]; + + if ([bodyParts count] == 0) { + /* no attachments */ + message = [self mimeMessageForContentWithHeaderMap:map]; + } + else { + /* attachments, create multipart/mixed */ + message = [self mimeMultiPartMessageWithHeaderMap:map + andBodyParts:bodyParts]; + } + [self logWithFormat:@"message: %@", message]; + + message = [message retain]; + [pool release]; + return [message autorelease]; +} + @end /* SOGoDraftObject */ diff --git a/SOGo/SoObjects/Mailer/Version b/SOGo/SoObjects/Mailer/Version index 5c7b2848..7c8be669 100644 --- a/SOGo/SoObjects/Mailer/Version +++ b/SOGo/SoObjects/Mailer/Version @@ -1,6 +1,6 @@ # Version file -SUBMINOR_VERSION:=39 +SUBMINOR_VERSION:=40 # v0.9.35 requires SOGoLogic v0.9.24 # v0.9.34 requires SOGoLogic v0.9.22 diff --git a/SOGo/UI/Mailer/ChangeLog b/SOGo/UI/Mailer/ChangeLog index acf01f5e..aea7aa96 100644 --- a/SOGo/UI/Mailer/ChangeLog +++ b/SOGo/UI/Mailer/ChangeLog @@ -1,3 +1,7 @@ +2004-10-27 Helge Hess + + * UIxMailEditor.m: added send related code (v0.9.49) + 2004-10-26 Helge Hess * UIxMailEditorAttach.m: added attachment delete (v0.9.48) diff --git a/SOGo/UI/Mailer/UIxMailEditor.m b/SOGo/UI/Mailer/UIxMailEditor.m index a280c94c..ac23275c 100644 --- a/SOGo/UI/Mailer/UIxMailEditor.m +++ b/SOGo/UI/Mailer/UIxMailEditor.m @@ -37,11 +37,14 @@ NSArray *bcc; NSString *subject; NSString *text; + NSString *tmpMessagePath; } @end #include +#include +#include #include "common.h" @implementation UIxMailEditor @@ -55,7 +58,23 @@ static NSArray *infoKeys = nil; nil]; } +- (void)deleteTemporaryMessageFile { + NSFileManager *fm; + + if (![self->tmpMessagePath isNotNull]) + return; + + fm = [NSFileManager defaultManager]; + if (![fm fileExistsAtPath:self->tmpMessagePath]) + return; + + [fm removeFileAtPath:self->tmpMessagePath handler:nil]; +} + - (void)dealloc { + [self deleteTemporaryMessageFile]; + [self->tmpMessagePath release]; + [self->text release]; [self->subject release]; [self->to release]; @@ -126,6 +145,22 @@ static NSArray *infoKeys = nil; return [self valuesForKeys:infoKeys]; } +/* MIME message */ + +- (NSString *)saveMessageToTemporaryFile:(NGMimeMessage *)_message { + NGMimeMessageGenerator *gen; + NSString *path; + + if (![_message isNotNull]) + return nil; + + gen = [[NGMimeMessageGenerator alloc] init]; + path = [gen generateMimeFromPartToFile:_message]; + [gen release]; gen = nil; + + return path; +} + /* requests */ - (BOOL)shouldTakeValuesFromRequest:(WORequest *)_rq inContext:(WOContext*)_c{ @@ -134,34 +169,80 @@ static NSArray *infoKeys = nil; /* actions */ -- (id)editAction { - [self logWithFormat:@"edit action, load content from: %@", - [self clientObject]]; - - [self loadInfo:[[self clientObject] fetchInfo]]; - return self; -} - -- (id)saveAction { +- (BOOL)_saveFormInfo { NSDictionary *info; if ((info = [self storeInfo]) != nil) { if (![[self clientObject] storeInfo:info]) { [self logWithFormat:@"ERROR: failed to store draft!"]; // TODO: improve error handling - return nil; + return NO; } } - [self logWithFormat:@"save action, store content to: %@", - [self clientObject]]; + // TODO: wrap content + + return YES; +} +- (id)failedToSaveFormResponse { + // TODO: improve error handling + return [NSException exceptionWithHTTPStatus:500 /* server error */ + reason:@"failed to store draft object on server!"]; +} + +- (id)editAction { + [self logWithFormat:@"edit action, load content from: %@", + [self clientObject]]; + + [self loadInfo:[[self clientObject] fetchInfo]]; return self; } +- (id)saveAction { + return [self _saveFormInfo] ? self : [self failedToSaveFormResponse]; +} + - (id)sendAction { - [self logWithFormat:@"send action, store content, send mail, store: %@", - [self clientObject]]; + NGMimeMessage *message; + + /* first, save form data */ + + if (![self _saveFormInfo]) + return [self failedToSaveFormResponse]; + + /* then, send mail */ + + if ((message = [[self clientObject] mimeMessage]) == nil) { + return [NSException exceptionWithHTTPStatus:500 /* server error */ + reason:@"could not create MIME message for draft!"]; + } + + self->tmpMessagePath = [[self saveMessageToTemporaryFile:message] copy]; + if (![self->tmpMessagePath isNotNull]) { + return [NSException exceptionWithHTTPStatus:500 /* server error */ + reason:@"could not save MIME message for draft!"]; + } + + [self logWithFormat:@"saved message to: %@", self->tmpMessagePath]; + + [self logWithFormat:@"TODO: send mail ..."]; + + /* + Flags we should set: + if the draft is a reply => [message markAnswered] + if the draft is a forward => [message addFlag:@"forwarded"] + + This is hard, we would need to find the original message in Cyrus. + */ + + /* delete draft */ + + /* finally store in Sent */ + + [self logWithFormat:@"TODO: store mail in Sent folder ..."]; + // if everything is ok, close the window (send a JS closing the Window) + [self deleteTemporaryMessageFile]; return self; } diff --git a/SOGo/UI/Mailer/Version b/SOGo/UI/Mailer/Version index 98a3496a..13cb5e08 100644 --- a/SOGo/UI/Mailer/Version +++ b/SOGo/UI/Mailer/Version @@ -1,6 +1,6 @@ # $Id$ -SUBMINOR_VERSION:=48 +SUBMINOR_VERSION:=49 # v0.9.43 requires NGObjWeb v4.3.73 # v0.9.42 requires NGObjWeb v4.3.72 -- 2.39.5