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/NSProcessInfo.h>
27 #import <Foundation/NSURL.h>
28 #import <Foundation/NSUserDefaults.h>
29 #import <Foundation/NSValue.h>
31 #import <NGObjWeb/NSException+HTTP.h>
32 #import <NGObjWeb/SoObject+SoDAV.h>
33 #import <NGObjWeb/WOContext+SoObjects.h>
34 #import <NGObjWeb/WORequest+So.h>
35 #import <NGObjWeb/WOResponse.h>
36 #import <NGExtensions/NGBase64Coding.h>
37 #import <NGExtensions/NSFileManager+Extensions.h>
38 #import <NGExtensions/NGHashMap.h>
39 #import <NGExtensions/NSNull+misc.h>
40 #import <NGExtensions/NSObject+Logs.h>
41 #import <NGExtensions/NGQuotedPrintableCoding.h>
42 #import <NGExtensions/NSString+misc.h>
43 #import <NGImap4/NGImap4Connection.h>
44 #import <NGImap4/NGImap4Client.h>
45 #import <NGImap4/NGImap4Envelope.h>
46 #import <NGImap4/NGImap4EnvelopeAddress.h>
47 #import <NGMail/NGMimeMessage.h>
48 #import <NGMail/NGMimeMessageGenerator.h>
49 #import <NGMime/NGMimeBodyPart.h>
50 #import <NGMime/NGMimeFileData.h>
51 #import <NGMime/NGMimeMultipartBody.h>
52 #import <NGMime/NGMimeType.h>
53 #import <NGMime/NGMimeHeaderFieldGenerator.h>
54 #import <NGMime/NGMimeHeaderFields.h>
56 #import <SoObjects/SOGo/NSArray+Utilities.h>
57 #import <SoObjects/SOGo/NSCalendarDate+SOGo.h>
58 #import <SoObjects/SOGo/NSString+Utilities.h>
59 #import <SoObjects/SOGo/SOGoMailer.h>
60 #import <SoObjects/SOGo/SOGoUser.h>
62 #import "NSData+Mail.h"
63 #import "SOGoMailAccount.h"
64 #import "SOGoMailFolder.h"
65 #import "SOGoMailObject.h"
66 #import "SOGoMailObject+Draft.h"
68 #import "SOGoDraftObject.h"
70 static NSString *contentTypeValue = @"text/plain; charset=utf-8";
71 static NSString *headerKeys[] = {@"subject", @"to", @"cc", @"bcc",
72 @"from", @"replyTo", @"message-id",
75 @implementation SOGoDraftObject
77 static NGMimeType *TextPlainType = nil;
78 static NGMimeType *MultiMixedType = nil;
79 static NSString *userAgent = @"SOGoMail 1.0";
80 static BOOL draftDeleteDisabled = NO; // for debugging
81 static BOOL debugOn = NO;
82 static BOOL showTextAttachmentsInline = NO;
86 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
88 /* Note: be aware of the charset issues before enabling this! */
89 showTextAttachmentsInline = [ud boolForKey: @"SOGoShowTextAttachmentsInline"];
91 if ((draftDeleteDisabled = [ud boolForKey: @"SOGoNoDraftDeleteAfterSend"]))
92 NSLog(@"WARNING: draft delete is disabled! (SOGoNoDraftDeleteAfterSend)");
94 TextPlainType = [[NGMimeType mimeType: @"text" subType: @"plain"] copy];
95 MultiMixedType = [[NGMimeType mimeType: @"multipart" subType: @"mixed"] copy];
100 if ((self = [super init]))
103 headers = [NSMutableDictionary new];
120 [sourceFlag release];
125 /* draft folder functionality */
127 - (NSString *) userSpoolFolderPath
129 return [[self container] userSpoolFolderPath];
132 /* draft object functionality */
134 - (NSString *) draftFolderPath
138 path = [[self userSpoolFolderPath] stringByAppendingPathComponent:
146 - (BOOL) _ensureDraftFolderPath
150 fm = [NSFileManager defaultManager];
152 return ([fm createDirectoriesAtPath: [container userSpoolFolderPath]
154 && [fm createDirectoriesAtPath: [self draftFolderPath]
158 - (NSString *) infoPath
160 return [[self draftFolderPath]
161 stringByAppendingPathComponent: @".info.plist"];
166 - (NSString *) _generateMessageID
168 NSMutableString *messageID;
171 messageID = [NSMutableString string];
172 [messageID appendFormat: @"<%@", [self globallyUniqueObjectId]];
173 pGUID = [[NSProcessInfo processInfo] globallyUniqueString];
174 [messageID appendFormat: @"@%u>", [pGUID hash]];
176 return [messageID lowercaseString];
179 - (void) setHeaders: (NSDictionary *) newHeaders
185 for (count = 0; count < 8; count++)
187 headerValue = [newHeaders objectForKey: headerKeys[count]];
189 [headers setObject: headerValue
190 forKey: headerKeys[count]];
191 else if ([headers objectForKey: headerKeys[count]])
192 [headers removeObjectForKey: headerKeys[count]];
195 messageID = [headers objectForKey: @"message-id"];
198 messageID = [self _generateMessageID];
199 [headers setObject: messageID forKey: @"message-id"];
203 - (NSDictionary *) headers
208 - (void) setText: (NSString *) newText
210 ASSIGN (text, newText);
218 - (void) setInReplyTo: (NSString *) newInReplyTo
220 ASSIGN (inReplyTo, newInReplyTo);
223 - (void) setSourceURL: (NSString *) newSourceURL
225 ASSIGN (sourceURL, newSourceURL);
228 - (void) setSourceFlag: (NSString *) newSourceFlag
230 ASSIGN (sourceFlag, newSourceFlag);
233 - (NSException *) storeInfo
235 NSMutableDictionary *infos;
238 if ([self _ensureDraftFolderPath])
240 infos = [NSMutableDictionary new];
241 [infos setObject: headers forKey: @"headers"];
243 [infos setObject: text forKey: @"text"];
245 [infos setObject: inReplyTo forKey: @"inReplyTo"];
247 [infos setObject: [NSNumber numberWithInt: IMAP4ID]
249 if (sourceURL && sourceFlag)
251 [infos setObject: sourceURL forKey: @"sourceURL"];
252 [infos setObject: sourceFlag forKey: @"sourceFlag"];
255 if ([infos writeToFile: [self infoPath] atomically:YES])
259 [self errorWithFormat: @"could not write info: '%@'",
261 error = [NSException exceptionWithHTTPStatus:500 /* server error */
262 reason: @"could not write draft info!"];
269 [self errorWithFormat: @"could not create folder for draft: '%@'",
270 [self draftFolderPath]];
271 error = [NSException exceptionWithHTTPStatus:500 /* server error */
272 reason: @"could not create folder for draft!"];
278 - (void) _loadInfosFromDictionary: (NSDictionary *) infoDict
282 value = [infoDict objectForKey: @"headers"];
284 [self setHeaders: value];
286 value = [infoDict objectForKey: @"text"];
287 if ([value length] > 0)
288 [self setText: value];
290 value = [infoDict objectForKey: @"IMAP4ID"];
292 [self setIMAP4ID: [value intValue]];
294 value = [infoDict objectForKey: @"sourceURL"];
296 [self setSourceURL: value];
297 value = [infoDict objectForKey: @"sourceFlag"];
299 [self setSourceFlag: value];
301 value = [infoDict objectForKey: @"inReplyTo"];
303 [self setInReplyTo: value];
306 - (NSString *) relativeImap4Name
308 return [NSString stringWithFormat: @"%d", IMAP4ID];
319 fm = [NSFileManager defaultManager];
320 if ([fm fileExistsAtPath: p])
322 infos = [NSDictionary dictionaryWithContentsOfFile: p];
324 [self _loadInfosFromDictionary: infos];
326 // [self errorWithFormat: @"draft info dictionary broken at path: %@", p];
329 [self debugWithFormat: @"Note: info object does not yet exist: %@", p];
332 - (void) setIMAP4ID: (int) newIMAP4ID
334 IMAP4ID = newIMAP4ID;
342 - (int) _IMAP4IDFromAppendResult: (NSDictionary *) result
344 NSDictionary *results;
345 NSString *flag, *newIdString;
347 results = [[result objectForKey: @"RawResponse"]
348 objectForKey: @"ResponseResult"];
349 flag = [results objectForKey: @"flag"];
350 newIdString = [[flag componentsSeparatedByString: @" "] objectAtIndex: 2];
352 return [newIdString intValue];
355 - (NSException *) save
357 NGImap4Client *client;
364 message = [self mimeMessageAsData];
366 client = [[self imap4Connection] client];
367 folder = [imap4 imap4FolderNameForURL: [container imap4URL]];
369 = [client append: message toFolder: folder
370 withFlags: [NSArray arrayWithObjects: @"seen", @"draft", nil]];
371 if ([[result objectForKey: @"result"] boolValue])
374 error = [imap4 markURLDeleted: [self imap4URL]];
375 IMAP4ID = [self _IMAP4IDFromAppendResult: result];
379 error = [NSException exceptionWithHTTPStatus:500 /* Server Error */
380 reason: @"Failed to store message"];
385 - (void) _addEMailsOfAddresses: (NSArray *) _addrs
386 toArray: (NSMutableArray *) _ma
388 NSEnumerator *addresses;
389 NGImap4EnvelopeAddress *currentAddress;
391 addresses = [_addrs objectEnumerator];
392 while ((currentAddress = [addresses nextObject]))
393 [_ma addObject: [currentAddress email]];
396 - (void) _addRecipients: (NSArray *) recipients
397 toArray: (NSMutableArray *) array
399 NSEnumerator *addresses;
400 NGImap4EnvelopeAddress *currentAddress;
402 addresses = [recipients objectEnumerator];
403 while ((currentAddress = [addresses nextObject]))
404 [array addObject: [currentAddress baseEMail]];
407 - (void) _purgeRecipients: (NSArray *) recipients
408 fromAddresses: (NSMutableArray *) addresses
410 NSEnumerator *allRecipients;
411 NSString *currentRecipient;
412 NGImap4EnvelopeAddress *currentAddress;
415 max = [addresses count];
417 allRecipients = [recipients objectEnumerator];
419 && ((currentRecipient = [allRecipients nextObject])))
420 for (count = max - 1; count >= 0; count--)
422 currentAddress = [addresses objectAtIndex: count];
423 if ([currentRecipient isEqualToString: [currentAddress baseEMail]])
425 [addresses removeObjectAtIndex: count];
431 - (void) _fillInReplyAddresses: (NSMutableDictionary *) _info
432 replyToAll: (BOOL) _replyToAll
433 envelope: (NGImap4Envelope *) _envelope
436 The rules as implemented by Thunderbird:
437 - if there is a 'reply-to' header, only include that (as TO)
438 - if we reply to all, all non-from addresses are added as CC
439 - the from is always the lone TO (except for reply-to)
441 Note: we cannot check reply-to, because Cyrus even sets a reply-to in the
442 envelope if none is contained in the message itself! (bug or
445 TODO: what about sender (RFC 822 3.6.2)
447 NSMutableArray *to, *addrs, *allRecipients;
448 NSArray *envelopeAddresses, *userEmails;
450 allRecipients = [NSMutableArray new];
451 userEmails = [[context activeUser] allEmails];
452 [allRecipients addObjectsFromArray: userEmails];
454 to = [NSMutableArray arrayWithCapacity: 2];
456 addrs = [NSMutableArray new];
457 envelopeAddresses = [_envelope replyTo];
458 if ([envelopeAddresses count])
459 [addrs setArray: envelopeAddresses];
461 [addrs setArray: [_envelope from]];
463 [self _purgeRecipients: allRecipients
464 fromAddresses: addrs];
465 [self _addEMailsOfAddresses: addrs toArray: to];
466 [self _addRecipients: addrs toArray: allRecipients];
467 [_info setObject: to forKey: @"to"];
469 /* If "to" is empty, we add at least ourself as a recipient!
470 This is for emails in the "Sent" folder that we reply to... */
473 if ([[_envelope replyTo] count])
474 [self _addEMailsOfAddresses: [_envelope replyTo] toArray: to];
476 [self _addEMailsOfAddresses: [_envelope from] toArray: to];
479 /* If we have no To but we have Cc recipients, let's move the Cc
480 to the To bucket... */
481 if ([[_info objectForKey: @"to"] count] == 0 && [_info objectForKey: @"cc"])
485 o = [_info objectForKey: @"cc"];
486 [_info setObject: o forKey: @"to"];
487 [_info removeObjectForKey: @"cc"];
490 /* CC processing if we reply-to-all: add all 'to' and 'cc' */
493 to = [NSMutableArray new];
495 [addrs setArray: [_envelope to]];
496 [self _purgeRecipients: allRecipients
497 fromAddresses: addrs];
498 [self _addEMailsOfAddresses: addrs toArray: to];
499 [self _addRecipients: addrs toArray: allRecipients];
501 [addrs setArray: [_envelope cc]];
502 [self _purgeRecipients: allRecipients
503 fromAddresses: addrs];
504 [self _addEMailsOfAddresses: addrs toArray: to];
506 [_info setObject: to forKey: @"cc"];
511 [allRecipients release];
515 - (NSArray *) _attachmentBodiesFromPaths: (NSArray *) paths
516 fromResponseFetch: (NSDictionary *) fetch;
518 NSEnumerator *attachmentKeys;
519 NSMutableArray *bodies;
520 NSString *currentKey;
523 bodies = [NSMutableArray array];
525 attachmentKeys = [paths objectEnumerator];
526 while ((currentKey = [attachmentKeys nextObject]))
528 body = [fetch objectForKey: [currentKey lowercaseString]];
529 [bodies addObject: [body objectForKey: @"data"]];
535 - (void) _fetchAttachments: (NSArray *) parts
536 fromMail: (SOGoMailObject *) sourceMail
538 unsigned int count, max;
539 NSArray *paths, *bodies;
541 NSDictionary *currentInfo;
547 paths = [parts keysWithFormat: @"BODY[%{path}]"];
548 response = [[sourceMail fetchParts: paths] objectForKey: @"RawResponse"];
549 bodies = [self _attachmentBodiesFromPaths: paths
550 fromResponseFetch: [response objectForKey: @"fetch"]];
551 for (count = 0; count < max; count++)
553 currentInfo = [parts objectAtIndex: count];
554 body = [[bodies objectAtIndex: count]
555 bodyDataFromEncoding: [currentInfo
556 objectForKey: @"encoding"]];
557 [self saveAttachment: body withMetadata: currentInfo];
562 - (void) fetchMailForEditing: (SOGoMailObject *) sourceMail
564 NSString *subject, *msgid;
565 NSMutableDictionary *info;
566 NSMutableArray *addresses;
567 NGImap4Envelope *sourceEnvelope;
569 [sourceMail fetchCoreInfos];
571 [self _fetchAttachments: [sourceMail fetchFileAttachmentKeys]
572 fromMail: sourceMail];
573 info = [NSMutableDictionary dictionaryWithCapacity: 16];
574 subject = [sourceMail subject];
575 if ([subject length] > 0)
576 [info setObject: subject forKey: @"subject"];
578 sourceEnvelope = [sourceMail envelope];
579 msgid = [sourceEnvelope messageID];
580 if ([msgid length] > 0)
581 [info setObject: msgid forKey: @"message-id"];
583 addresses = [NSMutableArray array];
584 [self _addEMailsOfAddresses: [sourceEnvelope to] toArray: addresses];
585 [info setObject: addresses forKey: @"to"];
586 addresses = [NSMutableArray array];
587 [self _addEMailsOfAddresses: [sourceEnvelope cc] toArray: addresses];
588 if ([addresses count] > 0)
589 [info setObject: addresses forKey: @"cc"];
590 addresses = [NSMutableArray array];
591 [self _addEMailsOfAddresses: [sourceEnvelope bcc] toArray: addresses];
592 if ([addresses count] > 0)
593 [info setObject: addresses forKey: @"bcc"];
594 addresses = [NSMutableArray array];
595 [self _addEMailsOfAddresses: [sourceEnvelope replyTo] toArray: addresses];
596 if ([addresses count] > 0)
597 [info setObject: addresses forKey: @"replyTo"];
598 [self setHeaders: info];
600 [self setText: [sourceMail contentForEditing]];
601 [self setSourceURL: [sourceMail imap4URLString]];
602 IMAP4ID = [[sourceMail nameInContainer] intValue];
607 - (void) fetchMailForReplying: (SOGoMailObject *) sourceMail
610 NSString *contentForReply, *msgID;
611 NSMutableDictionary *info;
612 NGImap4Envelope *sourceEnvelope;
614 [sourceMail fetchCoreInfos];
616 info = [NSMutableDictionary dictionaryWithCapacity: 16];
617 [info setObject: [sourceMail subjectForReply] forKey: @"subject"];
619 sourceEnvelope = [sourceMail envelope];
620 [self _fillInReplyAddresses: info replyToAll: toAll
621 envelope: sourceEnvelope];
622 msgID = [sourceEnvelope messageID];
623 if ([msgID length] > 0)
624 [self setInReplyTo: msgID];
625 contentForReply = [sourceMail contentForReply];
626 [self setText: contentForReply];
627 [self setHeaders: info];
628 [self setSourceURL: [sourceMail imap4URLString]];
629 [self setSourceFlag: @"Answered"];
633 - (void) fetchMailForForwarding: (SOGoMailObject *) sourceMail
635 NSDictionary *info, *attachment;
636 SOGoUser *currentUser;
638 [sourceMail fetchCoreInfos];
640 if ([sourceMail subjectForForward])
642 info = [NSDictionary dictionaryWithObject: [sourceMail subjectForForward]
644 [self setHeaders: info];
647 [self setSourceURL: [sourceMail imap4URLString]];
648 [self setSourceFlag: @"$Forwarded"];
651 currentUser = [context activeUser];
652 if ([[currentUser messageForwarding] isEqualToString: @"inline"])
654 [self setText: [sourceMail contentForInlineForward]];
655 [self _fetchAttachments: [sourceMail fetchFileAttachmentKeys]
656 fromMail: sourceMail];
660 // TODO: use subject for filename?
661 // error = [newDraft saveAttachment:content withName:@"forward.mail"];
662 attachment = [NSDictionary dictionaryWithObjectsAndKeys:
663 [sourceMail filenameForForward], @"filename",
664 @"message/rfc822", @"mimetype",
666 [self saveAttachment: [sourceMail content]
667 withMetadata: attachment];
674 - (NSString *) sender
678 if ((tmp = [headers objectForKey: @"from"]) == nil)
680 if ([tmp isKindOfClass:[NSArray class]])
681 return [tmp count] > 0 ? [tmp objectAtIndex: 0] : nil;
688 - (NSArray *) fetchAttachmentNames
696 fm = [NSFileManager defaultManager];
697 files = [fm directoryContentsAtPath: [self draftFolderPath]];
700 ma = [NSMutableArray arrayWithCapacity: max];
701 for (count = 0; count < max; count++)
703 filename = [files objectAtIndex: count];
704 if (![filename hasPrefix: @"."])
705 [ma addObject: filename];
711 - (BOOL) isValidAttachmentName: (NSString *) _name
713 static NSString *sescape[] = { @"/", @"..", @"~", @"\"", @"'", nil };
717 if (![_name isNotNull]) return NO;
718 if ([_name length] == 0) return NO;
719 if ([_name hasPrefix: @"."]) return NO;
721 for (i = 0; sescape[i] != nil; i++) {
722 r = [_name rangeOfString:sescape[i]];
723 if (r.length > 0) return NO;
728 - (NSString *) pathToAttachmentWithName: (NSString *) _name
730 if ([_name length] == 0)
733 return [[self draftFolderPath] stringByAppendingPathComponent:_name];
736 - (NSException *) invalidAttachmentNameError: (NSString *) _name
738 return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
739 reason: @"Invalid attachment name!"];
742 - (NSException *) saveAttachment: (NSData *) _attach
743 withMetadata: (NSDictionary *) metadata
745 NSString *p, *name, *mimeType;
748 if (![_attach isNotNull]) {
749 return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
750 reason: @"Missing attachment content!"];
753 if (![self _ensureDraftFolderPath]) {
754 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
755 reason: @"Could not create folder for draft!"];
758 name = [metadata objectForKey: @"filename"];
759 r = [name rangeOfString: @"\\"
760 options: NSBackwardsSearch];
762 name = [name substringFromIndex: r.location + 1];
764 if (![self isValidAttachmentName: name])
765 return [self invalidAttachmentNameError: name];
767 p = [self pathToAttachmentWithName: name];
768 if (![_attach writeToFile: p atomically: YES])
770 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
771 reason: @"Could not write attachment to draft!"];
774 mimeType = [metadata objectForKey: @"mimetype"];
775 if ([mimeType length] > 0)
777 p = [self pathToAttachmentWithName:
778 [NSString stringWithFormat: @".%@.mime", name]];
779 if (![[mimeType dataUsingEncoding: NSUTF8StringEncoding]
780 writeToFile: p atomically: YES])
782 return [NSException exceptionWithHTTPStatus:500 /* Server Error */
783 reason: @"Could not write attachment to draft!"];
787 return nil; /* everything OK */
790 - (NSException *) deleteAttachmentWithName: (NSString *) _name
798 if ([self isValidAttachmentName:_name])
800 fm = [NSFileManager defaultManager];
801 p = [self pathToAttachmentWithName:_name];
802 if ([fm fileExistsAtPath: p])
803 if (![fm removeFileAtPath: p handler: nil])
805 = [NSException exceptionWithHTTPStatus: 500 /* Server Error */
806 reason: @"Could not delete attachment from draft!"];
809 error = [self invalidAttachmentNameError:_name];
814 /* NGMime representations */
816 - (NGMimeBodyPart *) bodyPartForText
819 This add the text typed by the user (the primary plain/text part).
821 NGMutableHashMap *map;
822 NGMimeBodyPart *bodyPart;
824 /* prepare header of body part */
826 map = [[[NGMutableHashMap alloc] initWithCapacity: 1] autorelease];
828 // TODO: set charset in header!
829 [map setObject: @"text/plain" forKey: @"content-type"];
831 [map setObject: contentTypeValue forKey: @"content-type"];
833 // if ((body = text) != nil) {
834 // if ([body isKindOfClass: [NSString class]]) {
835 // [map setObject: contentTypeValue
836 // forKey: @"content-type"];
837 // // body = [body dataUsingEncoding:NSUTF8StringEncoding];
841 /* prepare body content */
843 bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease];
844 [bodyPart setBody: text];
849 - (NGMimeMessage *) mimeMessageForContentWithHeaderMap: (NGMutableHashMap *) map
851 NGMimeMessage *message;
855 [map setObject: @"text/plain" forKey: @"content-type"];
859 // if ([body isKindOfClass:[NSString class]])
860 /* Note: just 'utf8' is displayed wrong in Mail.app */
861 [map setObject: contentTypeValue
862 forKey: @"content-type"];
863 // body = [body dataUsingEncoding:NSUTF8StringEncoding];
864 // else if ([body isKindOfClass:[NSData class]] && addSuffix) {
865 // body = [[body mutableCopy] autorelease];
867 // else if (addSuffix) {
868 // [self warnWithFormat: @"Note: cannot add Internet marker to body: %@",
869 // NSStringFromClass([body class])];
872 message = [[[NGMimeMessage alloc] initWithHeader:map] autorelease];
873 [message setBody: body];
882 - (NSString *) mimeTypeForExtension: (NSString *) _ext
884 // TODO: make configurable
885 // TODO: use /etc/mime-types
886 if ([_ext isEqualToString: @"txt"]) return @"text/plain";
887 if ([_ext isEqualToString: @"html"]) return @"text/html";
888 if ([_ext isEqualToString: @"htm"]) return @"text/html";
889 if ([_ext isEqualToString: @"gif"]) return @"image/gif";
890 if ([_ext isEqualToString: @"jpg"]) return @"image/jpeg";
891 if ([_ext isEqualToString: @"jpeg"]) return @"image/jpeg";
892 if ([_ext isEqualToString: @"mail"]) return @"message/rfc822";
893 return @"application/octet-stream";
896 - (NSString *) contentTypeForAttachmentWithName: (NSString *) _name
901 p = [self pathToAttachmentWithName:
902 [NSString stringWithFormat: @".%@.mime", _name]];
903 mimeData = [NSData dataWithContentsOfFile: p];
906 s = [[NSString alloc] initWithData: mimeData
907 encoding: NSUTF8StringEncoding];
912 s = [self mimeTypeForExtension:[_name pathExtension]];
913 if ([_name length] > 0)
914 s = [s stringByAppendingFormat: @"; name=\"%@\"", _name];
920 - (NSString *) contentDispositionForAttachmentWithName: (NSString *) _name
926 type = [self contentTypeForAttachmentWithName:_name];
928 if ([type hasPrefix: @"text/"])
929 cdtype = showTextAttachmentsInline ? @"inline" : @"attachment";
930 else if ([type hasPrefix: @"image/"] || [type hasPrefix: @"message"])
933 cdtype = @"attachment";
935 cd = [cdtype stringByAppendingString: @"; filename=\""];
936 cd = [cd stringByAppendingString: _name];
937 cd = [cd stringByAppendingString: @"\""];
939 // TODO: add size parameter (useful addition, RFC 2183)
943 - (NGMimeBodyPart *) bodyPartForAttachmentWithName: (NSString *) _name
946 NGMutableHashMap *map;
947 NGMimeBodyPart *bodyPart;
950 BOOL attachAsString, is7bit;
954 if (_name == nil) return nil;
956 /* check attachment */
958 fm = [NSFileManager defaultManager];
959 p = [self pathToAttachmentWithName:_name];
960 if (![fm isReadableFileAtPath:p]) {
961 [self errorWithFormat: @"did not find attachment: '%@'", _name];
967 /* prepare header of body part */
969 map = [[[NGMutableHashMap alloc] initWithCapacity:4] autorelease];
971 if ((s = [self contentTypeForAttachmentWithName:_name]) != nil) {
972 [map setObject:s forKey: @"content-type"];
973 if ([s hasPrefix: @"text/"])
974 attachAsString = YES;
975 else if ([s hasPrefix: @"message/rfc822"])
978 if ((s = [self contentDispositionForAttachmentWithName:_name]))
980 NGMimeContentDispositionHeaderField *o;
982 o = [[NGMimeContentDispositionHeaderField alloc] initWithString: s];
983 [map setObject:o forKey: @"content-disposition"];
987 /* prepare body content */
989 if (attachAsString) { // TODO: is this really necessary?
992 content = [[NSData alloc] initWithContentsOfMappedFile:p];
994 s = [[NSString alloc] initWithData:content
995 encoding:[NSString defaultCStringEncoding]];
998 [content release]; content = nil;
1001 [self warnWithFormat:
1002 @"could not get text attachment as string: '%@'", _name];
1009 Note: Apparently NGMimeFileData objects are not processed by the MIME
1012 body = [[NGMimeFileData alloc] initWithPath:p removeFile:NO];
1013 [map setObject: @"7bit" forKey: @"content-transfer-encoding"];
1014 [map setObject:[NSNumber numberWithInt:[body length]]
1015 forKey: @"content-length"];
1019 Note: in OGo this is done in LSWImapMailEditor.m:2477. Apparently
1020 NGMimeFileData objects are not processed by the MIME generator!
1024 content = [[NSData alloc] initWithContentsOfMappedFile:p];
1025 encoded = [content dataByEncodingBase64];
1026 [content release]; content = nil;
1028 [map setObject: @"base64" forKey: @"content-transfer-encoding"];
1029 [map setObject:[NSNumber numberWithInt:[encoded length]]
1030 forKey: @"content-length"];
1032 /* Note: the -init method will create a temporary file! */
1033 body = [[NGMimeFileData alloc] initWithBytes:[encoded bytes]
1034 length:[encoded length]];
1037 bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease];
1038 [bodyPart setBody:body];
1040 [body release]; body = nil;
1044 - (NSArray *) bodyPartsForAllAttachments
1046 /* returns nil on error */
1049 NGMimeBodyPart *bodyPart;
1050 NSMutableArray *bodyParts;
1052 names = [self fetchAttachmentNames];
1053 count = [names count];
1054 bodyParts = [NSMutableArray arrayWithCapacity: count];
1056 for (i = 0; i < count; i++)
1058 bodyPart = [self bodyPartForAttachmentWithName: [names objectAtIndex: i]];
1059 [bodyParts addObject: bodyPart];
1065 - (NGMimeMessage *) mimeMultiPartMessageWithHeaderMap: (NGMutableHashMap *) map
1066 andBodyParts: (NSArray *) _bodyParts
1068 NGMimeMessage *message;
1069 NGMimeMultipartBody *mBody;
1070 NGMimeBodyPart *part;
1073 [map addObject: MultiMixedType forKey: @"content-type"];
1075 message = [[NGMimeMessage alloc] initWithHeader: map];
1076 [message autorelease];
1077 mBody = [[NGMimeMultipartBody alloc] initWithPart: message];
1079 part = [self bodyPartForText];
1080 [mBody addBodyPart: part];
1082 e = [_bodyParts objectEnumerator];
1083 part = [e nextObject];
1086 [mBody addBodyPart: part];
1087 part = [e nextObject];
1090 [message setBody: mBody];
1096 - (void) _addHeaders: (NSDictionary *) _h
1097 toHeaderMap: (NGMutableHashMap *) _map
1099 NSEnumerator *names;
1102 if ([_h count] == 0)
1105 names = [_h keyEnumerator];
1106 while ((name = [names nextObject]) != nil) {
1109 value = [_h objectForKey:name];
1110 [_map addObject:value forKey:name];
1114 - (BOOL) isEmptyValue: (id) _value
1116 if (![_value isNotNull])
1119 if ([_value isKindOfClass: [NSArray class]])
1120 return [_value count] == 0 ? YES : NO;
1122 if ([_value isKindOfClass: [NSString class]])
1123 return [_value length] == 0 ? YES : NO;
1128 - (NGMutableHashMap *) mimeHeaderMapWithHeaders: (NSDictionary *) _headers
1130 NGMutableHashMap *map;
1132 NSString *s, *dateString;
1135 map = [[[NGMutableHashMap alloc] initWithCapacity:16] autorelease];
1137 /* add recipients */
1139 if ((emails = [headers objectForKey: @"to"]) != nil)
1140 [map setObjects: emails forKey: @"to"];
1141 if ((emails = [headers objectForKey: @"cc"]) != nil)
1142 [map setObjects:emails forKey: @"cc"];
1143 if ((emails = [headers objectForKey: @"bcc"]) != nil)
1144 [map setObjects:emails forKey: @"bcc"];
1148 from = [headers objectForKey: @"from"];
1149 replyTo = [headers objectForKey: @"replyTo"];
1151 if (![self isEmptyValue:from]) {
1152 if ([from isKindOfClass:[NSArray class]])
1153 [map setObjects: from forKey: @"from"];
1155 [map setObject: from forKey: @"from"];
1158 if (![self isEmptyValue: replyTo]) {
1159 if ([from isKindOfClass:[NSArray class]])
1160 [map setObjects:from forKey: @"reply-to"];
1162 [map setObject:from forKey: @"reply-to"];
1164 else if (![self isEmptyValue:from])
1165 [map setObjects:[map objectsForKey: @"from"] forKey: @"reply-to"];
1168 [map setObject: inReplyTo forKey: @"in-reply-to"];
1171 if ([(s = [headers objectForKey: @"subject"]) length] > 0)
1172 [map setObject: [s asQPSubjectString: @"utf-8"]
1173 forKey: @"subject"];
1175 [map setObject: [headers objectForKey: @"message-id"]
1176 forKey: @"message-id"];
1178 /* add standard headers */
1179 dateString = [[NSCalendarDate date] rfc822DateString];
1180 [map addObject: dateString forKey: @"date"];
1181 [map addObject: @"1.0" forKey: @"MIME-Version"];
1182 [map addObject: userAgent forKey: @"User-Agent"];
1184 /* add custom headers */
1186 // [self _addHeaders: [lInfo objectForKey: @"headers"] toHeaderMap:map];
1187 [self _addHeaders: _headers toHeaderMap: map];
1192 - (NGMimeMessage *) mimeMessageWithHeaders: (NSDictionary *) _headers
1194 NGMutableHashMap *map;
1196 NGMimeMessage *message;
1200 map = [self mimeHeaderMapWithHeaders: _headers];
1203 [self debugWithFormat: @"MIME Envelope: %@", map];
1205 bodyParts = [self bodyPartsForAllAttachments];
1208 [self debugWithFormat: @"attachments: %@", bodyParts];
1210 if ([bodyParts count] == 0)
1211 /* no attachments */
1212 message = [self mimeMessageForContentWithHeaderMap: map];
1214 /* attachments, create multipart/mixed */
1215 message = [self mimeMultiPartMessageWithHeaderMap: map
1216 andBodyParts: bodyParts];
1217 [self debugWithFormat: @"message: %@", message];
1220 [self errorWithFormat:
1221 @"could not create body parts for attachments!"];
1227 - (NGMimeMessage *) mimeMessage
1229 return [self mimeMessageWithHeaders: nil];
1232 - (NSData *) mimeMessageAsData
1234 NGMimeMessageGenerator *generator;
1237 generator = [NGMimeMessageGenerator new];
1238 message = [generator generateMimeFromPart: [self mimeMessage]];
1239 [generator release];
1244 - (NSArray *) allRecipients
1246 NSMutableArray *allRecipients;
1247 NSArray *recipients;
1248 NSString *fieldNames[] = {@"to", @"cc", @"bcc"};
1251 allRecipients = [NSMutableArray arrayWithCapacity: 16];
1253 for (count = 0; count < 3; count++)
1255 recipients = [headers objectForKey: fieldNames[count]];
1256 if ([recipients count] > 0)
1257 [allRecipients addObjectsFromArray: recipients];
1260 return allRecipients;
1263 - (NSException *) sendMail
1266 SOGoMailFolder *sentFolder;
1268 NSURL *sourceIMAP4URL;
1271 sentFolder = [[self mailAccountFolder] sentFolderInContext: context];
1272 if ([sentFolder isKindOfClass: [NSException class]])
1273 error = (NSException *) sentFolder;
1276 message = [self mimeMessageAsData];
1277 error = [[SOGoMailer sharedMailer] sendMailData: message
1278 toRecipients: [self allRecipients]
1279 sender: [self sender]];
1282 error = [sentFolder postData: message flags: @"seen"];
1285 [self imap4Connection];
1287 [imap4 markURLDeleted: [self imap4URL]];
1288 if (sourceURL && sourceFlag)
1290 sourceIMAP4URL = [NSURL URLWithString: sourceURL];
1291 [imap4 addFlags: sourceFlag toURL: sourceIMAP4URL];
1293 if (!draftDeleteDisabled)
1294 error = [self delete];
1302 - (NSException *) delete
1306 if ([[NSFileManager defaultManager]
1307 removeFileAtPath: [self draftFolderPath]
1311 error = [NSException exceptionWithHTTPStatus: 500 /* server error */
1312 reason: @"could not delete draft"];
1319 - (NSString *) contentAsString
1324 message = [self mimeMessageAsData];
1327 str = [[NSString alloc] initWithData: message
1328 encoding: NSUTF8StringEncoding];
1330 [self errorWithFormat: @"could not load draft as UTF-8 (data size=%d)",
1337 [self errorWithFormat: @"message data is empty"];
1346 - (BOOL) isDebuggingEnabled
1351 @end /* SOGoDraftObject */