2 Copyright (C) 2004-2005 SKYRIX Software AG
4 This file is part of OpenGroupware.org.
6 OGo is free software; you can redistribute it and/or modify it under
7 the terms of the GNU Lesser General Public License as published by the
8 Free Software Foundation; either version 2, or (at your option) any
11 OGo is distributed in the hope that it will be useful, but WITHOUT ANY
12 WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
14 License for more details.
16 You should have received a copy of the GNU Lesser General Public
17 License along with OGo; see the file COPYING. If not, write to the
18 Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
22 #import <Foundation/NSArray.h>
23 #import <Foundation/NSAutoreleasePool.h>
24 #import <Foundation/NSDictionary.h>
25 #import <Foundation/NSKeyValueCoding.h>
26 #import <Foundation/NSURL.h>
27 #import <Foundation/NSUserDefaults.h>
28 #import <Foundation/NSValue.h>
30 #import <NGObjWeb/NSException+HTTP.h>
31 #import <NGObjWeb/SoObject+SoDAV.h>
32 #import <NGObjWeb/WOContext+SoObjects.h>
33 #import <NGObjWeb/WORequest+So.h>
34 #import <NGObjWeb/WOResponse.h>
35 #import <NGExtensions/NGBase64Coding.h>
36 #import <NGExtensions/NSFileManager+Extensions.h>
37 #import <NGExtensions/NGHashMap.h>
38 #import <NGExtensions/NSNull+misc.h>
39 #import <NGExtensions/NSObject+Logs.h>
40 #import <NGExtensions/NGQuotedPrintableCoding.h>
41 #import <NGExtensions/NSString+misc.h>
42 #import <NGImap4/NGImap4Connection.h>
43 #import <NGImap4/NGImap4Client.h>
44 #import <NGImap4/NGImap4Envelope.h>
45 #import <NGImap4/NGImap4EnvelopeAddress.h>
46 #import <NGMail/NGMimeMessage.h>
47 #import <NGMail/NGMimeMessageGenerator.h>
48 #import <NGMime/NGMimeBodyPart.h>
49 #import <NGMime/NGMimeFileData.h>
50 #import <NGMime/NGMimeMultipartBody.h>
51 #import <NGMime/NGMimeType.h>
52 #import <NGMime/NGMimeHeaderFieldGenerator.h>
54 #import <SoObjects/SOGo/NSArray+Utilities.h>
55 #import <SoObjects/SOGo/NSCalendarDate+SOGo.h>
56 #import <SoObjects/SOGo/NSString+Utilities.h>
57 #import <SoObjects/SOGo/SOGoMailer.h>
58 #import <SoObjects/SOGo/SOGoUser.h>
59 #import "SOGoMailAccount.h"
60 #import "SOGoMailFolder.h"
61 #import "SOGoMailObject.h"
62 #import "SOGoMailObject+Draft.h"
64 #import "SOGoDraftObject.h"
66 static NSString *contentTypeValue = @"text/plain; charset=utf-8";
67 static NSString *headerKeys[] = {@"subject", @"to", @"cc", @"bcc",
68 @"from", @"replyTo", nil};
70 @implementation SOGoDraftObject
72 static NGMimeType *TextPlainType = nil;
73 static NGMimeType *MultiMixedType = nil;
74 static NSString *userAgent = @"SOGoMail 1.0";
75 static BOOL draftDeleteDisabled = NO; // for debugging
76 static BOOL debugOn = NO;
77 static BOOL showTextAttachmentsInline = NO;
81 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
83 /* Note: be aware of the charset issues before enabling this! */
84 showTextAttachmentsInline = [ud boolForKey: @"SOGoShowTextAttachmentsInline"];
86 if ((draftDeleteDisabled = [ud boolForKey: @"SOGoNoDraftDeleteAfterSend"]))
87 NSLog(@"WARNING: draft delete is disabled! (SOGoNoDraftDeleteAfterSend)");
89 TextPlainType = [[NGMimeType mimeType: @"text" subType: @"plain"] copy];
90 MultiMixedType = [[NGMimeType mimeType: @"multipart" subType: @"mixed"] copy];
95 if ((self = [super init]))
98 headers = [NSMutableDictionary new];
114 [sourceFlag release];
118 /* draft folder functionality */
120 - (NSString *) userSpoolFolderPath
122 return [[self container] userSpoolFolderPath];
125 /* draft object functionality */
127 - (NSString *) draftFolderPath
131 path = [[self userSpoolFolderPath] stringByAppendingPathComponent:
139 - (BOOL) _ensureDraftFolderPath
143 fm = [NSFileManager defaultManager];
145 return ([fm createDirectoriesAtPath: [container userSpoolFolderPath]
147 && [fm createDirectoriesAtPath: [self draftFolderPath]
151 - (NSString *) infoPath
153 return [[self draftFolderPath]
154 stringByAppendingPathComponent: @".info.plist"];
159 - (void) setHeaders: (NSDictionary *) newHeaders
164 for (count = 0; count < 6; count++)
166 headerValue = [newHeaders objectForKey: headerKeys[count]];
168 [headers setObject: headerValue
169 forKey: headerKeys[count]];
171 [headers removeObjectForKey: headerKeys[count]];
175 - (NSDictionary *) headers
180 - (void) setText: (NSString *) newText
182 ASSIGN (text, newText);
190 - (void) setSourceURL: (NSString *) newSourceURL
192 ASSIGN (sourceURL, newSourceURL);
195 - (void) setSourceFlag: (NSString *) newSourceFlag
197 ASSIGN (sourceFlag, newSourceFlag);
200 - (NSException *) storeInfo
202 NSMutableDictionary *infos;
205 if ([self _ensureDraftFolderPath])
207 infos = [NSMutableDictionary new];
208 [infos setObject: headers forKey: @"headers"];
210 [infos setObject: text forKey: @"text"];
212 [infos setObject: [NSNumber numberWithInt: IMAP4ID]
214 if (sourceURL && sourceFlag)
216 [infos setObject: sourceURL forKey: @"sourceURL"];
217 [infos setObject: sourceFlag forKey: @"sourceFlag"];
220 if ([infos writeToFile: [self infoPath] atomically:YES])
224 [self errorWithFormat: @"could not write info: '%@'",
226 error = [NSException exceptionWithHTTPStatus:500 /* server error */
227 reason: @"could not write draft info!"];
234 [self errorWithFormat: @"could not create folder for draft: '%@'",
235 [self draftFolderPath]];
236 error = [NSException exceptionWithHTTPStatus:500 /* server error */
237 reason: @"could not create folder for draft!"];
243 - (void) _loadInfosFromDictionary: (NSDictionary *) infoDict
247 value = [infoDict objectForKey: @"headers"];
249 [self setHeaders: value];
251 value = [infoDict objectForKey: @"text"];
252 if ([value length] > 0)
253 [self setText: value];
255 value = [infoDict objectForKey: @"IMAP4ID"];
257 [self setIMAP4ID: [value intValue]];
259 value = [infoDict objectForKey: @"sourceURL"];
261 [self setSourceURL: value];
262 value = [infoDict objectForKey: @"sourceFlag"];
264 [self setSourceFlag: value];
267 - (NSString *) relativeImap4Name
269 return [NSString stringWithFormat: @"%d", IMAP4ID];
280 fm = [NSFileManager defaultManager];
281 if ([fm fileExistsAtPath: p])
283 infos = [NSDictionary dictionaryWithContentsOfFile: p];
285 [self _loadInfosFromDictionary: infos];
287 // [self errorWithFormat: @"draft info dictionary broken at path: %@", p];
290 [self debugWithFormat: @"Note: info object does not yet exist: %@", p];
293 - (void) setIMAP4ID: (int) newIMAP4ID
295 IMAP4ID = newIMAP4ID;
303 - (int) _IMAP4IDFromAppendResult: (NSDictionary *) result
305 NSDictionary *results;
306 NSString *flag, *newIdString;
308 results = [[result objectForKey: @"RawResponse"]
309 objectForKey: @"ResponseResult"];
310 flag = [results objectForKey: @"flag"];
311 newIdString = [[flag componentsSeparatedByString: @" "] objectAtIndex: 2];
313 return [newIdString intValue];
316 - (NSException *) save
318 NGImap4Client *client;
325 message = [self mimeMessageAsData];
327 client = [[self imap4Connection] client];
328 folder = [imap4 imap4FolderNameForURL: [container imap4URL]];
330 = [client append: message toFolder: folder
331 withFlags: [NSArray arrayWithObjects: @"seen", @"draft", nil]];
332 if ([[result objectForKey: @"result"] boolValue])
335 error = [imap4 markURLDeleted: [self imap4URL]];
336 IMAP4ID = [self _IMAP4IDFromAppendResult: result];
340 error = [NSException exceptionWithHTTPStatus:500 /* Server Error */
341 reason: @"Failed to store message"];
346 - (void) _addEMailsOfAddresses: (NSArray *) _addrs
347 toArray: (NSMutableArray *) _ma
351 for (i = 0, count = [_addrs count]; i < count; i++)
353 [(NGImap4EnvelopeAddress *) [_addrs objectAtIndex: i] email]];
356 - (void) _fillInReplyAddresses: (NSMutableDictionary *) _info
357 replyToAll: (BOOL) _replyToAll
358 envelope: (NGImap4Envelope *) _envelope
361 The rules as implemented by Thunderbird:
362 - if there is a 'reply-to' header, only include that (as TO)
363 - if we reply to all, all non-from addresses are added as CC
364 - the from is always the lone TO (except for reply-to)
366 Note: we cannot check reply-to, because Cyrus even sets a reply-to in the
367 envelope if none is contained in the message itself! (bug or
370 TODO: what about sender (RFC 822 3.6.2)
375 to = [NSMutableArray arrayWithCapacity:2];
377 /* first check for "reply-to" */
379 addrs = [_envelope replyTo];
380 if ([addrs count] == 0)
381 /* no "reply-to", try "from" */
382 addrs = [_envelope from];
384 [self _addEMailsOfAddresses: addrs toArray: to];
385 [_info setObject: to forKey: @"to"];
387 /* CC processing if we reply-to-all: add all 'to' and 'cc' */
391 to = [NSMutableArray arrayWithCapacity:8];
393 [self _addEMailsOfAddresses: [_envelope to] toArray: to];
394 [self _addEMailsOfAddresses: [_envelope cc] toArray: to];
396 [_info setObject: to forKey: @"cc"];
400 - (void) _fetchAttachments: (NSArray *) parts
401 fromMail: (SOGoMailObject *) sourceMail
403 unsigned int count, max;
404 NSDictionary *currentPart, *attachment, *body;
405 NSArray *paths, *result;
410 paths = [parts keysWithFormat: @"BODY[%{path}]"];
411 result = [[sourceMail fetchParts: paths] objectForKey: @"fetch"];
412 for (count = 0; count < max; count++)
414 currentPart = [parts objectAtIndex: count];
415 body = [[result objectAtIndex: count] objectForKey: @"body"];
416 attachment = [NSDictionary dictionaryWithObjectsAndKeys:
417 [currentPart objectForKey: @"filename"],
419 [currentPart objectForKey: @"mimetype"],
422 [self saveAttachment: [body objectForKey: @"data"]
423 withMetadata: attachment];
428 - (void) fetchMailForEditing: (SOGoMailObject *) sourceMail
431 NSMutableDictionary *info;
432 NSMutableArray *addresses;
433 NGImap4Envelope *sourceEnvelope;
435 [sourceMail fetchCoreInfos];
437 [self _fetchAttachments: [sourceMail fetchFileAttachmentKeys]
438 fromMail: sourceMail];
439 info = [NSMutableDictionary dictionaryWithCapacity: 16];
440 subject = [sourceMail subject];
441 if ([subject length] > 0)
442 [info setObject: subject forKey: @"subject"];
444 sourceEnvelope = [sourceMail envelope];
445 addresses = [NSMutableArray array];
446 [self _addEMailsOfAddresses: [sourceEnvelope to] toArray: addresses];
447 [info setObject: addresses forKey: @"to"];
448 addresses = [NSMutableArray array];
449 [self _addEMailsOfAddresses: [sourceEnvelope cc] toArray: addresses];
450 if ([addresses count] > 0)
451 [info setObject: addresses forKey: @"cc"];
452 addresses = [NSMutableArray array];
453 [self _addEMailsOfAddresses: [sourceEnvelope bcc] toArray: addresses];
454 if ([addresses count] > 0)
455 [info setObject: addresses forKey: @"bcc"];
456 addresses = [NSMutableArray array];
457 [self _addEMailsOfAddresses: [sourceEnvelope replyTo] toArray: addresses];
458 if ([addresses count] > 0)
459 [info setObject: addresses forKey: @"replyTo"];
460 [self setHeaders: info];
462 [self setText: [sourceMail contentForEditing]];
463 [self setSourceURL: [sourceMail imap4URLString]];
464 IMAP4ID = [[sourceMail nameInContainer] intValue];
469 - (void) fetchMailForReplying: (SOGoMailObject *) sourceMail
472 NSString *contentForReply;
473 NSMutableDictionary *info;
475 [sourceMail fetchCoreInfos];
477 info = [NSMutableDictionary dictionaryWithCapacity: 16];
478 [info setObject: [sourceMail subjectForReply] forKey: @"subject"];
479 [self _fillInReplyAddresses: info replyToAll: toAll
480 envelope: [sourceMail envelope]];
481 contentForReply = [sourceMail contentForReply];
482 [self setText: contentForReply];
483 [self setHeaders: info];
484 [self setSourceURL: [sourceMail imap4URLString]];
485 [self setSourceFlag: @"Answered"];
489 - (void) fetchMailForForwarding: (SOGoMailObject *) sourceMail
491 NSDictionary *info, *attachment;
492 SOGoUser *currentUser;
494 [sourceMail fetchCoreInfos];
496 info = [NSDictionary dictionaryWithObject: [sourceMail subjectForForward]
498 [self setHeaders: info];
499 [self setSourceURL: [sourceMail imap4URLString]];
500 [self setSourceFlag: @"$Forwarded"];
503 currentUser = [context activeUser];
504 if ([[currentUser messageForwarding] isEqualToString: @"inline"])
505 [self setText: [sourceMail contentForInlineForward]];
508 // TODO: use subject for filename?
509 // error = [newDraft saveAttachment:content withName:@"forward.mail"];
510 attachment = [NSDictionary dictionaryWithObjectsAndKeys:
511 [sourceMail filenameForForward], @"filename",
512 @"message/rfc822", @"mime-type",
514 [self saveAttachment: [sourceMail content]
515 withMetadata: attachment];
522 - (NSString *) sender
526 if ((tmp = [headers objectForKey: @"from"]) == nil)
528 if ([tmp isKindOfClass:[NSArray class]])
529 return [tmp count] > 0 ? [tmp objectAtIndex: 0] : nil;
536 - (NSArray *) fetchAttachmentNames
544 fm = [NSFileManager defaultManager];
545 files = [fm directoryContentsAtPath: [self draftFolderPath]];
548 ma = [NSMutableArray arrayWithCapacity: max];
549 for (count = 0; count < max; count++)
551 filename = [files objectAtIndex: count];
552 if (![filename hasPrefix: @"."])
553 [ma addObject: filename];
559 - (BOOL) isValidAttachmentName: (NSString *) _name
561 static NSString *sescape[] = { @"/", @"..", @"~", @"\"", @"'", @" ", nil };
565 if (![_name isNotNull]) return NO;
566 if ([_name length] == 0) return NO;
567 if ([_name hasPrefix: @"."]) return NO;
569 for (i = 0; sescape[i] != nil; i++) {
570 r = [_name rangeOfString:sescape[i]];
571 if (r.length > 0) return NO;
576 - (NSString *) pathToAttachmentWithName: (NSString *) _name
578 if ([_name length] == 0)
581 return [[self draftFolderPath] stringByAppendingPathComponent:_name];
584 - (NSException *) invalidAttachmentNameError: (NSString *) _name
586 return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
587 reason: @"Invalid attachment name!"];
590 - (NSException *) saveAttachment: (NSData *) _attach
591 withMetadata: (NSDictionary *) metadata
593 NSString *p, *name, *mimeType;
595 if (![_attach isNotNull]) {
596 return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
597 reason: @"Missing attachment content!"];
600 if (![self _ensureDraftFolderPath]) {
601 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
602 reason: @"Could not create folder for draft!"];
604 name = [metadata objectForKey: @"filename"];
605 if (![self isValidAttachmentName: name])
606 return [self invalidAttachmentNameError: name];
608 p = [self pathToAttachmentWithName: name];
609 if (![_attach writeToFile: p atomically: YES])
611 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
612 reason: @"Could not write attachment to draft!"];
615 mimeType = [metadata objectForKey: @"mime-type"];
616 if ([mimeType length] > 0)
618 p = [self pathToAttachmentWithName:
619 [NSString stringWithFormat: @".%@.mime", name]];
620 if (![[mimeType dataUsingEncoding: NSUTF8StringEncoding]
621 writeToFile: p atomically: YES])
623 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
624 reason: @"Could not write attachment to draft!"];
628 return nil; /* everything OK */
631 - (NSException *) deleteAttachmentWithName: (NSString *) _name
639 if ([self isValidAttachmentName:_name])
641 fm = [NSFileManager defaultManager];
642 p = [self pathToAttachmentWithName:_name];
643 if ([fm fileExistsAtPath: p])
644 if (![fm removeFileAtPath: p handler: nil])
646 = [NSException exceptionWithHTTPStatus: 500 /* Server Error */
647 reason: @"Could not delete attachment from draft!"];
650 error = [self invalidAttachmentNameError:_name];
655 /* NGMime representations */
657 - (NGMimeBodyPart *) bodyPartForText
660 This add the text typed by the user (the primary plain/text part).
662 NGMutableHashMap *map;
663 NGMimeBodyPart *bodyPart;
665 /* prepare header of body part */
667 map = [[[NGMutableHashMap alloc] initWithCapacity: 1] autorelease];
669 // TODO: set charset in header!
670 [map setObject: @"text/plain" forKey: @"content-type"];
672 [map setObject: contentTypeValue forKey: @"content-type"];
674 // if ((body = text) != nil) {
675 // if ([body isKindOfClass: [NSString class]]) {
676 // [map setObject: contentTypeValue
677 // forKey: @"content-type"];
678 // // body = [body dataUsingEncoding:NSUTF8StringEncoding];
682 /* prepare body content */
684 bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease];
685 [bodyPart setBody: text];
690 - (NGMimeMessage *) mimeMessageForContentWithHeaderMap: (NGMutableHashMap *) map
692 NGMimeMessage *message;
696 [map setObject: @"text/plain" forKey: @"content-type"];
700 // if ([body isKindOfClass:[NSString class]])
701 /* Note: just 'utf8' is displayed wrong in Mail.app */
702 [map setObject: contentTypeValue
703 forKey: @"content-type"];
704 // body = [body dataUsingEncoding:NSUTF8StringEncoding];
705 // else if ([body isKindOfClass:[NSData class]] && addSuffix) {
706 // body = [[body mutableCopy] autorelease];
708 // else if (addSuffix) {
709 // [self warnWithFormat: @"Note: cannot add Internet marker to body: %@",
710 // NSStringFromClass([body class])];
713 message = [[[NGMimeMessage alloc] initWithHeader:map] autorelease];
714 [message setBody: body];
723 - (NSString *) mimeTypeForExtension: (NSString *) _ext
725 // TODO: make configurable
726 // TODO: use /etc/mime-types
727 if ([_ext isEqualToString: @"txt"]) return @"text/plain";
728 if ([_ext isEqualToString: @"html"]) return @"text/html";
729 if ([_ext isEqualToString: @"htm"]) return @"text/html";
730 if ([_ext isEqualToString: @"gif"]) return @"image/gif";
731 if ([_ext isEqualToString: @"jpg"]) return @"image/jpeg";
732 if ([_ext isEqualToString: @"jpeg"]) return @"image/jpeg";
733 if ([_ext isEqualToString: @"mail"]) return @"message/rfc822";
734 return @"application/octet-stream";
737 - (NSString *) contentTypeForAttachmentWithName: (NSString *) _name
742 p = [self pathToAttachmentWithName:
743 [NSString stringWithFormat: @".%@.mime", _name]];
744 mimeData = [NSData dataWithContentsOfFile: p];
747 s = [[NSString alloc] initWithData: mimeData
748 encoding: NSUTF8StringEncoding];
753 s = [self mimeTypeForExtension:[_name pathExtension]];
754 if ([_name length] > 0)
755 s = [s stringByAppendingFormat: @"; name=\"%@\"", _name];
761 - (NSString *) contentDispositionForAttachmentWithName: (NSString *) _name
767 type = [self contentTypeForAttachmentWithName:_name];
769 if ([type hasPrefix: @"text/"])
770 cdtype = showTextAttachmentsInline ? @"inline" : @"attachment";
771 else if ([type hasPrefix: @"image/"] || [type hasPrefix: @"message"])
774 cdtype = @"attachment";
776 cd = [cdtype stringByAppendingString: @"; filename=\""];
777 cd = [cd stringByAppendingString: _name];
778 cd = [cd stringByAppendingString: @"\""];
780 // TODO: add size parameter (useful addition, RFC 2183)
784 - (NGMimeBodyPart *) bodyPartForAttachmentWithName: (NSString *) _name
787 NGMutableHashMap *map;
788 NGMimeBodyPart *bodyPart;
791 BOOL attachAsString, is7bit;
795 if (_name == nil) return nil;
797 /* check attachment */
799 fm = [NSFileManager defaultManager];
800 p = [self pathToAttachmentWithName:_name];
801 if (![fm isReadableFileAtPath:p]) {
802 [self errorWithFormat: @"did not find attachment: '%@'", _name];
808 /* prepare header of body part */
810 map = [[[NGMutableHashMap alloc] initWithCapacity:4] autorelease];
812 if ((s = [self contentTypeForAttachmentWithName:_name]) != nil) {
813 [map setObject:s forKey: @"content-type"];
814 if ([s hasPrefix: @"text/"])
815 attachAsString = YES;
816 else if ([s hasPrefix: @"message/rfc822"])
819 if ((s = [self contentDispositionForAttachmentWithName:_name]))
820 [map setObject:s forKey: @"content-disposition"];
822 /* prepare body content */
824 if (attachAsString) { // TODO: is this really necessary?
827 content = [[NSData alloc] initWithContentsOfMappedFile:p];
829 s = [[NSString alloc] initWithData:content
830 encoding:[NSString defaultCStringEncoding]];
833 [content release]; content = nil;
836 [self warnWithFormat:
837 @"could not get text attachment as string: '%@'", _name];
844 Note: Apparently NGMimeFileData objects are not processed by the MIME
847 body = [[NGMimeFileData alloc] initWithPath:p removeFile:NO];
848 [map setObject: @"7bit" forKey: @"content-transfer-encoding"];
849 [map setObject:[NSNumber numberWithInt:[body length]]
850 forKey: @"content-length"];
854 Note: in OGo this is done in LSWImapMailEditor.m:2477. Apparently
855 NGMimeFileData objects are not processed by the MIME generator!
859 content = [[NSData alloc] initWithContentsOfMappedFile:p];
860 encoded = [content dataByEncodingBase64];
861 [content release]; content = nil;
863 [map setObject: @"base64" forKey: @"content-transfer-encoding"];
864 [map setObject:[NSNumber numberWithInt:[encoded length]]
865 forKey: @"content-length"];
867 /* Note: the -init method will create a temporary file! */
868 body = [[NGMimeFileData alloc] initWithBytes:[encoded bytes]
869 length:[encoded length]];
872 bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease];
873 [bodyPart setBody:body];
875 [body release]; body = nil;
879 - (NSArray *) bodyPartsForAllAttachments
881 /* returns nil on error */
884 NGMimeBodyPart *bodyPart;
885 NSMutableArray *bodyParts;
887 names = [self fetchAttachmentNames];
888 count = [names count];
889 bodyParts = [NSMutableArray arrayWithCapacity: count];
891 for (i = 0; i < count; i++)
893 bodyPart = [self bodyPartForAttachmentWithName: [names objectAtIndex: i]];
894 [bodyParts addObject: bodyPart];
900 - (NGMimeMessage *) mimeMultiPartMessageWithHeaderMap: (NGMutableHashMap *) map
901 andBodyParts: (NSArray *) _bodyParts
903 NGMimeMessage *message;
904 NGMimeMultipartBody *mBody;
905 NGMimeBodyPart *part;
908 [map addObject: MultiMixedType forKey: @"content-type"];
910 message = [[NGMimeMessage alloc] initWithHeader: map];
911 [message autorelease];
912 mBody = [[NGMimeMultipartBody alloc] initWithPart: message];
914 part = [self bodyPartForText];
915 [mBody addBodyPart: part];
917 e = [_bodyParts objectEnumerator];
918 part = [e nextObject];
921 [mBody addBodyPart: part];
922 part = [e nextObject];
925 [message setBody: mBody];
931 - (void) _addHeaders: (NSDictionary *) _h
932 toHeaderMap: (NGMutableHashMap *) _map
940 names = [_h keyEnumerator];
941 while ((name = [names nextObject]) != nil) {
944 value = [_h objectForKey:name];
945 [_map addObject:value forKey:name];
949 - (BOOL) isEmptyValue: (id) _value
951 if (![_value isNotNull])
954 if ([_value isKindOfClass: [NSArray class]])
955 return [_value count] == 0 ? YES : NO;
957 if ([_value isKindOfClass: [NSString class]])
958 return [_value length] == 0 ? YES : NO;
963 - (NGMutableHashMap *) mimeHeaderMapWithHeaders: (NSDictionary *) _headers
965 NGMutableHashMap *map;
967 NSString *s, *dateString;
970 map = [[[NGMutableHashMap alloc] initWithCapacity:16] autorelease];
974 if ((emails = [headers objectForKey: @"to"]) != nil)
975 [map setObjects: emails forKey: @"to"];
976 if ((emails = [headers objectForKey: @"cc"]) != nil)
977 [map setObjects:emails forKey: @"cc"];
978 if ((emails = [headers objectForKey: @"bcc"]) != nil)
979 [map setObjects:emails forKey: @"bcc"];
983 from = [headers objectForKey: @"from"];
984 replyTo = [headers objectForKey: @"replyTo"];
986 if (![self isEmptyValue:from]) {
987 if ([from isKindOfClass:[NSArray class]])
988 [map setObjects: from forKey: @"from"];
990 [map setObject: from forKey: @"from"];
993 if (![self isEmptyValue: replyTo]) {
994 if ([from isKindOfClass:[NSArray class]])
995 [map setObjects:from forKey: @"reply-to"];
997 [map setObject:from forKey: @"reply-to"];
999 else if (![self isEmptyValue:from])
1000 [map setObjects:[map objectsForKey: @"from"] forKey: @"reply-to"];
1004 if ([(s = [headers objectForKey: @"subject"]) length] > 0)
1005 [map setObject: [s asQPSubjectString: @"utf-8"]
1006 forKey: @"subject"];
1007 // [map setObject: [s asQPSubjectString: @"utf-8"] forKey: @"subject"];
1009 /* add standard headers */
1011 dateString = [[NSCalendarDate date] rfc822DateString];
1012 [map addObject: dateString forKey: @"date"];
1013 [map addObject: @"1.0" forKey: @"MIME-Version"];
1014 [map addObject: userAgent forKey: @"X-Mailer"];
1016 /* add custom headers */
1018 // [self _addHeaders: [lInfo objectForKey: @"headers"] toHeaderMap:map];
1019 [self _addHeaders: _headers toHeaderMap: map];
1024 - (NGMimeMessage *) mimeMessageWithHeaders: (NSDictionary *) _headers
1026 NGMutableHashMap *map;
1028 NGMimeMessage *message;
1032 map = [self mimeHeaderMapWithHeaders: _headers];
1035 [self debugWithFormat: @"MIME Envelope: %@", map];
1037 bodyParts = [self bodyPartsForAllAttachments];
1040 [self debugWithFormat: @"attachments: %@", bodyParts];
1042 if ([bodyParts count] == 0)
1043 /* no attachments */
1044 message = [self mimeMessageForContentWithHeaderMap: map];
1046 /* attachments, create multipart/mixed */
1047 message = [self mimeMultiPartMessageWithHeaderMap: map
1048 andBodyParts: bodyParts];
1049 [self debugWithFormat: @"message: %@", message];
1052 [self errorWithFormat:
1053 @"could not create body parts for attachments!"];
1059 - (NGMimeMessage *) mimeMessage
1061 return [self mimeMessageWithHeaders: nil];
1064 - (NSData *) mimeMessageAsData
1066 NGMimeMessageGenerator *generator;
1069 generator = [NGMimeMessageGenerator new];
1070 message = [generator generateMimeFromPart: [self mimeMessage]];
1071 [generator release];
1076 - (NSArray *) allRecipients
1078 NSMutableArray *allRecipients;
1079 NSArray *recipients;
1080 NSString *fieldNames[] = {@"to", @"cc", @"bcc"};
1083 allRecipients = [NSMutableArray arrayWithCapacity: 16];
1085 for (count = 0; count < 3; count++)
1087 recipients = [headers objectForKey: fieldNames[count]];
1088 if ([recipients count] > 0)
1089 [allRecipients addObjectsFromArray: recipients];
1092 return allRecipients;
1095 - (NSException *) sendMail
1098 SOGoMailFolder *sentFolder;
1100 NSURL *sourceIMAP4URL;
1103 sentFolder = [[self mailAccountFolder] sentFolderInContext: context];
1104 if ([sentFolder isKindOfClass: [NSException class]])
1105 error = (NSException *) sentFolder;
1108 message = [self mimeMessageAsData];
1109 error = [[SOGoMailer sharedMailer] sendMailData: message
1110 toRecipients: [self allRecipients]
1111 sender: [self sender]];
1114 error = [sentFolder postData: message flags: @"seen"];
1117 [self imap4Connection];
1119 [imap4 markURLDeleted: [self imap4URL]];
1120 if (sourceURL && sourceFlag)
1122 sourceIMAP4URL = [NSURL URLWithString: sourceURL];
1123 [imap4 addFlags: sourceFlag toURL: sourceIMAP4URL];
1125 if (!draftDeleteDisabled)
1126 error = [self delete];
1134 - (NSException *) delete
1138 if ([[NSFileManager defaultManager]
1139 removeFileAtPath: [self draftFolderPath]
1143 error = [NSException exceptionWithHTTPStatus: 500 /* server error */
1144 reason: @"could not delete draft"];
1151 - (NSString *) contentAsString
1156 message = [self mimeMessageAsData];
1159 str = [[NSString alloc] initWithData: message
1160 encoding: NSUTF8StringEncoding];
1162 [self errorWithFormat: @"could not load draft as UTF-8 (data size=%d)",
1169 [self errorWithFormat: @"message data is empty"];
1178 - (BOOL) isDebuggingEnabled
1183 @end /* SOGoDraftObject */