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>
60 #import "NSData+Mail.h"
61 #import "SOGoMailAccount.h"
62 #import "SOGoMailFolder.h"
63 #import "SOGoMailObject.h"
64 #import "SOGoMailObject+Draft.h"
66 #import "SOGoDraftObject.h"
68 static NSString *contentTypeValue = @"text/plain; charset=utf-8";
69 static NSString *headerKeys[] = {@"subject", @"to", @"cc", @"bcc",
73 @implementation SOGoDraftObject
75 static NGMimeType *TextPlainType = nil;
76 static NGMimeType *MultiMixedType = nil;
77 static NSString *userAgent = @"SOGoMail 1.0";
78 static BOOL draftDeleteDisabled = NO; // for debugging
79 static BOOL debugOn = NO;
80 static BOOL showTextAttachmentsInline = NO;
84 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
86 /* Note: be aware of the charset issues before enabling this! */
87 showTextAttachmentsInline = [ud boolForKey: @"SOGoShowTextAttachmentsInline"];
89 if ((draftDeleteDisabled = [ud boolForKey: @"SOGoNoDraftDeleteAfterSend"]))
90 NSLog(@"WARNING: draft delete is disabled! (SOGoNoDraftDeleteAfterSend)");
92 TextPlainType = [[NGMimeType mimeType: @"text" subType: @"plain"] copy];
93 MultiMixedType = [[NGMimeType mimeType: @"multipart" subType: @"mixed"] copy];
98 if ((self = [super init]))
101 headers = [NSMutableDictionary new];
118 [sourceFlag release];
123 /* draft folder functionality */
125 - (NSString *) userSpoolFolderPath
127 return [[self container] userSpoolFolderPath];
130 /* draft object functionality */
132 - (NSString *) draftFolderPath
136 path = [[self userSpoolFolderPath] stringByAppendingPathComponent:
144 - (BOOL) _ensureDraftFolderPath
148 fm = [NSFileManager defaultManager];
150 return ([fm createDirectoriesAtPath: [container userSpoolFolderPath]
152 && [fm createDirectoriesAtPath: [self draftFolderPath]
156 - (NSString *) infoPath
158 return [[self draftFolderPath]
159 stringByAppendingPathComponent: @".info.plist"];
164 - (void) setHeaders: (NSDictionary *) newHeaders
169 for (count = 0; count < 7; count++)
171 headerValue = [newHeaders objectForKey: headerKeys[count]];
173 [headers setObject: headerValue
174 forKey: headerKeys[count]];
176 [headers removeObjectForKey: headerKeys[count]];
180 - (NSDictionary *) headers
185 - (void) setText: (NSString *) newText
187 ASSIGN (text, newText);
195 - (void) setInReplyTo: (NSString *) newInReplyTo
197 ASSIGN (inReplyTo, newInReplyTo);
200 - (void) setSourceURL: (NSString *) newSourceURL
202 ASSIGN (sourceURL, newSourceURL);
205 - (void) setSourceFlag: (NSString *) newSourceFlag
207 ASSIGN (sourceFlag, newSourceFlag);
210 - (NSException *) storeInfo
212 NSMutableDictionary *infos;
215 if ([self _ensureDraftFolderPath])
217 infos = [NSMutableDictionary new];
218 [infos setObject: headers forKey: @"headers"];
220 [infos setObject: text forKey: @"text"];
222 [infos setObject: inReplyTo forKey: @"inReplyTo"];
224 [infos setObject: [NSNumber numberWithInt: IMAP4ID]
226 if (sourceURL && sourceFlag)
228 [infos setObject: sourceURL forKey: @"sourceURL"];
229 [infos setObject: sourceFlag forKey: @"sourceFlag"];
232 if ([infos writeToFile: [self infoPath] atomically:YES])
236 [self errorWithFormat: @"could not write info: '%@'",
238 error = [NSException exceptionWithHTTPStatus:500 /* server error */
239 reason: @"could not write draft info!"];
246 [self errorWithFormat: @"could not create folder for draft: '%@'",
247 [self draftFolderPath]];
248 error = [NSException exceptionWithHTTPStatus:500 /* server error */
249 reason: @"could not create folder for draft!"];
255 - (void) _loadInfosFromDictionary: (NSDictionary *) infoDict
259 value = [infoDict objectForKey: @"headers"];
261 [self setHeaders: value];
263 value = [infoDict objectForKey: @"text"];
264 if ([value length] > 0)
265 [self setText: value];
267 value = [infoDict objectForKey: @"IMAP4ID"];
269 [self setIMAP4ID: [value intValue]];
271 value = [infoDict objectForKey: @"sourceURL"];
273 [self setSourceURL: value];
274 value = [infoDict objectForKey: @"sourceFlag"];
276 [self setSourceFlag: value];
278 value = [infoDict objectForKey: @"inReplyTo"];
280 [self setInReplyTo: value];
283 - (NSString *) relativeImap4Name
285 return [NSString stringWithFormat: @"%d", IMAP4ID];
296 fm = [NSFileManager defaultManager];
297 if ([fm fileExistsAtPath: p])
299 infos = [NSDictionary dictionaryWithContentsOfFile: p];
301 [self _loadInfosFromDictionary: infos];
303 // [self errorWithFormat: @"draft info dictionary broken at path: %@", p];
306 [self debugWithFormat: @"Note: info object does not yet exist: %@", p];
309 - (void) setIMAP4ID: (int) newIMAP4ID
311 IMAP4ID = newIMAP4ID;
319 - (int) _IMAP4IDFromAppendResult: (NSDictionary *) result
321 NSDictionary *results;
322 NSString *flag, *newIdString;
324 results = [[result objectForKey: @"RawResponse"]
325 objectForKey: @"ResponseResult"];
326 flag = [results objectForKey: @"flag"];
327 newIdString = [[flag componentsSeparatedByString: @" "] objectAtIndex: 2];
329 return [newIdString intValue];
332 - (NSException *) save
334 NGImap4Client *client;
341 message = [self mimeMessageAsData];
343 client = [[self imap4Connection] client];
344 folder = [imap4 imap4FolderNameForURL: [container imap4URL]];
346 = [client append: message toFolder: folder
347 withFlags: [NSArray arrayWithObjects: @"seen", @"draft", nil]];
348 if ([[result objectForKey: @"result"] boolValue])
351 error = [imap4 markURLDeleted: [self imap4URL]];
352 IMAP4ID = [self _IMAP4IDFromAppendResult: result];
356 error = [NSException exceptionWithHTTPStatus:500 /* Server Error */
357 reason: @"Failed to store message"];
362 - (void) _addEMailsOfAddresses: (NSArray *) _addrs
363 toArray: (NSMutableArray *) _ma
367 for (i = 0, count = [_addrs count]; i < count; i++)
369 [(NGImap4EnvelopeAddress *) [_addrs objectAtIndex: i] email]];
372 - (void) _fillInReplyAddresses: (NSMutableDictionary *) _info
373 replyToAll: (BOOL) _replyToAll
374 envelope: (NGImap4Envelope *) _envelope
377 The rules as implemented by Thunderbird:
378 - if there is a 'reply-to' header, only include that (as TO)
379 - if we reply to all, all non-from addresses are added as CC
380 - the from is always the lone TO (except for reply-to)
382 Note: we cannot check reply-to, because Cyrus even sets a reply-to in the
383 envelope if none is contained in the message itself! (bug or
386 TODO: what about sender (RFC 822 3.6.2)
391 to = [NSMutableArray arrayWithCapacity:2];
393 /* first check for "reply-to" */
395 addrs = [_envelope replyTo];
396 if ([addrs count] == 0)
397 /* no "reply-to", try "from" */
398 addrs = [_envelope from];
400 [self _addEMailsOfAddresses: addrs toArray: to];
401 [_info setObject: to forKey: @"to"];
403 /* CC processing if we reply-to-all: add all 'to' and 'cc' */
407 to = [NSMutableArray arrayWithCapacity:8];
409 [self _addEMailsOfAddresses: [_envelope to] toArray: to];
410 [self _addEMailsOfAddresses: [_envelope cc] toArray: to];
412 [_info setObject: to forKey: @"cc"];
416 - (NSArray *) _attachmentBodiesFromPaths: (NSArray *) paths
417 fromResponseFetch: (NSDictionary *) fetch;
419 NSEnumerator *attachmentKeys;
420 NSMutableArray *bodies;
421 NSString *currentKey;
424 bodies = [NSMutableArray array];
426 attachmentKeys = [paths objectEnumerator];
427 while ((currentKey = [attachmentKeys nextObject]))
429 body = [fetch objectForKey: [currentKey lowercaseString]];
430 [bodies addObject: [body objectForKey: @"data"]];
436 - (void) _fetchAttachments: (NSArray *) parts
437 fromMail: (SOGoMailObject *) sourceMail
439 unsigned int count, max;
440 NSArray *paths, *bodies;
442 NSDictionary *currentInfo;
448 paths = [parts keysWithFormat: @"BODY[%{path}]"];
449 response = [[sourceMail fetchParts: paths] objectForKey: @"RawResponse"];
450 bodies = [self _attachmentBodiesFromPaths: paths
451 fromResponseFetch: [response objectForKey: @"fetch"]];
452 for (count = 0; count < max; count++)
454 currentInfo = [parts objectAtIndex: count];
455 body = [[bodies objectAtIndex: count]
456 bodyDataFromEncoding: [currentInfo
457 objectForKey: @"encoding"]];
458 [self saveAttachment: body withMetadata: currentInfo];
463 - (void) fetchMailForEditing: (SOGoMailObject *) sourceMail
466 NSMutableDictionary *info;
467 NSMutableArray *addresses;
468 NGImap4Envelope *sourceEnvelope;
470 [sourceMail fetchCoreInfos];
472 [self _fetchAttachments: [sourceMail fetchFileAttachmentKeys]
473 fromMail: sourceMail];
474 info = [NSMutableDictionary dictionaryWithCapacity: 16];
475 subject = [sourceMail subject];
476 if ([subject length] > 0)
477 [info setObject: subject forKey: @"subject"];
479 sourceEnvelope = [sourceMail envelope];
480 addresses = [NSMutableArray array];
481 [self _addEMailsOfAddresses: [sourceEnvelope to] toArray: addresses];
482 [info setObject: addresses forKey: @"to"];
483 addresses = [NSMutableArray array];
484 [self _addEMailsOfAddresses: [sourceEnvelope cc] toArray: addresses];
485 if ([addresses count] > 0)
486 [info setObject: addresses forKey: @"cc"];
487 addresses = [NSMutableArray array];
488 [self _addEMailsOfAddresses: [sourceEnvelope bcc] toArray: addresses];
489 if ([addresses count] > 0)
490 [info setObject: addresses forKey: @"bcc"];
491 addresses = [NSMutableArray array];
492 [self _addEMailsOfAddresses: [sourceEnvelope replyTo] toArray: addresses];
493 if ([addresses count] > 0)
494 [info setObject: addresses forKey: @"replyTo"];
495 [self setHeaders: info];
497 [self setText: [sourceMail contentForEditing]];
498 [self setSourceURL: [sourceMail imap4URLString]];
499 IMAP4ID = [[sourceMail nameInContainer] intValue];
504 - (void) fetchMailForReplying: (SOGoMailObject *) sourceMail
507 NSString *contentForReply, *msgID;
508 NSMutableDictionary *info;
509 NGImap4Envelope *sourceEnvelope;
511 [sourceMail fetchCoreInfos];
513 info = [NSMutableDictionary dictionaryWithCapacity: 16];
514 [info setObject: [sourceMail subjectForReply] forKey: @"subject"];
516 sourceEnvelope = [sourceMail envelope];
517 [self _fillInReplyAddresses: info replyToAll: toAll
518 envelope: sourceEnvelope];
519 msgID = [sourceEnvelope messageID];
520 if ([msgID length] > 0)
521 [self setInReplyTo: msgID];
522 contentForReply = [sourceMail contentForReply];
523 [self setText: contentForReply];
524 [self setHeaders: info];
525 [self setSourceURL: [sourceMail imap4URLString]];
526 [self setSourceFlag: @"Answered"];
530 - (void) fetchMailForForwarding: (SOGoMailObject *) sourceMail
532 NSDictionary *info, *attachment;
533 SOGoUser *currentUser;
535 [sourceMail fetchCoreInfos];
537 info = [NSDictionary dictionaryWithObject: [sourceMail subjectForForward]
539 [self setHeaders: info];
540 [self setSourceURL: [sourceMail imap4URLString]];
541 [self setSourceFlag: @"$Forwarded"];
544 currentUser = [context activeUser];
545 if ([[currentUser messageForwarding] isEqualToString: @"inline"])
546 [self setText: [sourceMail contentForInlineForward]];
549 // TODO: use subject for filename?
550 // error = [newDraft saveAttachment:content withName:@"forward.mail"];
551 attachment = [NSDictionary dictionaryWithObjectsAndKeys:
552 [sourceMail filenameForForward], @"filename",
553 @"message/rfc822", @"mimetype",
555 [self saveAttachment: [sourceMail content]
556 withMetadata: attachment];
563 - (NSString *) sender
567 if ((tmp = [headers objectForKey: @"from"]) == nil)
569 if ([tmp isKindOfClass:[NSArray class]])
570 return [tmp count] > 0 ? [tmp objectAtIndex: 0] : nil;
577 - (NSArray *) fetchAttachmentNames
585 fm = [NSFileManager defaultManager];
586 files = [fm directoryContentsAtPath: [self draftFolderPath]];
589 ma = [NSMutableArray arrayWithCapacity: max];
590 for (count = 0; count < max; count++)
592 filename = [files objectAtIndex: count];
593 if (![filename hasPrefix: @"."])
594 [ma addObject: filename];
600 - (BOOL) isValidAttachmentName: (NSString *) _name
602 static NSString *sescape[] = { @"/", @"..", @"~", @"\"", @"'", nil };
606 if (![_name isNotNull]) return NO;
607 if ([_name length] == 0) return NO;
608 if ([_name hasPrefix: @"."]) return NO;
610 for (i = 0; sescape[i] != nil; i++) {
611 r = [_name rangeOfString:sescape[i]];
612 if (r.length > 0) return NO;
617 - (NSString *) pathToAttachmentWithName: (NSString *) _name
619 if ([_name length] == 0)
622 return [[self draftFolderPath] stringByAppendingPathComponent:_name];
625 - (NSException *) invalidAttachmentNameError: (NSString *) _name
627 return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
628 reason: @"Invalid attachment name!"];
631 - (NSException *) saveAttachment: (NSData *) _attach
632 withMetadata: (NSDictionary *) metadata
634 NSString *p, *name, *mimeType;
637 if (![_attach isNotNull]) {
638 return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
639 reason: @"Missing attachment content!"];
642 if (![self _ensureDraftFolderPath]) {
643 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
644 reason: @"Could not create folder for draft!"];
647 name = [metadata objectForKey: @"filename"];
648 r = [name rangeOfString: @"\\"
649 options: NSBackwardsSearch];
651 name = [name substringFromIndex: r.location + 1];
653 if (![self isValidAttachmentName: name])
654 return [self invalidAttachmentNameError: name];
656 p = [self pathToAttachmentWithName: name];
657 if (![_attach writeToFile: p atomically: YES])
659 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
660 reason: @"Could not write attachment to draft!"];
663 mimeType = [metadata objectForKey: @"mimetype"];
664 if ([mimeType length] > 0)
666 p = [self pathToAttachmentWithName:
667 [NSString stringWithFormat: @".%@.mime", name]];
668 if (![[mimeType dataUsingEncoding: NSUTF8StringEncoding]
669 writeToFile: p atomically: YES])
671 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
672 reason: @"Could not write attachment to draft!"];
676 return nil; /* everything OK */
679 - (NSException *) deleteAttachmentWithName: (NSString *) _name
687 if ([self isValidAttachmentName:_name])
689 fm = [NSFileManager defaultManager];
690 p = [self pathToAttachmentWithName:_name];
691 if ([fm fileExistsAtPath: p])
692 if (![fm removeFileAtPath: p handler: nil])
694 = [NSException exceptionWithHTTPStatus: 500 /* Server Error */
695 reason: @"Could not delete attachment from draft!"];
698 error = [self invalidAttachmentNameError:_name];
703 /* NGMime representations */
705 - (NGMimeBodyPart *) bodyPartForText
708 This add the text typed by the user (the primary plain/text part).
710 NGMutableHashMap *map;
711 NGMimeBodyPart *bodyPart;
713 /* prepare header of body part */
715 map = [[[NGMutableHashMap alloc] initWithCapacity: 1] autorelease];
717 // TODO: set charset in header!
718 [map setObject: @"text/plain" forKey: @"content-type"];
720 [map setObject: contentTypeValue forKey: @"content-type"];
722 // if ((body = text) != nil) {
723 // if ([body isKindOfClass: [NSString class]]) {
724 // [map setObject: contentTypeValue
725 // forKey: @"content-type"];
726 // // body = [body dataUsingEncoding:NSUTF8StringEncoding];
730 /* prepare body content */
732 bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease];
733 [bodyPart setBody: text];
738 - (NGMimeMessage *) mimeMessageForContentWithHeaderMap: (NGMutableHashMap *) map
740 NGMimeMessage *message;
744 [map setObject: @"text/plain" forKey: @"content-type"];
748 // if ([body isKindOfClass:[NSString class]])
749 /* Note: just 'utf8' is displayed wrong in Mail.app */
750 [map setObject: contentTypeValue
751 forKey: @"content-type"];
752 // body = [body dataUsingEncoding:NSUTF8StringEncoding];
753 // else if ([body isKindOfClass:[NSData class]] && addSuffix) {
754 // body = [[body mutableCopy] autorelease];
756 // else if (addSuffix) {
757 // [self warnWithFormat: @"Note: cannot add Internet marker to body: %@",
758 // NSStringFromClass([body class])];
761 message = [[[NGMimeMessage alloc] initWithHeader:map] autorelease];
762 [message setBody: body];
771 - (NSString *) mimeTypeForExtension: (NSString *) _ext
773 // TODO: make configurable
774 // TODO: use /etc/mime-types
775 if ([_ext isEqualToString: @"txt"]) return @"text/plain";
776 if ([_ext isEqualToString: @"html"]) return @"text/html";
777 if ([_ext isEqualToString: @"htm"]) return @"text/html";
778 if ([_ext isEqualToString: @"gif"]) return @"image/gif";
779 if ([_ext isEqualToString: @"jpg"]) return @"image/jpeg";
780 if ([_ext isEqualToString: @"jpeg"]) return @"image/jpeg";
781 if ([_ext isEqualToString: @"mail"]) return @"message/rfc822";
782 return @"application/octet-stream";
785 - (NSString *) contentTypeForAttachmentWithName: (NSString *) _name
790 p = [self pathToAttachmentWithName:
791 [NSString stringWithFormat: @".%@.mime", _name]];
792 mimeData = [NSData dataWithContentsOfFile: p];
795 s = [[NSString alloc] initWithData: mimeData
796 encoding: NSUTF8StringEncoding];
801 s = [self mimeTypeForExtension:[_name pathExtension]];
802 if ([_name length] > 0)
803 s = [s stringByAppendingFormat: @"; name=\"%@\"", _name];
809 - (NSString *) contentDispositionForAttachmentWithName: (NSString *) _name
815 type = [self contentTypeForAttachmentWithName:_name];
817 if ([type hasPrefix: @"text/"])
818 cdtype = showTextAttachmentsInline ? @"inline" : @"attachment";
819 else if ([type hasPrefix: @"image/"] || [type hasPrefix: @"message"])
822 cdtype = @"attachment";
824 cd = [cdtype stringByAppendingString: @"; filename=\""];
825 cd = [cd stringByAppendingString: _name];
826 cd = [cd stringByAppendingString: @"\""];
828 // TODO: add size parameter (useful addition, RFC 2183)
832 - (NGMimeBodyPart *) bodyPartForAttachmentWithName: (NSString *) _name
835 NGMutableHashMap *map;
836 NGMimeBodyPart *bodyPart;
839 BOOL attachAsString, is7bit;
843 if (_name == nil) return nil;
845 /* check attachment */
847 fm = [NSFileManager defaultManager];
848 p = [self pathToAttachmentWithName:_name];
849 if (![fm isReadableFileAtPath:p]) {
850 [self errorWithFormat: @"did not find attachment: '%@'", _name];
856 /* prepare header of body part */
858 map = [[[NGMutableHashMap alloc] initWithCapacity:4] autorelease];
860 if ((s = [self contentTypeForAttachmentWithName:_name]) != nil) {
861 [map setObject:s forKey: @"content-type"];
862 if ([s hasPrefix: @"text/"])
863 attachAsString = YES;
864 else if ([s hasPrefix: @"message/rfc822"])
867 if ((s = [self contentDispositionForAttachmentWithName:_name]))
868 [map setObject:s forKey: @"content-disposition"];
870 /* prepare body content */
872 if (attachAsString) { // TODO: is this really necessary?
875 content = [[NSData alloc] initWithContentsOfMappedFile:p];
877 s = [[NSString alloc] initWithData:content
878 encoding:[NSString defaultCStringEncoding]];
881 [content release]; content = nil;
884 [self warnWithFormat:
885 @"could not get text attachment as string: '%@'", _name];
892 Note: Apparently NGMimeFileData objects are not processed by the MIME
895 body = [[NGMimeFileData alloc] initWithPath:p removeFile:NO];
896 [map setObject: @"7bit" forKey: @"content-transfer-encoding"];
897 [map setObject:[NSNumber numberWithInt:[body length]]
898 forKey: @"content-length"];
902 Note: in OGo this is done in LSWImapMailEditor.m:2477. Apparently
903 NGMimeFileData objects are not processed by the MIME generator!
907 content = [[NSData alloc] initWithContentsOfMappedFile:p];
908 encoded = [content dataByEncodingBase64];
909 [content release]; content = nil;
911 [map setObject: @"base64" forKey: @"content-transfer-encoding"];
912 [map setObject:[NSNumber numberWithInt:[encoded length]]
913 forKey: @"content-length"];
915 /* Note: the -init method will create a temporary file! */
916 body = [[NGMimeFileData alloc] initWithBytes:[encoded bytes]
917 length:[encoded length]];
920 bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease];
921 [bodyPart setBody:body];
923 [body release]; body = nil;
927 - (NSArray *) bodyPartsForAllAttachments
929 /* returns nil on error */
932 NGMimeBodyPart *bodyPart;
933 NSMutableArray *bodyParts;
935 names = [self fetchAttachmentNames];
936 count = [names count];
937 bodyParts = [NSMutableArray arrayWithCapacity: count];
939 for (i = 0; i < count; i++)
941 bodyPart = [self bodyPartForAttachmentWithName: [names objectAtIndex: i]];
942 [bodyParts addObject: bodyPart];
948 - (NGMimeMessage *) mimeMultiPartMessageWithHeaderMap: (NGMutableHashMap *) map
949 andBodyParts: (NSArray *) _bodyParts
951 NGMimeMessage *message;
952 NGMimeMultipartBody *mBody;
953 NGMimeBodyPart *part;
956 [map addObject: MultiMixedType forKey: @"content-type"];
958 message = [[NGMimeMessage alloc] initWithHeader: map];
959 [message autorelease];
960 mBody = [[NGMimeMultipartBody alloc] initWithPart: message];
962 part = [self bodyPartForText];
963 [mBody addBodyPart: part];
965 e = [_bodyParts objectEnumerator];
966 part = [e nextObject];
969 [mBody addBodyPart: part];
970 part = [e nextObject];
973 [message setBody: mBody];
979 - (void) _addHeaders: (NSDictionary *) _h
980 toHeaderMap: (NGMutableHashMap *) _map
988 names = [_h keyEnumerator];
989 while ((name = [names nextObject]) != nil) {
992 value = [_h objectForKey:name];
993 [_map addObject:value forKey:name];
997 - (BOOL) isEmptyValue: (id) _value
999 if (![_value isNotNull])
1002 if ([_value isKindOfClass: [NSArray class]])
1003 return [_value count] == 0 ? YES : NO;
1005 if ([_value isKindOfClass: [NSString class]])
1006 return [_value length] == 0 ? YES : NO;
1011 - (NGMutableHashMap *) mimeHeaderMapWithHeaders: (NSDictionary *) _headers
1013 NGMutableHashMap *map;
1015 NSString *s, *dateString;
1018 map = [[[NGMutableHashMap alloc] initWithCapacity:16] autorelease];
1020 /* add recipients */
1022 if ((emails = [headers objectForKey: @"to"]) != nil)
1023 [map setObjects: emails forKey: @"to"];
1024 if ((emails = [headers objectForKey: @"cc"]) != nil)
1025 [map setObjects:emails forKey: @"cc"];
1026 if ((emails = [headers objectForKey: @"bcc"]) != nil)
1027 [map setObjects:emails forKey: @"bcc"];
1031 from = [headers objectForKey: @"from"];
1032 replyTo = [headers objectForKey: @"replyTo"];
1034 if (![self isEmptyValue:from]) {
1035 if ([from isKindOfClass:[NSArray class]])
1036 [map setObjects: from forKey: @"from"];
1038 [map setObject: from forKey: @"from"];
1041 if (![self isEmptyValue: replyTo]) {
1042 if ([from isKindOfClass:[NSArray class]])
1043 [map setObjects:from forKey: @"reply-to"];
1045 [map setObject:from forKey: @"reply-to"];
1047 else if (![self isEmptyValue:from])
1048 [map setObjects:[map objectsForKey: @"from"] forKey: @"reply-to"];
1052 [map setObject: inReplyTo forKey: @"in-reply-to"];
1054 if ([(s = [headers objectForKey: @"subject"]) length] > 0)
1055 [map setObject: [s asQPSubjectString: @"utf-8"]
1056 forKey: @"subject"];
1057 // [map setObject: [s asQPSubjectString: @"utf-8"] forKey: @"subject"];
1059 /* add standard headers */
1061 dateString = [[NSCalendarDate date] rfc822DateString];
1062 [map addObject: dateString forKey: @"date"];
1063 [map addObject: @"1.0" forKey: @"MIME-Version"];
1064 [map addObject: userAgent forKey: @"X-Mailer"];
1066 /* add custom headers */
1068 // [self _addHeaders: [lInfo objectForKey: @"headers"] toHeaderMap:map];
1069 [self _addHeaders: _headers toHeaderMap: map];
1074 - (NGMimeMessage *) mimeMessageWithHeaders: (NSDictionary *) _headers
1076 NGMutableHashMap *map;
1078 NGMimeMessage *message;
1082 map = [self mimeHeaderMapWithHeaders: _headers];
1085 [self debugWithFormat: @"MIME Envelope: %@", map];
1087 bodyParts = [self bodyPartsForAllAttachments];
1090 [self debugWithFormat: @"attachments: %@", bodyParts];
1092 if ([bodyParts count] == 0)
1093 /* no attachments */
1094 message = [self mimeMessageForContentWithHeaderMap: map];
1096 /* attachments, create multipart/mixed */
1097 message = [self mimeMultiPartMessageWithHeaderMap: map
1098 andBodyParts: bodyParts];
1099 [self debugWithFormat: @"message: %@", message];
1102 [self errorWithFormat:
1103 @"could not create body parts for attachments!"];
1109 - (NGMimeMessage *) mimeMessage
1111 return [self mimeMessageWithHeaders: nil];
1114 - (NSData *) mimeMessageAsData
1116 NGMimeMessageGenerator *generator;
1119 generator = [NGMimeMessageGenerator new];
1120 message = [generator generateMimeFromPart: [self mimeMessage]];
1121 [generator release];
1126 - (NSArray *) allRecipients
1128 NSMutableArray *allRecipients;
1129 NSArray *recipients;
1130 NSString *fieldNames[] = {@"to", @"cc", @"bcc"};
1133 allRecipients = [NSMutableArray arrayWithCapacity: 16];
1135 for (count = 0; count < 3; count++)
1137 recipients = [headers objectForKey: fieldNames[count]];
1138 if ([recipients count] > 0)
1139 [allRecipients addObjectsFromArray: recipients];
1142 return allRecipients;
1145 - (NSException *) sendMail
1148 SOGoMailFolder *sentFolder;
1150 NSURL *sourceIMAP4URL;
1153 sentFolder = [[self mailAccountFolder] sentFolderInContext: context];
1154 if ([sentFolder isKindOfClass: [NSException class]])
1155 error = (NSException *) sentFolder;
1158 message = [self mimeMessageAsData];
1159 error = [[SOGoMailer sharedMailer] sendMailData: message
1160 toRecipients: [self allRecipients]
1161 sender: [self sender]];
1164 error = [sentFolder postData: message flags: @"seen"];
1167 [self imap4Connection];
1169 [imap4 markURLDeleted: [self imap4URL]];
1170 if (sourceURL && sourceFlag)
1172 sourceIMAP4URL = [NSURL URLWithString: sourceURL];
1173 [imap4 addFlags: sourceFlag toURL: sourceIMAP4URL];
1175 if (!draftDeleteDisabled)
1176 error = [self delete];
1184 - (NSException *) delete
1188 if ([[NSFileManager defaultManager]
1189 removeFileAtPath: [self draftFolderPath]
1193 error = [NSException exceptionWithHTTPStatus: 500 /* server error */
1194 reason: @"could not delete draft"];
1201 - (NSString *) contentAsString
1206 message = [self mimeMessageAsData];
1209 str = [[NSString alloc] initWithData: message
1210 encoding: NSUTF8StringEncoding];
1212 [self errorWithFormat: @"could not load draft as UTF-8 (data size=%d)",
1219 [self errorWithFormat: @"message data is empty"];
1228 - (BOOL) isDebuggingEnabled
1233 @end /* SOGoDraftObject */