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",
71 @implementation SOGoDraftObject
73 static NGMimeType *TextPlainType = nil;
74 static NGMimeType *MultiMixedType = nil;
75 static NSString *userAgent = @"SOGoMail 1.0";
76 static BOOL draftDeleteDisabled = NO; // for debugging
77 static BOOL debugOn = NO;
78 static BOOL showTextAttachmentsInline = NO;
82 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
84 /* Note: be aware of the charset issues before enabling this! */
85 showTextAttachmentsInline = [ud boolForKey: @"SOGoShowTextAttachmentsInline"];
87 if ((draftDeleteDisabled = [ud boolForKey: @"SOGoNoDraftDeleteAfterSend"]))
88 NSLog(@"WARNING: draft delete is disabled! (SOGoNoDraftDeleteAfterSend)");
90 TextPlainType = [[NGMimeType mimeType: @"text" subType: @"plain"] copy];
91 MultiMixedType = [[NGMimeType mimeType: @"multipart" subType: @"mixed"] copy];
96 if ((self = [super init]))
99 headers = [NSMutableDictionary new];
116 [sourceFlag release];
121 /* draft folder functionality */
123 - (NSString *) userSpoolFolderPath
125 return [[self container] userSpoolFolderPath];
128 /* draft object functionality */
130 - (NSString *) draftFolderPath
134 path = [[self userSpoolFolderPath] stringByAppendingPathComponent:
142 - (BOOL) _ensureDraftFolderPath
146 fm = [NSFileManager defaultManager];
148 return ([fm createDirectoriesAtPath: [container userSpoolFolderPath]
150 && [fm createDirectoriesAtPath: [self draftFolderPath]
154 - (NSString *) infoPath
156 return [[self draftFolderPath]
157 stringByAppendingPathComponent: @".info.plist"];
162 - (void) setHeaders: (NSDictionary *) newHeaders
167 for (count = 0; count < 7; count++)
169 headerValue = [newHeaders objectForKey: headerKeys[count]];
171 [headers setObject: headerValue
172 forKey: headerKeys[count]];
174 [headers removeObjectForKey: headerKeys[count]];
178 - (NSDictionary *) headers
183 - (void) setText: (NSString *) newText
185 ASSIGN (text, newText);
193 - (void) setInReplyTo: (NSString *) newInReplyTo
195 ASSIGN (inReplyTo, newInReplyTo);
198 - (void) setSourceURL: (NSString *) newSourceURL
200 ASSIGN (sourceURL, newSourceURL);
203 - (void) setSourceFlag: (NSString *) newSourceFlag
205 ASSIGN (sourceFlag, newSourceFlag);
208 - (NSException *) storeInfo
210 NSMutableDictionary *infos;
213 if ([self _ensureDraftFolderPath])
215 infos = [NSMutableDictionary new];
216 [infos setObject: headers forKey: @"headers"];
218 [infos setObject: text forKey: @"text"];
220 [infos setObject: inReplyTo forKey: @"inReplyTo"];
222 [infos setObject: [NSNumber numberWithInt: IMAP4ID]
224 if (sourceURL && sourceFlag)
226 [infos setObject: sourceURL forKey: @"sourceURL"];
227 [infos setObject: sourceFlag forKey: @"sourceFlag"];
230 if ([infos writeToFile: [self infoPath] atomically:YES])
234 [self errorWithFormat: @"could not write info: '%@'",
236 error = [NSException exceptionWithHTTPStatus:500 /* server error */
237 reason: @"could not write draft info!"];
244 [self errorWithFormat: @"could not create folder for draft: '%@'",
245 [self draftFolderPath]];
246 error = [NSException exceptionWithHTTPStatus:500 /* server error */
247 reason: @"could not create folder for draft!"];
253 - (void) _loadInfosFromDictionary: (NSDictionary *) infoDict
257 value = [infoDict objectForKey: @"headers"];
259 [self setHeaders: value];
261 value = [infoDict objectForKey: @"text"];
262 if ([value length] > 0)
263 [self setText: value];
265 value = [infoDict objectForKey: @"IMAP4ID"];
267 [self setIMAP4ID: [value intValue]];
269 value = [infoDict objectForKey: @"sourceURL"];
271 [self setSourceURL: value];
272 value = [infoDict objectForKey: @"sourceFlag"];
274 [self setSourceFlag: value];
276 value = [infoDict objectForKey: @"inReplyTo"];
278 [self setInReplyTo: value];
281 - (NSString *) relativeImap4Name
283 return [NSString stringWithFormat: @"%d", IMAP4ID];
294 fm = [NSFileManager defaultManager];
295 if ([fm fileExistsAtPath: p])
297 infos = [NSDictionary dictionaryWithContentsOfFile: p];
299 [self _loadInfosFromDictionary: infos];
301 // [self errorWithFormat: @"draft info dictionary broken at path: %@", p];
304 [self debugWithFormat: @"Note: info object does not yet exist: %@", p];
307 - (void) setIMAP4ID: (int) newIMAP4ID
309 IMAP4ID = newIMAP4ID;
317 - (int) _IMAP4IDFromAppendResult: (NSDictionary *) result
319 NSDictionary *results;
320 NSString *flag, *newIdString;
322 results = [[result objectForKey: @"RawResponse"]
323 objectForKey: @"ResponseResult"];
324 flag = [results objectForKey: @"flag"];
325 newIdString = [[flag componentsSeparatedByString: @" "] objectAtIndex: 2];
327 return [newIdString intValue];
330 - (NSException *) save
332 NGImap4Client *client;
339 message = [self mimeMessageAsData];
341 client = [[self imap4Connection] client];
342 folder = [imap4 imap4FolderNameForURL: [container imap4URL]];
344 = [client append: message toFolder: folder
345 withFlags: [NSArray arrayWithObjects: @"seen", @"draft", nil]];
346 if ([[result objectForKey: @"result"] boolValue])
349 error = [imap4 markURLDeleted: [self imap4URL]];
350 IMAP4ID = [self _IMAP4IDFromAppendResult: result];
354 error = [NSException exceptionWithHTTPStatus:500 /* Server Error */
355 reason: @"Failed to store message"];
360 - (void) _addEMailsOfAddresses: (NSArray *) _addrs
361 toArray: (NSMutableArray *) _ma
365 for (i = 0, count = [_addrs count]; i < count; i++)
367 [(NGImap4EnvelopeAddress *) [_addrs objectAtIndex: i] email]];
370 - (void) _fillInReplyAddresses: (NSMutableDictionary *) _info
371 replyToAll: (BOOL) _replyToAll
372 envelope: (NGImap4Envelope *) _envelope
375 The rules as implemented by Thunderbird:
376 - if there is a 'reply-to' header, only include that (as TO)
377 - if we reply to all, all non-from addresses are added as CC
378 - the from is always the lone TO (except for reply-to)
380 Note: we cannot check reply-to, because Cyrus even sets a reply-to in the
381 envelope if none is contained in the message itself! (bug or
384 TODO: what about sender (RFC 822 3.6.2)
389 to = [NSMutableArray arrayWithCapacity:2];
391 /* first check for "reply-to" */
393 addrs = [_envelope replyTo];
394 if ([addrs count] == 0)
395 /* no "reply-to", try "from" */
396 addrs = [_envelope from];
398 [self _addEMailsOfAddresses: addrs toArray: to];
399 [_info setObject: to forKey: @"to"];
401 /* CC processing if we reply-to-all: add all 'to' and 'cc' */
405 to = [NSMutableArray arrayWithCapacity:8];
407 [self _addEMailsOfAddresses: [_envelope to] toArray: to];
408 [self _addEMailsOfAddresses: [_envelope cc] toArray: to];
410 [_info setObject: to forKey: @"cc"];
414 - (void) _fetchAttachments: (NSArray *) parts
415 fromMail: (SOGoMailObject *) sourceMail
417 unsigned int count, max;
418 NSDictionary *currentPart, *attachment, *body;
419 NSArray *paths, *result;
424 paths = [parts keysWithFormat: @"BODY[%{path}]"];
425 result = [[sourceMail fetchParts: paths] objectForKey: @"fetch"];
426 for (count = 0; count < max; count++)
428 currentPart = [parts objectAtIndex: count];
429 body = [[result objectAtIndex: count] objectForKey: @"body"];
430 attachment = [NSDictionary dictionaryWithObjectsAndKeys:
431 [currentPart objectForKey: @"filename"],
433 [currentPart objectForKey: @"mimetype"],
436 [self saveAttachment: [body objectForKey: @"data"]
437 withMetadata: attachment];
442 - (void) fetchMailForEditing: (SOGoMailObject *) sourceMail
445 NSMutableDictionary *info;
446 NSMutableArray *addresses;
447 NGImap4Envelope *sourceEnvelope;
449 [sourceMail fetchCoreInfos];
451 [self _fetchAttachments: [sourceMail fetchFileAttachmentKeys]
452 fromMail: sourceMail];
453 info = [NSMutableDictionary dictionaryWithCapacity: 16];
454 subject = [sourceMail subject];
455 if ([subject length] > 0)
456 [info setObject: subject forKey: @"subject"];
458 sourceEnvelope = [sourceMail envelope];
459 addresses = [NSMutableArray array];
460 [self _addEMailsOfAddresses: [sourceEnvelope to] toArray: addresses];
461 [info setObject: addresses forKey: @"to"];
462 addresses = [NSMutableArray array];
463 [self _addEMailsOfAddresses: [sourceEnvelope cc] toArray: addresses];
464 if ([addresses count] > 0)
465 [info setObject: addresses forKey: @"cc"];
466 addresses = [NSMutableArray array];
467 [self _addEMailsOfAddresses: [sourceEnvelope bcc] toArray: addresses];
468 if ([addresses count] > 0)
469 [info setObject: addresses forKey: @"bcc"];
470 addresses = [NSMutableArray array];
471 [self _addEMailsOfAddresses: [sourceEnvelope replyTo] toArray: addresses];
472 if ([addresses count] > 0)
473 [info setObject: addresses forKey: @"replyTo"];
474 [self setHeaders: info];
476 [self setText: [sourceMail contentForEditing]];
477 [self setSourceURL: [sourceMail imap4URLString]];
478 IMAP4ID = [[sourceMail nameInContainer] intValue];
483 - (void) fetchMailForReplying: (SOGoMailObject *) sourceMail
486 NSString *contentForReply, *msgID;
487 NSMutableDictionary *info;
488 NGImap4Envelope *sourceEnvelope;
490 [sourceMail fetchCoreInfos];
492 info = [NSMutableDictionary dictionaryWithCapacity: 16];
493 [info setObject: [sourceMail subjectForReply] forKey: @"subject"];
495 sourceEnvelope = [sourceMail envelope];
496 [self _fillInReplyAddresses: info replyToAll: toAll
497 envelope: sourceEnvelope];
498 msgID = [sourceEnvelope messageID];
499 if ([msgID length] > 0)
500 [self setInReplyTo: msgID];
501 contentForReply = [sourceMail contentForReply];
502 [self setText: contentForReply];
503 [self setHeaders: info];
504 [self setSourceURL: [sourceMail imap4URLString]];
505 [self setSourceFlag: @"Answered"];
509 - (void) fetchMailForForwarding: (SOGoMailObject *) sourceMail
511 NSDictionary *info, *attachment;
512 SOGoUser *currentUser;
514 [sourceMail fetchCoreInfos];
516 info = [NSDictionary dictionaryWithObject: [sourceMail subjectForForward]
518 [self setHeaders: info];
519 [self setSourceURL: [sourceMail imap4URLString]];
520 [self setSourceFlag: @"$Forwarded"];
523 currentUser = [context activeUser];
524 if ([[currentUser messageForwarding] isEqualToString: @"inline"])
525 [self setText: [sourceMail contentForInlineForward]];
528 // TODO: use subject for filename?
529 // error = [newDraft saveAttachment:content withName:@"forward.mail"];
530 attachment = [NSDictionary dictionaryWithObjectsAndKeys:
531 [sourceMail filenameForForward], @"filename",
532 @"message/rfc822", @"mime-type",
534 [self saveAttachment: [sourceMail content]
535 withMetadata: attachment];
542 - (NSString *) sender
546 if ((tmp = [headers objectForKey: @"from"]) == nil)
548 if ([tmp isKindOfClass:[NSArray class]])
549 return [tmp count] > 0 ? [tmp objectAtIndex: 0] : nil;
556 - (NSArray *) fetchAttachmentNames
564 fm = [NSFileManager defaultManager];
565 files = [fm directoryContentsAtPath: [self draftFolderPath]];
568 ma = [NSMutableArray arrayWithCapacity: max];
569 for (count = 0; count < max; count++)
571 filename = [files objectAtIndex: count];
572 if (![filename hasPrefix: @"."])
573 [ma addObject: filename];
579 - (BOOL) isValidAttachmentName: (NSString *) _name
581 static NSString *sescape[] = { @"/", @"..", @"~", @"\"", @"'", nil };
585 if (![_name isNotNull]) return NO;
586 if ([_name length] == 0) return NO;
587 if ([_name hasPrefix: @"."]) return NO;
589 for (i = 0; sescape[i] != nil; i++) {
590 r = [_name rangeOfString:sescape[i]];
591 if (r.length > 0) return NO;
596 - (NSString *) pathToAttachmentWithName: (NSString *) _name
598 if ([_name length] == 0)
601 return [[self draftFolderPath] stringByAppendingPathComponent:_name];
604 - (NSException *) invalidAttachmentNameError: (NSString *) _name
606 return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
607 reason: @"Invalid attachment name!"];
610 - (NSException *) saveAttachment: (NSData *) _attach
611 withMetadata: (NSDictionary *) metadata
613 NSString *p, *name, *mimeType;
616 if (![_attach isNotNull]) {
617 return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
618 reason: @"Missing attachment content!"];
621 if (![self _ensureDraftFolderPath]) {
622 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
623 reason: @"Could not create folder for draft!"];
626 name = [metadata objectForKey: @"filename"];
627 r = [name rangeOfString: @"\\"
628 options: NSBackwardsSearch];
630 name = [name substringFromIndex: r.location + 1];
632 if (![self isValidAttachmentName: name])
633 return [self invalidAttachmentNameError: name];
635 p = [self pathToAttachmentWithName: name];
636 if (![_attach writeToFile: p atomically: YES])
638 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
639 reason: @"Could not write attachment to draft!"];
642 mimeType = [metadata objectForKey: @"mime-type"];
643 if ([mimeType length] > 0)
645 p = [self pathToAttachmentWithName:
646 [NSString stringWithFormat: @".%@.mime", name]];
647 if (![[mimeType dataUsingEncoding: NSUTF8StringEncoding]
648 writeToFile: p atomically: YES])
650 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
651 reason: @"Could not write attachment to draft!"];
655 return nil; /* everything OK */
658 - (NSException *) deleteAttachmentWithName: (NSString *) _name
666 if ([self isValidAttachmentName:_name])
668 fm = [NSFileManager defaultManager];
669 p = [self pathToAttachmentWithName:_name];
670 if ([fm fileExistsAtPath: p])
671 if (![fm removeFileAtPath: p handler: nil])
673 = [NSException exceptionWithHTTPStatus: 500 /* Server Error */
674 reason: @"Could not delete attachment from draft!"];
677 error = [self invalidAttachmentNameError:_name];
682 /* NGMime representations */
684 - (NGMimeBodyPart *) bodyPartForText
687 This add the text typed by the user (the primary plain/text part).
689 NGMutableHashMap *map;
690 NGMimeBodyPart *bodyPart;
692 /* prepare header of body part */
694 map = [[[NGMutableHashMap alloc] initWithCapacity: 1] autorelease];
696 // TODO: set charset in header!
697 [map setObject: @"text/plain" forKey: @"content-type"];
699 [map setObject: contentTypeValue forKey: @"content-type"];
701 // if ((body = text) != nil) {
702 // if ([body isKindOfClass: [NSString class]]) {
703 // [map setObject: contentTypeValue
704 // forKey: @"content-type"];
705 // // body = [body dataUsingEncoding:NSUTF8StringEncoding];
709 /* prepare body content */
711 bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease];
712 [bodyPart setBody: text];
717 - (NGMimeMessage *) mimeMessageForContentWithHeaderMap: (NGMutableHashMap *) map
719 NGMimeMessage *message;
723 [map setObject: @"text/plain" forKey: @"content-type"];
727 // if ([body isKindOfClass:[NSString class]])
728 /* Note: just 'utf8' is displayed wrong in Mail.app */
729 [map setObject: contentTypeValue
730 forKey: @"content-type"];
731 // body = [body dataUsingEncoding:NSUTF8StringEncoding];
732 // else if ([body isKindOfClass:[NSData class]] && addSuffix) {
733 // body = [[body mutableCopy] autorelease];
735 // else if (addSuffix) {
736 // [self warnWithFormat: @"Note: cannot add Internet marker to body: %@",
737 // NSStringFromClass([body class])];
740 message = [[[NGMimeMessage alloc] initWithHeader:map] autorelease];
741 [message setBody: body];
750 - (NSString *) mimeTypeForExtension: (NSString *) _ext
752 // TODO: make configurable
753 // TODO: use /etc/mime-types
754 if ([_ext isEqualToString: @"txt"]) return @"text/plain";
755 if ([_ext isEqualToString: @"html"]) return @"text/html";
756 if ([_ext isEqualToString: @"htm"]) return @"text/html";
757 if ([_ext isEqualToString: @"gif"]) return @"image/gif";
758 if ([_ext isEqualToString: @"jpg"]) return @"image/jpeg";
759 if ([_ext isEqualToString: @"jpeg"]) return @"image/jpeg";
760 if ([_ext isEqualToString: @"mail"]) return @"message/rfc822";
761 return @"application/octet-stream";
764 - (NSString *) contentTypeForAttachmentWithName: (NSString *) _name
769 p = [self pathToAttachmentWithName:
770 [NSString stringWithFormat: @".%@.mime", _name]];
771 mimeData = [NSData dataWithContentsOfFile: p];
774 s = [[NSString alloc] initWithData: mimeData
775 encoding: NSUTF8StringEncoding];
780 s = [self mimeTypeForExtension:[_name pathExtension]];
781 if ([_name length] > 0)
782 s = [s stringByAppendingFormat: @"; name=\"%@\"", _name];
788 - (NSString *) contentDispositionForAttachmentWithName: (NSString *) _name
794 type = [self contentTypeForAttachmentWithName:_name];
796 if ([type hasPrefix: @"text/"])
797 cdtype = showTextAttachmentsInline ? @"inline" : @"attachment";
798 else if ([type hasPrefix: @"image/"] || [type hasPrefix: @"message"])
801 cdtype = @"attachment";
803 cd = [cdtype stringByAppendingString: @"; filename=\""];
804 cd = [cd stringByAppendingString: _name];
805 cd = [cd stringByAppendingString: @"\""];
807 // TODO: add size parameter (useful addition, RFC 2183)
811 - (NGMimeBodyPart *) bodyPartForAttachmentWithName: (NSString *) _name
814 NGMutableHashMap *map;
815 NGMimeBodyPart *bodyPart;
818 BOOL attachAsString, is7bit;
822 if (_name == nil) return nil;
824 /* check attachment */
826 fm = [NSFileManager defaultManager];
827 p = [self pathToAttachmentWithName:_name];
828 if (![fm isReadableFileAtPath:p]) {
829 [self errorWithFormat: @"did not find attachment: '%@'", _name];
835 /* prepare header of body part */
837 map = [[[NGMutableHashMap alloc] initWithCapacity:4] autorelease];
839 if ((s = [self contentTypeForAttachmentWithName:_name]) != nil) {
840 [map setObject:s forKey: @"content-type"];
841 if ([s hasPrefix: @"text/"])
842 attachAsString = YES;
843 else if ([s hasPrefix: @"message/rfc822"])
846 if ((s = [self contentDispositionForAttachmentWithName:_name]))
847 [map setObject:s forKey: @"content-disposition"];
849 /* prepare body content */
851 if (attachAsString) { // TODO: is this really necessary?
854 content = [[NSData alloc] initWithContentsOfMappedFile:p];
856 s = [[NSString alloc] initWithData:content
857 encoding:[NSString defaultCStringEncoding]];
860 [content release]; content = nil;
863 [self warnWithFormat:
864 @"could not get text attachment as string: '%@'", _name];
871 Note: Apparently NGMimeFileData objects are not processed by the MIME
874 body = [[NGMimeFileData alloc] initWithPath:p removeFile:NO];
875 [map setObject: @"7bit" forKey: @"content-transfer-encoding"];
876 [map setObject:[NSNumber numberWithInt:[body length]]
877 forKey: @"content-length"];
881 Note: in OGo this is done in LSWImapMailEditor.m:2477. Apparently
882 NGMimeFileData objects are not processed by the MIME generator!
886 content = [[NSData alloc] initWithContentsOfMappedFile:p];
887 encoded = [content dataByEncodingBase64];
888 [content release]; content = nil;
890 [map setObject: @"base64" forKey: @"content-transfer-encoding"];
891 [map setObject:[NSNumber numberWithInt:[encoded length]]
892 forKey: @"content-length"];
894 /* Note: the -init method will create a temporary file! */
895 body = [[NGMimeFileData alloc] initWithBytes:[encoded bytes]
896 length:[encoded length]];
899 bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease];
900 [bodyPart setBody:body];
902 [body release]; body = nil;
906 - (NSArray *) bodyPartsForAllAttachments
908 /* returns nil on error */
911 NGMimeBodyPart *bodyPart;
912 NSMutableArray *bodyParts;
914 names = [self fetchAttachmentNames];
915 count = [names count];
916 bodyParts = [NSMutableArray arrayWithCapacity: count];
918 for (i = 0; i < count; i++)
920 bodyPart = [self bodyPartForAttachmentWithName: [names objectAtIndex: i]];
921 [bodyParts addObject: bodyPart];
927 - (NGMimeMessage *) mimeMultiPartMessageWithHeaderMap: (NGMutableHashMap *) map
928 andBodyParts: (NSArray *) _bodyParts
930 NGMimeMessage *message;
931 NGMimeMultipartBody *mBody;
932 NGMimeBodyPart *part;
935 [map addObject: MultiMixedType forKey: @"content-type"];
937 message = [[NGMimeMessage alloc] initWithHeader: map];
938 [message autorelease];
939 mBody = [[NGMimeMultipartBody alloc] initWithPart: message];
941 part = [self bodyPartForText];
942 [mBody addBodyPart: part];
944 e = [_bodyParts objectEnumerator];
945 part = [e nextObject];
948 [mBody addBodyPart: part];
949 part = [e nextObject];
952 [message setBody: mBody];
958 - (void) _addHeaders: (NSDictionary *) _h
959 toHeaderMap: (NGMutableHashMap *) _map
967 names = [_h keyEnumerator];
968 while ((name = [names nextObject]) != nil) {
971 value = [_h objectForKey:name];
972 [_map addObject:value forKey:name];
976 - (BOOL) isEmptyValue: (id) _value
978 if (![_value isNotNull])
981 if ([_value isKindOfClass: [NSArray class]])
982 return [_value count] == 0 ? YES : NO;
984 if ([_value isKindOfClass: [NSString class]])
985 return [_value length] == 0 ? YES : NO;
990 - (NGMutableHashMap *) mimeHeaderMapWithHeaders: (NSDictionary *) _headers
992 NGMutableHashMap *map;
994 NSString *s, *dateString;
997 map = [[[NGMutableHashMap alloc] initWithCapacity:16] autorelease];
1001 if ((emails = [headers objectForKey: @"to"]) != nil)
1002 [map setObjects: emails forKey: @"to"];
1003 if ((emails = [headers objectForKey: @"cc"]) != nil)
1004 [map setObjects:emails forKey: @"cc"];
1005 if ((emails = [headers objectForKey: @"bcc"]) != nil)
1006 [map setObjects:emails forKey: @"bcc"];
1010 from = [headers objectForKey: @"from"];
1011 replyTo = [headers objectForKey: @"replyTo"];
1013 if (![self isEmptyValue:from]) {
1014 if ([from isKindOfClass:[NSArray class]])
1015 [map setObjects: from forKey: @"from"];
1017 [map setObject: from forKey: @"from"];
1020 if (![self isEmptyValue: replyTo]) {
1021 if ([from isKindOfClass:[NSArray class]])
1022 [map setObjects:from forKey: @"reply-to"];
1024 [map setObject:from forKey: @"reply-to"];
1026 else if (![self isEmptyValue:from])
1027 [map setObjects:[map objectsForKey: @"from"] forKey: @"reply-to"];
1031 [map setObject: inReplyTo forKey: @"in-reply-to"];
1033 if ([(s = [headers objectForKey: @"subject"]) length] > 0)
1034 [map setObject: [s asQPSubjectString: @"utf-8"]
1035 forKey: @"subject"];
1036 // [map setObject: [s asQPSubjectString: @"utf-8"] forKey: @"subject"];
1038 /* add standard headers */
1040 dateString = [[NSCalendarDate date] rfc822DateString];
1041 [map addObject: dateString forKey: @"date"];
1042 [map addObject: @"1.0" forKey: @"MIME-Version"];
1043 [map addObject: userAgent forKey: @"X-Mailer"];
1045 /* add custom headers */
1047 // [self _addHeaders: [lInfo objectForKey: @"headers"] toHeaderMap:map];
1048 [self _addHeaders: _headers toHeaderMap: map];
1053 - (NGMimeMessage *) mimeMessageWithHeaders: (NSDictionary *) _headers
1055 NGMutableHashMap *map;
1057 NGMimeMessage *message;
1061 map = [self mimeHeaderMapWithHeaders: _headers];
1064 [self debugWithFormat: @"MIME Envelope: %@", map];
1066 bodyParts = [self bodyPartsForAllAttachments];
1069 [self debugWithFormat: @"attachments: %@", bodyParts];
1071 if ([bodyParts count] == 0)
1072 /* no attachments */
1073 message = [self mimeMessageForContentWithHeaderMap: map];
1075 /* attachments, create multipart/mixed */
1076 message = [self mimeMultiPartMessageWithHeaderMap: map
1077 andBodyParts: bodyParts];
1078 [self debugWithFormat: @"message: %@", message];
1081 [self errorWithFormat:
1082 @"could not create body parts for attachments!"];
1088 - (NGMimeMessage *) mimeMessage
1090 return [self mimeMessageWithHeaders: nil];
1093 - (NSData *) mimeMessageAsData
1095 NGMimeMessageGenerator *generator;
1098 generator = [NGMimeMessageGenerator new];
1099 message = [generator generateMimeFromPart: [self mimeMessage]];
1100 [generator release];
1105 - (NSArray *) allRecipients
1107 NSMutableArray *allRecipients;
1108 NSArray *recipients;
1109 NSString *fieldNames[] = {@"to", @"cc", @"bcc"};
1112 allRecipients = [NSMutableArray arrayWithCapacity: 16];
1114 for (count = 0; count < 3; count++)
1116 recipients = [headers objectForKey: fieldNames[count]];
1117 if ([recipients count] > 0)
1118 [allRecipients addObjectsFromArray: recipients];
1121 return allRecipients;
1124 - (NSException *) sendMail
1127 SOGoMailFolder *sentFolder;
1129 NSURL *sourceIMAP4URL;
1132 sentFolder = [[self mailAccountFolder] sentFolderInContext: context];
1133 if ([sentFolder isKindOfClass: [NSException class]])
1134 error = (NSException *) sentFolder;
1137 message = [self mimeMessageAsData];
1138 error = [[SOGoMailer sharedMailer] sendMailData: message
1139 toRecipients: [self allRecipients]
1140 sender: [self sender]];
1143 error = [sentFolder postData: message flags: @"seen"];
1146 [self imap4Connection];
1148 [imap4 markURLDeleted: [self imap4URL]];
1149 if (sourceURL && sourceFlag)
1151 sourceIMAP4URL = [NSURL URLWithString: sourceURL];
1152 [imap4 addFlags: sourceFlag toURL: sourceIMAP4URL];
1154 if (!draftDeleteDisabled)
1155 error = [self delete];
1163 - (NSException *) delete
1167 if ([[NSFileManager defaultManager]
1168 removeFileAtPath: [self draftFolderPath]
1172 error = [NSException exceptionWithHTTPStatus: 500 /* server error */
1173 reason: @"could not delete draft"];
1180 - (NSString *) contentAsString
1185 message = [self mimeMessageAsData];
1188 str = [[NSString alloc] initWithData: message
1189 encoding: NSUTF8StringEncoding];
1191 [self errorWithFormat: @"could not load draft as UTF-8 (data size=%d)",
1198 [self errorWithFormat: @"message data is empty"];
1207 - (BOOL) isDebuggingEnabled
1212 @end /* SOGoDraftObject */