]> err.no Git - scalable-opengroupware.org/blob - SoObjects/Mailer/SOGoDraftObject.m
2cc71010e49324d027058eefafd6d656707c3bd9
[scalable-opengroupware.org] / SoObjects / Mailer / SOGoDraftObject.m
1 /*
2   Copyright (C) 2004-2005 SKYRIX Software AG
3
4   This file is part of OpenGroupware.org.
5
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
9   later version.
10
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.
15
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
19   02111-1307, USA.
20 */
21
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>
29
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>
53
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
60 #import "NSData+Mail.h"
61 #import "SOGoMailAccount.h"
62 #import "SOGoMailFolder.h"
63 #import "SOGoMailObject.h"
64 #import "SOGoMailObject+Draft.h"
65
66 #import "SOGoDraftObject.h"
67
68 static NSString *contentTypeValue = @"text/plain; charset=utf-8";
69 static NSString *headerKeys[] = {@"subject", @"to", @"cc", @"bcc", 
70                                  @"from", @"replyTo",
71                                  nil};
72
73 @implementation SOGoDraftObject
74
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;
81
82 + (void) initialize
83 {
84   NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
85   
86   /* Note: be aware of the charset issues before enabling this! */
87   showTextAttachmentsInline = [ud boolForKey: @"SOGoShowTextAttachmentsInline"];
88   
89   if ((draftDeleteDisabled = [ud boolForKey: @"SOGoNoDraftDeleteAfterSend"]))
90     NSLog(@"WARNING: draft delete is disabled! (SOGoNoDraftDeleteAfterSend)");
91   
92   TextPlainType  = [[NGMimeType mimeType: @"text" subType: @"plain"]  copy];
93   MultiMixedType = [[NGMimeType mimeType: @"multipart" subType: @"mixed"]  copy];
94 }
95
96 - (id) init
97 {
98   if ((self = [super init]))
99     {
100       IMAP4ID = -1;
101       headers = [NSMutableDictionary new];
102       text = @"";
103       sourceURL = nil;
104       sourceFlag = nil;
105       inReplyTo = nil;
106     }
107
108   return self;
109 }
110
111 - (void) dealloc
112 {
113   [headers release];
114   [text release];
115   [envelope release];
116   [path release];
117   [sourceURL release];
118   [sourceFlag release];
119   [inReplyTo release];
120   [super dealloc];
121 }
122
123 /* draft folder functionality */
124
125 - (NSString *) userSpoolFolderPath
126 {
127   return [[self container] userSpoolFolderPath];
128 }
129
130 /* draft object functionality */
131
132 - (NSString *) draftFolderPath
133 {
134   if (!path)
135     {
136       path = [[self userSpoolFolderPath] stringByAppendingPathComponent:
137                                            nameInContainer];
138       [path retain];
139     }
140
141   return path;
142 }
143
144 - (BOOL) _ensureDraftFolderPath
145 {
146   NSFileManager *fm;
147
148   fm = [NSFileManager defaultManager];
149   
150   return ([fm createDirectoriesAtPath: [container userSpoolFolderPath]
151               attributes: nil]
152           && [fm createDirectoriesAtPath: [self draftFolderPath]
153                  attributes:nil]);
154 }
155
156 - (NSString *) infoPath
157 {
158   return [[self draftFolderPath]
159            stringByAppendingPathComponent: @".info.plist"];
160 }
161
162 /* contents */
163
164 - (void) setHeaders: (NSDictionary *) newHeaders
165 {
166   id headerValue;
167   unsigned int count;
168
169   for (count = 0; count < 7; count++)
170     {
171       headerValue = [newHeaders objectForKey: headerKeys[count]];
172       if (headerValue)
173         [headers setObject: headerValue
174                  forKey: headerKeys[count]];
175       else
176         [headers removeObjectForKey: headerKeys[count]];
177     }
178 }
179
180 - (NSDictionary *) headers
181 {
182   return headers;
183 }
184
185 - (void) setText: (NSString *) newText
186 {
187   ASSIGN (text, newText);
188 }
189
190 - (NSString *) text
191 {
192   return text;
193 }
194
195 - (void) setInReplyTo: (NSString *) newInReplyTo
196 {
197   ASSIGN (inReplyTo, newInReplyTo);
198 }
199
200 - (void) setSourceURL: (NSString *) newSourceURL
201 {
202   ASSIGN (sourceURL, newSourceURL);
203 }
204
205 - (void) setSourceFlag: (NSString *) newSourceFlag
206 {
207   ASSIGN (sourceFlag, newSourceFlag);
208 }
209
210 - (NSException *) storeInfo
211 {
212   NSMutableDictionary *infos;
213   NSException *error;
214
215   if ([self _ensureDraftFolderPath])
216     {
217       infos = [NSMutableDictionary new];
218       [infos setObject: headers forKey: @"headers"];
219       if (text)
220         [infos setObject: text forKey: @"text"];
221       if (inReplyTo)
222         [infos setObject: inReplyTo forKey: @"inReplyTo"];
223       if (IMAP4ID > -1)
224         [infos setObject: [NSNumber numberWithInt: IMAP4ID]
225                forKey: @"IMAP4ID"];
226       if (sourceURL && sourceFlag)
227         {
228           [infos setObject: sourceURL forKey: @"sourceURL"];
229           [infos setObject: sourceFlag forKey: @"sourceFlag"];
230         }
231
232       if ([infos writeToFile: [self infoPath] atomically:YES])
233         error = nil;
234       else
235         {
236           [self errorWithFormat: @"could not write info: '%@'",
237                 [self infoPath]];
238           error = [NSException exceptionWithHTTPStatus:500 /* server error */
239                                reason: @"could not write draft info!"];
240         }
241
242       [infos release];
243     }
244   else
245     {
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!"];
250     }
251
252   return error;
253 }
254
255 - (void) _loadInfosFromDictionary: (NSDictionary *) infoDict
256 {
257   id value;
258
259   value = [infoDict objectForKey: @"headers"];
260   if (value)
261     [self setHeaders: value];
262
263   value = [infoDict objectForKey: @"text"];
264   if ([value length] > 0)
265     [self setText: value];
266
267   value = [infoDict objectForKey: @"IMAP4ID"];
268   if (value)
269     [self setIMAP4ID: [value intValue]];
270
271   value = [infoDict objectForKey: @"sourceURL"];
272   if (value)
273     [self setSourceURL: value];
274   value = [infoDict objectForKey: @"sourceFlag"];
275   if (value)
276     [self setSourceFlag: value];
277
278   value = [infoDict objectForKey: @"inReplyTo"];
279   if (value)
280     [self setInReplyTo: value];
281 }
282
283 - (NSString *) relativeImap4Name
284 {
285   return [NSString stringWithFormat: @"%d", IMAP4ID];
286 }
287
288 - (void) fetchInfo
289 {
290   NSString *p;
291   NSDictionary *infos;
292   NSFileManager *fm;
293
294   p = [self infoPath];
295
296   fm = [NSFileManager defaultManager];
297   if ([fm fileExistsAtPath: p])
298     {
299       infos = [NSDictionary dictionaryWithContentsOfFile: p];
300       if (infos)
301         [self _loadInfosFromDictionary: infos];
302 //       else
303 //      [self errorWithFormat: @"draft info dictionary broken at path: %@", p];
304     }
305   else
306     [self debugWithFormat: @"Note: info object does not yet exist: %@", p];
307 }
308
309 - (void) setIMAP4ID: (int) newIMAP4ID
310 {
311   IMAP4ID = newIMAP4ID;
312 }
313
314 - (int) IMAP4ID
315 {
316   return IMAP4ID;
317 }
318
319 - (int) _IMAP4IDFromAppendResult: (NSDictionary *) result
320 {
321   NSDictionary *results;
322   NSString *flag, *newIdString;
323
324   results = [[result objectForKey: @"RawResponse"]
325               objectForKey: @"ResponseResult"];
326   flag = [results objectForKey: @"flag"];
327   newIdString = [[flag componentsSeparatedByString: @" "] objectAtIndex: 2];
328
329   return [newIdString intValue];
330 }
331
332 - (NSException *) save
333 {
334   NGImap4Client *client;
335   NSException *error;
336   NSData *message;
337   NSString *folder;
338   id result;
339
340   error = nil;
341   message = [self mimeMessageAsData];
342
343   client = [[self imap4Connection] client];
344   folder = [imap4 imap4FolderNameForURL: [container imap4URL]];
345   result
346     = [client append: message toFolder: folder
347               withFlags: [NSArray arrayWithObjects: @"seen", @"draft", nil]];
348   if ([[result objectForKey: @"result"] boolValue])
349     {
350       if (IMAP4ID > -1)
351         error = [imap4 markURLDeleted: [self imap4URL]];
352       IMAP4ID = [self _IMAP4IDFromAppendResult: result];
353       [self storeInfo];
354     }
355   else
356     error = [NSException exceptionWithHTTPStatus:500 /* Server Error */
357                          reason: @"Failed to store message"];
358
359   return error;
360 }
361
362 - (void) _addEMailsOfAddresses: (NSArray *) _addrs
363                        toArray: (NSMutableArray *) _ma
364 {
365   unsigned i, count;
366
367   for (i = 0, count = [_addrs count]; i < count; i++)
368     [_ma addObject:
369            [(NGImap4EnvelopeAddress *) [_addrs objectAtIndex: i] email]];
370 }
371
372 - (void) _fillInReplyAddresses: (NSMutableDictionary *) _info
373                     replyToAll: (BOOL) _replyToAll
374                       envelope: (NGImap4Envelope *) _envelope
375 {
376   /*
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)
381     
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
384           feature?)
385     
386     TODO: what about sender (RFC 822 3.6.2)
387   */
388   NSMutableArray *to;
389   NSArray *addrs;
390   
391   to = [NSMutableArray arrayWithCapacity:2];
392
393   /* first check for "reply-to" */
394   
395   addrs = [_envelope replyTo];
396   if ([addrs count] == 0)
397     /* no "reply-to", try "from" */
398     addrs = [_envelope from];
399
400   [self _addEMailsOfAddresses: addrs toArray: to];
401   [_info setObject: to forKey: @"to"];
402
403   /* CC processing if we reply-to-all: add all 'to' and 'cc'  */
404   
405   if (_replyToAll)
406     {
407       to = [NSMutableArray arrayWithCapacity:8];
408
409       [self _addEMailsOfAddresses: [_envelope to] toArray: to];
410       [self _addEMailsOfAddresses: [_envelope cc] toArray: to];
411     
412       [_info setObject: to forKey: @"cc"];
413     }
414 }
415
416 - (NSArray *) _attachmentBodiesFromPaths: (NSArray *) paths
417                        fromResponseFetch: (NSDictionary *) fetch;
418 {
419   NSEnumerator *attachmentKeys;
420   NSMutableArray *bodies;
421   NSString *currentKey;
422   NSDictionary *body;
423
424   bodies = [NSMutableArray array];
425
426   attachmentKeys = [paths objectEnumerator];
427   while ((currentKey = [attachmentKeys nextObject]))
428     {
429       body = [fetch objectForKey: [currentKey lowercaseString]];
430       [bodies addObject: [body objectForKey: @"data"]];
431     }
432
433   return bodies;
434 }
435
436 - (void) _fetchAttachments: (NSArray *) parts
437                   fromMail: (SOGoMailObject *) sourceMail
438 {
439   unsigned int count, max;
440   NSArray *paths, *bodies;
441   NSData *body;
442   NSDictionary *currentInfo;
443   NGHashMap *response;
444
445   max = [parts count];
446   if (max > 0)
447     {
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++)
453         {
454           currentInfo = [parts objectAtIndex: count];
455           body = [[bodies objectAtIndex: count]
456                    bodyDataFromEncoding: [currentInfo
457                                            objectForKey: @"encoding"]];
458           [self saveAttachment: body withMetadata: currentInfo];
459         }
460     }
461 }
462
463 - (void) fetchMailForEditing: (SOGoMailObject *) sourceMail
464 {
465   NSString *subject;
466   NSMutableDictionary *info;
467   NSMutableArray *addresses;
468   NGImap4Envelope *sourceEnvelope;
469
470   [sourceMail fetchCoreInfos];
471
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"];
478
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];
496
497   [self setText: [sourceMail contentForEditing]];
498   [self setSourceURL: [sourceMail imap4URLString]];
499   IMAP4ID = [[sourceMail nameInContainer] intValue];
500
501   [self storeInfo];
502 }
503
504 - (void) fetchMailForReplying: (SOGoMailObject *) sourceMail
505                         toAll: (BOOL) toAll
506 {
507   NSString *contentForReply, *msgID;
508   NSMutableDictionary *info;
509   NGImap4Envelope *sourceEnvelope;
510
511   [sourceMail fetchCoreInfos];
512
513   info = [NSMutableDictionary dictionaryWithCapacity: 16];
514   [info setObject: [sourceMail subjectForReply] forKey: @"subject"];
515
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"];
527   [self storeInfo];
528 }
529
530 - (void) fetchMailForForwarding: (SOGoMailObject *) sourceMail
531 {
532   NSDictionary *info, *attachment;
533   SOGoUser *currentUser;
534
535   [sourceMail fetchCoreInfos];
536
537   info = [NSDictionary dictionaryWithObject: [sourceMail subjectForForward]
538                        forKey: @"subject"];
539   [self setHeaders: info];
540   [self setSourceURL: [sourceMail imap4URLString]];
541   [self setSourceFlag: @"$Forwarded"];
542
543   /* attach message */
544   currentUser = [context activeUser];
545   if ([[currentUser messageForwarding] isEqualToString: @"inline"])
546     [self setText: [sourceMail contentForInlineForward]];
547   else
548     {
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",
554                                  nil];
555       [self saveAttachment: [sourceMail content]
556             withMetadata: attachment];
557     }
558   [self storeInfo];
559 }
560
561 /* accessors */
562
563 - (NSString *) sender
564 {
565   id tmp;
566   
567   if ((tmp = [headers objectForKey: @"from"]) == nil)
568     return nil;
569   if ([tmp isKindOfClass:[NSArray class]])
570     return [tmp count] > 0 ? [tmp objectAtIndex: 0] : nil;
571
572   return tmp;
573 }
574
575 /* attachments */
576
577 - (NSArray *) fetchAttachmentNames
578 {
579   NSMutableArray *ma;
580   NSFileManager *fm;
581   NSArray *files;
582   unsigned count, max;
583   NSString *filename;
584
585   fm = [NSFileManager defaultManager];
586   files = [fm directoryContentsAtPath: [self draftFolderPath]];
587
588   max = [files count];
589   ma = [NSMutableArray arrayWithCapacity: max];
590   for (count = 0; count < max; count++)
591     {
592       filename = [files objectAtIndex: count];
593       if (![filename hasPrefix: @"."])
594         [ma addObject: filename];
595     }
596
597   return ma;
598 }
599
600 - (BOOL) isValidAttachmentName: (NSString *) _name
601 {
602   static NSString *sescape[] = { @"/", @"..", @"~", @"\"", @"'", nil };
603   unsigned i;
604   NSRange  r;
605
606   if (![_name isNotNull])     return NO;
607   if ([_name length] == 0)    return NO;
608   if ([_name hasPrefix: @"."]) return NO;
609   
610   for (i = 0; sescape[i] != nil; i++) {
611     r = [_name rangeOfString:sescape[i]];
612     if (r.length > 0) return NO;
613   }
614   return YES;
615 }
616
617 - (NSString *) pathToAttachmentWithName: (NSString *) _name
618 {
619   if ([_name length] == 0)
620     return nil;
621   
622   return [[self draftFolderPath] stringByAppendingPathComponent:_name];
623 }
624
625 - (NSException *) invalidAttachmentNameError: (NSString *) _name
626 {
627   return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
628                       reason: @"Invalid attachment name!"];
629 }
630
631 - (NSException *) saveAttachment: (NSData *) _attach
632                     withMetadata: (NSDictionary *) metadata
633 {
634   NSString *p, *name, *mimeType;
635   NSRange r;
636
637   if (![_attach isNotNull]) {
638     return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
639                         reason: @"Missing attachment content!"];
640   }
641   
642   if (![self _ensureDraftFolderPath]) {
643     return [NSException exceptionWithHTTPStatus:500 /* Server Error */
644                         reason: @"Could not create folder for draft!"];
645   }
646
647   name = [metadata objectForKey: @"filename"];
648   r = [name rangeOfString: @"\\"
649             options: NSBackwardsSearch];
650   if (r.length > 0)
651     name = [name substringFromIndex: r.location + 1];
652
653   if (![self isValidAttachmentName: name])
654     return [self invalidAttachmentNameError: name];
655   
656   p = [self pathToAttachmentWithName: name];
657   if (![_attach writeToFile: p atomically: YES])
658     {
659       return [NSException exceptionWithHTTPStatus:500 /* Server Error */
660                           reason: @"Could not write attachment to draft!"];
661     }
662
663   mimeType = [metadata objectForKey: @"mimetype"];
664   if ([mimeType length] > 0)
665     {
666       p = [self pathToAttachmentWithName:
667                   [NSString stringWithFormat: @".%@.mime", name]];
668       if (![[mimeType dataUsingEncoding: NSUTF8StringEncoding]
669              writeToFile: p atomically: YES])
670         {
671           return [NSException exceptionWithHTTPStatus:500 /* Server Error */
672                               reason: @"Could not write attachment to draft!"];
673         }
674     }
675   
676   return nil; /* everything OK */
677 }
678
679 - (NSException *) deleteAttachmentWithName: (NSString *) _name
680 {
681   NSFileManager *fm;
682   NSString *p;
683   NSException *error;
684
685   error = nil;
686
687   if ([self isValidAttachmentName:_name]) 
688     {
689       fm = [NSFileManager defaultManager];
690       p = [self pathToAttachmentWithName:_name];
691       if ([fm fileExistsAtPath: p])
692         if (![fm removeFileAtPath: p handler: nil])
693           error
694             = [NSException exceptionWithHTTPStatus: 500 /* Server Error */
695                            reason: @"Could not delete attachment from draft!"];
696     }
697   else
698     error = [self invalidAttachmentNameError:_name];
699
700   return error;  
701 }
702
703 /* NGMime representations */
704
705 - (NGMimeBodyPart *) bodyPartForText
706 {
707   /*
708     This add the text typed by the user (the primary plain/text part).
709   */
710   NGMutableHashMap *map;
711   NGMimeBodyPart   *bodyPart;
712   
713   /* prepare header of body part */
714
715   map = [[[NGMutableHashMap alloc] initWithCapacity: 1] autorelease];
716
717   // TODO: set charset in header!
718   [map setObject: @"text/plain" forKey: @"content-type"];
719   if (text)
720     [map setObject: contentTypeValue forKey: @"content-type"];
721
722 //   if ((body = text) != nil) {
723 //     if ([body isKindOfClass: [NSString class]]) {
724 //       [map setObject: contentTypeValue
725 //         forKey: @"content-type"];
726 // //       body = [body dataUsingEncoding:NSUTF8StringEncoding];
727 //     }
728 //   }
729   
730   /* prepare body content */
731   
732   bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease];
733   [bodyPart setBody: text];
734
735   return bodyPart;
736 }
737
738 - (NGMimeMessage *) mimeMessageForContentWithHeaderMap: (NGMutableHashMap *) map
739 {
740   NGMimeMessage *message;  
741 //   BOOL     addSuffix;
742   id       body;
743
744   [map setObject: @"text/plain" forKey: @"content-type"];
745   body = text;
746   if (body)
747     {
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];
755 //       }
756 //       else if (addSuffix) {
757 //      [self warnWithFormat: @"Note: cannot add Internet marker to body: %@",
758 //            NSStringFromClass([body class])];
759 //       }
760
761         message = [[[NGMimeMessage alloc] initWithHeader:map] autorelease];
762         [message setBody: body];
763     }
764   else
765     message = nil;
766
767
768   return message;
769 }
770
771 - (NSString *) mimeTypeForExtension: (NSString *) _ext
772 {
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";
783 }
784
785 - (NSString *) contentTypeForAttachmentWithName: (NSString *) _name
786 {
787   NSString *s, *p;
788   NSData *mimeData;
789   
790   p = [self pathToAttachmentWithName:
791               [NSString stringWithFormat: @".%@.mime", _name]];
792   mimeData = [NSData dataWithContentsOfFile: p];
793   if (mimeData)
794     {
795       s = [[NSString alloc] initWithData: mimeData
796                             encoding: NSUTF8StringEncoding];
797       [s autorelease];
798     }
799   else
800     {
801       s = [self mimeTypeForExtension:[_name pathExtension]];
802       if ([_name length] > 0)
803         s = [s stringByAppendingFormat: @"; name=\"%@\"", _name];
804     }
805
806   return s;
807 }
808
809 - (NSString *) contentDispositionForAttachmentWithName: (NSString *) _name
810 {
811   NSString *type;
812   NSString *cdtype;
813   NSString *cd;
814   
815   type = [self contentTypeForAttachmentWithName:_name];
816   
817   if ([type hasPrefix: @"text/"])
818     cdtype = showTextAttachmentsInline ? @"inline" : @"attachment";
819   else if ([type hasPrefix: @"image/"] || [type hasPrefix: @"message"])
820     cdtype = @"inline";
821   else
822     cdtype = @"attachment";
823   
824   cd = [cdtype stringByAppendingString: @"; filename=\""];
825   cd = [cd stringByAppendingString: _name];
826   cd = [cd stringByAppendingString: @"\""];
827
828   // TODO: add size parameter (useful addition, RFC 2183)
829   return cd;
830 }
831
832 - (NGMimeBodyPart *) bodyPartForAttachmentWithName: (NSString *) _name
833 {
834   NSFileManager    *fm;
835   NGMutableHashMap *map;
836   NGMimeBodyPart   *bodyPart;
837   NSString         *s;
838   NSData           *content;
839   BOOL             attachAsString, is7bit;
840   NSString         *p;
841   id body;
842
843   if (_name == nil) return nil;
844
845   /* check attachment */
846   
847   fm = [NSFileManager defaultManager];
848   p  = [self pathToAttachmentWithName:_name];
849   if (![fm isReadableFileAtPath:p]) {
850     [self errorWithFormat: @"did not find attachment: '%@'", _name];
851     return nil;
852   }
853   attachAsString = NO;
854   is7bit         = NO;
855   
856   /* prepare header of body part */
857
858   map = [[[NGMutableHashMap alloc] initWithCapacity:4] autorelease];
859
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"])
865       is7bit = YES;
866   }
867   if ((s = [self contentDispositionForAttachmentWithName:_name]))
868     [map setObject:s forKey: @"content-disposition"];
869   
870   /* prepare body content */
871   
872   if (attachAsString) { // TODO: is this really necessary?
873     NSString *s;
874     
875     content = [[NSData alloc] initWithContentsOfMappedFile:p];
876     
877     s = [[NSString alloc] initWithData:content
878                           encoding:[NSString defaultCStringEncoding]];
879     if (s != nil) {
880       body = s;
881       [content release]; content = nil;
882     }
883     else {
884       [self warnWithFormat:
885               @"could not get text attachment as string: '%@'", _name];
886       body = content;
887       content = nil;
888     }
889   }
890   else if (is7bit) {
891     /* 
892        Note: Apparently NGMimeFileData objects are not processed by the MIME
893              generator!
894     */
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"];
899   }
900   else {
901     /* 
902        Note: in OGo this is done in LSWImapMailEditor.m:2477. Apparently
903              NGMimeFileData objects are not processed by the MIME generator!
904     */
905     NSData *encoded;
906     
907     content = [[NSData alloc] initWithContentsOfMappedFile:p];
908     encoded = [content dataByEncodingBase64];
909     [content release]; content = nil;
910     
911     [map setObject: @"base64" forKey: @"content-transfer-encoding"];
912     [map setObject:[NSNumber numberWithInt:[encoded length]] 
913          forKey: @"content-length"];
914     
915     /* Note: the -init method will create a temporary file! */
916     body = [[NGMimeFileData alloc] initWithBytes:[encoded bytes]
917                                    length:[encoded length]];
918   }
919   
920   bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease];
921   [bodyPart setBody:body];
922   
923   [body release]; body = nil;
924   return bodyPart;
925 }
926
927 - (NSArray *) bodyPartsForAllAttachments
928 {
929   /* returns nil on error */
930   NSArray  *names;
931   unsigned i, count;
932   NGMimeBodyPart *bodyPart;
933   NSMutableArray *bodyParts;
934
935   names = [self fetchAttachmentNames];
936   count = [names count];
937   bodyParts = [NSMutableArray arrayWithCapacity: count];
938
939   for (i = 0; i < count; i++)
940     {
941       bodyPart = [self bodyPartForAttachmentWithName: [names objectAtIndex: i]];
942       [bodyParts addObject: bodyPart];
943     }
944
945   return bodyParts;
946 }
947
948 - (NGMimeMessage *) mimeMultiPartMessageWithHeaderMap: (NGMutableHashMap *) map
949                                          andBodyParts: (NSArray *) _bodyParts
950 {
951   NGMimeMessage       *message;  
952   NGMimeMultipartBody *mBody;
953   NGMimeBodyPart      *part;
954   NSEnumerator        *e;
955   
956   [map addObject: MultiMixedType forKey: @"content-type"];
957
958   message = [[NGMimeMessage alloc] initWithHeader: map];
959   [message autorelease];
960   mBody = [[NGMimeMultipartBody alloc] initWithPart: message];
961
962   part = [self bodyPartForText];
963   [mBody addBodyPart: part];
964
965   e = [_bodyParts objectEnumerator];
966   part = [e nextObject];
967   while (part)
968     {
969       [mBody addBodyPart: part];
970       part = [e nextObject];
971     }
972
973   [message setBody: mBody];
974   [mBody release];
975
976   return message;
977 }
978
979 - (void) _addHeaders: (NSDictionary *) _h
980          toHeaderMap: (NGMutableHashMap *) _map
981 {
982   NSEnumerator *names;
983   NSString *name;
984
985   if ([_h count] == 0)
986     return;
987     
988   names = [_h keyEnumerator];
989   while ((name = [names nextObject]) != nil) {
990     id value;
991       
992     value = [_h objectForKey:name];
993     [_map addObject:value forKey:name];
994   }
995 }
996
997 - (BOOL) isEmptyValue: (id) _value
998 {
999   if (![_value isNotNull])
1000     return YES;
1001   
1002   if ([_value isKindOfClass: [NSArray class]])
1003     return [_value count] == 0 ? YES : NO;
1004   
1005   if ([_value isKindOfClass: [NSString class]])
1006     return [_value length] == 0 ? YES : NO;
1007
1008   return NO;
1009 }
1010
1011 - (NGMutableHashMap *) mimeHeaderMapWithHeaders: (NSDictionary *) _headers
1012 {
1013   NGMutableHashMap *map;
1014   NSArray      *emails;
1015   NSString     *s, *dateString;
1016   id           from, replyTo;
1017   
1018   map = [[[NGMutableHashMap alloc] initWithCapacity:16] autorelease];
1019   
1020   /* add recipients */
1021   
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"];
1028
1029   /* add senders */
1030   
1031   from = [headers objectForKey: @"from"];
1032   replyTo = [headers objectForKey: @"replyTo"];
1033   
1034   if (![self isEmptyValue:from]) {
1035     if ([from isKindOfClass:[NSArray class]])
1036       [map setObjects: from forKey: @"from"];
1037     else
1038       [map setObject: from forKey: @"from"];
1039   }
1040   
1041   if (![self isEmptyValue: replyTo]) {
1042     if ([from isKindOfClass:[NSArray class]])
1043       [map setObjects:from forKey: @"reply-to"];
1044     else
1045       [map setObject:from forKey: @"reply-to"];
1046   }
1047   else if (![self isEmptyValue:from])
1048     [map setObjects:[map objectsForKey: @"from"] forKey: @"reply-to"];
1049   
1050   /* add subject */
1051   if (inReplyTo)
1052     [map setObject: inReplyTo forKey: @"in-reply-to"];
1053
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"];
1058   
1059   /* add standard headers */
1060
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"];
1065
1066   /* add custom headers */
1067   
1068 //   [self _addHeaders: [lInfo objectForKey: @"headers"] toHeaderMap:map];
1069   [self _addHeaders: _headers toHeaderMap: map];
1070   
1071   return map;
1072 }
1073
1074 - (NGMimeMessage *) mimeMessageWithHeaders: (NSDictionary *) _headers
1075 {
1076   NGMutableHashMap  *map;
1077   NSArray           *bodyParts;
1078   NGMimeMessage     *message;
1079
1080   message = nil;
1081
1082   map = [self mimeHeaderMapWithHeaders: _headers];
1083   if (map)
1084     {
1085       [self debugWithFormat: @"MIME Envelope: %@", map];
1086   
1087       bodyParts = [self bodyPartsForAllAttachments];
1088       if (bodyParts)
1089         {
1090           [self debugWithFormat: @"attachments: %@", bodyParts];
1091   
1092           if ([bodyParts count] == 0)
1093             /* no attachments */
1094             message = [self mimeMessageForContentWithHeaderMap: map];
1095           else
1096             /* attachments, create multipart/mixed */
1097             message = [self mimeMultiPartMessageWithHeaderMap: map 
1098                             andBodyParts: bodyParts];
1099           [self debugWithFormat: @"message: %@", message];
1100         }
1101       else
1102         [self errorWithFormat:
1103                 @"could not create body parts for attachments!"];
1104     }
1105
1106   return message;
1107 }
1108
1109 - (NGMimeMessage *) mimeMessage
1110 {
1111   return [self mimeMessageWithHeaders: nil];
1112 }
1113
1114 - (NSData *) mimeMessageAsData
1115 {
1116   NGMimeMessageGenerator *generator;
1117   NSData *message;
1118
1119   generator = [NGMimeMessageGenerator new];
1120   message = [generator generateMimeFromPart: [self mimeMessage]];
1121   [generator release];
1122
1123   return message;
1124 }
1125
1126 - (NSArray *) allRecipients
1127 {
1128   NSMutableArray *allRecipients;
1129   NSArray *recipients;
1130   NSString *fieldNames[] = {@"to", @"cc", @"bcc"};
1131   unsigned int count;
1132
1133   allRecipients = [NSMutableArray arrayWithCapacity: 16];
1134
1135   for (count = 0; count < 3; count++)
1136     {
1137       recipients = [headers objectForKey: fieldNames[count]];
1138       if ([recipients count] > 0)
1139         [allRecipients addObjectsFromArray: recipients];
1140     }
1141
1142   return allRecipients;
1143 }
1144
1145 - (NSException *) sendMail
1146 {
1147   NSException *error;
1148   SOGoMailFolder *sentFolder;
1149   NSData *message;
1150   NSURL *sourceIMAP4URL;
1151   
1152   /* send mail */
1153   sentFolder = [[self mailAccountFolder] sentFolderInContext: context];
1154   if ([sentFolder isKindOfClass: [NSException class]])
1155     error = (NSException *) sentFolder;
1156   else
1157     {
1158       message = [self mimeMessageAsData];
1159       error = [[SOGoMailer sharedMailer] sendMailData: message
1160                                          toRecipients: [self allRecipients]
1161                                          sender: [self sender]];
1162       if (!error)
1163         {
1164           error = [sentFolder postData: message flags: @"seen"];
1165           if (!error)
1166             {
1167               [self imap4Connection];
1168               if (IMAP4ID > -1)
1169                 [imap4 markURLDeleted: [self imap4URL]];
1170               if (sourceURL && sourceFlag)
1171                 {
1172                   sourceIMAP4URL = [NSURL URLWithString: sourceURL];
1173                   [imap4 addFlags: sourceFlag toURL: sourceIMAP4URL];
1174                 }
1175               if (!draftDeleteDisabled)
1176                 error = [self delete];
1177             }
1178         }
1179     }
1180
1181   return error;
1182 }
1183
1184 - (NSException *) delete
1185 {
1186   NSException *error;
1187
1188   if ([[NSFileManager defaultManager]
1189         removeFileAtPath: [self draftFolderPath]
1190         handler: nil])
1191     error = nil;
1192   else
1193     error = [NSException exceptionWithHTTPStatus: 500 /* server error */
1194                          reason: @"could not delete draft"];
1195
1196   return error;
1197 }
1198
1199 /* operations */
1200
1201 - (NSString *) contentAsString
1202 {
1203   NSString *str;
1204   NSData *message;
1205
1206   message = [self mimeMessageAsData];
1207   if (message)
1208     {
1209       str = [[NSString alloc] initWithData: message
1210                               encoding: NSUTF8StringEncoding];
1211       if (!str)
1212         [self errorWithFormat: @"could not load draft as UTF-8 (data size=%d)",
1213               [message length]];
1214       else
1215         [str autorelease];
1216     }
1217   else
1218     {
1219       [self errorWithFormat: @"message data is empty"];
1220       str = nil;
1221     }
1222
1223   return str;
1224 }
1225
1226 /* debugging */
1227
1228 - (BOOL) isDebuggingEnabled
1229 {
1230   return debugOn;
1231 }
1232
1233 @end /* SOGoDraftObject */