*/
#include "SOGoDraftObject.h"
+#include <NGMail/NGMimeMessage.h>
+#include <NGMime/NGMimeBodyPart.h>
+#include <NGMime/NGMimeMultipartBody.h>
+#include <NGMime/NGMimeFileData.h>
+#include <NGMime/NGMimeType.h>
#include <NGExtensions/NSFileManager+Extensions.h>
#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];
}
return YES;
}
- (NSDictionary *)fetchInfo {
- NSDictionary *info;
NSString *p;
+
+ if (self->info != nil)
+ return self->info;
p = [self infoPath];
if (![[self spoolFileManager] fileExistsAtPath:p]) {
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 {
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;
if (![self isValidAttachmentName:_name])
return NO;
- p = [[self draftFolderPath] stringByAppendingPathComponent:_name];
+ p = [self pathToAttachmentWithName:_name];
return [_attachment writeToFile:p atomically:YES];
}
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 */
NSArray *bcc;
NSString *subject;
NSString *text;
+ NSString *tmpMessagePath;
}
@end
#include <SOGo/SoObjects/Mailer/SOGoDraftObject.h>
+#include <NGMail/NGMimeMessage.h>
+#include <NGMail/NGMimeMessageGenerator.h>
#include "common.h"
@implementation UIxMailEditor
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];
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{
/* 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;
}