]> err.no Git - scalable-opengroupware.org/blob - SoObjects/Mailer/SOGoDraftObject.m
moved SOGo files up
[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 #include "SOGoDraftObject.h"
23 #include <SoObjects/SOGo/WOContext+Agenor.h>
24 #include <NGMail/NGMimeMessage.h>
25 #include <NGMail/NGMimeMessageGenerator.h>
26 #include <NGMail/NGSendMail.h>
27 #include <NGMime/NGMimeBodyPart.h>
28 #include <NGMime/NGMimeFileData.h>
29 #include <NGMime/NGMimeMultipartBody.h>
30 #include <NGMime/NGMimeType.h>
31 #include <NGImap4/NGImap4Envelope.h>
32 #include <NGImap4/NGImap4EnvelopeAddress.h>
33 #include <NGExtensions/NSFileManager+Extensions.h>
34 #include "common.h"
35
36 @implementation SOGoDraftObject
37
38 static NGMimeType  *TextPlainType  = nil;
39 static NGMimeType  *MultiMixedType = nil;
40 static NSString    *userAgent      = @"SOGoMail 1.0";
41 static BOOL        draftDeleteDisabled = NO; // for debugging
42 static BOOL        debugOn = NO;
43 static BOOL        showTextAttachmentsInline  = NO;
44 static NSString    *fromInternetSuffixPattern = nil;
45
46 + (int)version {
47   return [super version] + 0 /* v1 */;
48 }
49
50 + (void)initialize {
51   NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
52   
53   NSAssert2([super version] == 1,
54             @"invalid superclass (%@) version %i !",
55             NSStringFromClass([self superclass]), [super version]);
56   
57   /* Note: be aware of the charset issues before enabling this! */
58   showTextAttachmentsInline = [ud boolForKey:@"SOGoShowTextAttachmentsInline"];
59   
60   if ((draftDeleteDisabled = [ud boolForKey:@"SOGoNoDraftDeleteAfterSend"]))
61     NSLog(@"WARNING: draft delete is disabled! (SOGoNoDraftDeleteAfterSend)");
62   
63   fromInternetSuffixPattern = [ud stringForKey:@"SOGoInternetMailSuffix"];
64   if ([fromInternetSuffixPattern length] == 0)
65     NSLog(@"Note: no 'SOGoInternetMailSuffix' is configured.");
66   else {
67     fromInternetSuffixPattern =
68       [@"\n" stringByAppendingString:fromInternetSuffixPattern];
69   }
70   
71   TextPlainType  = [[NGMimeType mimeType:@"text"      subType:@"plain"]  copy];
72   MultiMixedType = [[NGMimeType mimeType:@"multipart" subType:@"mixed"]  copy];
73 }
74
75 - (void)dealloc {
76   [self->envelope release];
77   [self->info release];
78   [self->path release];
79   [super dealloc];
80 }
81
82 /* draft folder functionality */
83
84 - (NSFileManager *)spoolFileManager {
85   return [[self container] spoolFileManager];
86 }
87 - (NSString *)userSpoolFolderPath {
88   return [[self container] userSpoolFolderPath];
89 }
90 - (BOOL)_ensureUserSpoolFolderPath {
91   return [[self container] _ensureUserSpoolFolderPath];
92 }
93
94 /* draft object functionality */
95
96 - (NSString *)draftFolderPath {
97   if (self->path != nil)
98     return self->path;
99   
100   self->path = [[[self userSpoolFolderPath] stringByAppendingPathComponent:
101                                               [self nameInContainer]] copy];
102   return self->path;
103 }
104 - (BOOL)_ensureDraftFolderPath {
105   NSFileManager *fm;
106   
107   if (![self _ensureUserSpoolFolderPath])
108     return NO;
109   
110   if ((fm = [self spoolFileManager]) == nil) {
111     [self errorWithFormat:@"missing spool file manager!"];
112     return NO;
113   }
114   return [fm createDirectoriesAtPath:[self draftFolderPath] attributes:nil];
115 }
116
117 - (NSString *)infoPath {
118   return [[self draftFolderPath] 
119                 stringByAppendingPathComponent:@".info.plist"];
120 }
121
122 /* contents */
123
124 - (NSException *)storeInfo:(NSDictionary *)_info {
125   if (_info == nil) {
126     return [NSException exceptionWithHTTPStatus:500 /* server error */
127                         reason:@"got no info to write for draft!"];
128   }
129   if (![self _ensureDraftFolderPath]) {
130     [self errorWithFormat:@"could not create folder for draft: '%@'",
131             [self draftFolderPath]];
132     return [NSException exceptionWithHTTPStatus:500 /* server error */
133                         reason:@"could not create folder for draft!"];
134   }
135   if (![_info writeToFile:[self infoPath] atomically:YES]) {
136     [self errorWithFormat:@"could not write info: '%@'", [self infoPath]];
137     return [NSException exceptionWithHTTPStatus:500 /* server error */
138                         reason:@"could not write draft info!"];
139   }
140   
141   /* reset info cache */
142   [self->info release]; self->info = nil;
143   
144   return nil /* everything is excellent */;
145 }
146 - (NSDictionary *)fetchInfo {
147   NSString *p;
148
149   if (self->info != nil)
150     return self->info;
151   
152   p = [self infoPath];
153   if (![[self spoolFileManager] fileExistsAtPath:p]) {
154     [self debugWithFormat:@"Note: info object does not yet exist: %@", p];
155     return nil;
156   }
157   
158   self->info = [[NSDictionary alloc] initWithContentsOfFile:p];
159   if (self->info == nil)
160     [self errorWithFormat:@"draft info dictionary broken at path: %@", p];
161   
162   return self->info;
163 }
164
165 /* accessors */
166
167 - (NSString *)sender {
168   id tmp;
169   
170   if ((tmp = [[self fetchInfo] objectForKey:@"from"]) == nil)
171     return nil;
172   if ([tmp isKindOfClass:[NSArray class]])
173     return [tmp count] > 0 ? [tmp objectAtIndex:0] : nil;
174   return tmp;
175 }
176
177 /* attachments */
178
179 - (NSArray *)fetchAttachmentNames {
180   NSMutableArray *ma;
181   NSFileManager  *fm;
182   NSArray        *files;
183   unsigned i, count;
184   
185   fm = [self spoolFileManager];
186   if ((files = [fm directoryContentsAtPath:[self draftFolderPath]]) == nil)
187     return nil;
188   
189   count = [files count];
190   ma    = [NSMutableArray arrayWithCapacity:count];
191   for (i = 0; i < count; i++) {
192     NSString *filename;
193     
194     filename = [files objectAtIndex:i];
195     if ([filename hasPrefix:@"."])
196       continue;
197     
198     [ma addObject:filename];
199   }
200   return ma;
201 }
202
203 - (BOOL)isValidAttachmentName:(NSString *)_name {
204   static NSString *sescape[] = { @"/", @"..", @"~", @"\"", @"'", @" ", nil };
205   unsigned i;
206   NSRange  r;
207
208   if (![_name isNotNull])     return NO;
209   if ([_name length] == 0)    return NO;
210   if ([_name hasPrefix:@"."]) return NO;
211   
212   for (i = 0; sescape[i] != nil; i++) {
213     r = [_name rangeOfString:sescape[i]];
214     if (r.length > 0) return NO;
215   }
216   return YES;
217 }
218
219 - (NSString *)pathToAttachmentWithName:(NSString *)_name {
220   if ([_name length] == 0)
221     return nil;
222   
223   return [[self draftFolderPath] stringByAppendingPathComponent:_name];
224 }
225
226 - (NSException *)invalidAttachmentNameError:(NSString *)_name {
227   return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
228                       reason:@"Invalid attachment name!"];
229 }
230
231 - (NSException *)saveAttachment:(NSData *)_attach withName:(NSString *)_name {
232   NSString *p;
233   
234   if (![_attach isNotNull]) {
235     return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
236                         reason:@"Missing attachment content!"];
237   }
238   
239   if (![self _ensureDraftFolderPath]) {
240     return [NSException exceptionWithHTTPStatus:500 /* Server Error */
241                         reason:@"Could not create folder for draft!"];
242   }
243   if (![self isValidAttachmentName:_name])
244     return [self invalidAttachmentNameError:_name];
245   
246   p = [self pathToAttachmentWithName:_name];
247   if (![_attach writeToFile:p atomically:YES]) {
248     return [NSException exceptionWithHTTPStatus:500 /* Server Error */
249                         reason:@"Could not write attachment to draft!"];
250   }
251   
252   return nil; /* everything OK */
253 }
254
255 - (NSException *)deleteAttachmentWithName:(NSString *)_name {
256   NSFileManager *fm;
257   NSString *p;
258   
259   if (![self isValidAttachmentName:_name])
260     return [self invalidAttachmentNameError:_name];
261   
262   fm = [self spoolFileManager];
263   p  = [self pathToAttachmentWithName:_name];
264   if (![fm fileExistsAtPath:p])
265     return nil; /* well, doesn't exist, so its deleted ;-) */
266   
267   if (![fm removeFileAtPath:p handler:nil]) {
268     [self logWithFormat:@"ERROR: failed to delete file: %@", p];
269     return [NSException exceptionWithHTTPStatus:500 /* Server Error */
270                         reason:@"Could not delete attachment from draft!"];
271   }
272   return nil; /* everything OK */
273 }
274
275 /* NGMime representations */
276
277 - (NGMimeBodyPart *)bodyPartForText {
278   /*
279     This add the text typed by the user (the primary plain/text part).
280   */
281   NGMutableHashMap *map;
282   NGMimeBodyPart   *bodyPart;
283   NSDictionary     *lInfo;
284   id body;
285   
286   if ((lInfo = [self fetchInfo]) == nil)
287     return nil;
288   
289   /* prepare header of body part */
290
291   map = [[[NGMutableHashMap alloc] initWithCapacity:2] autorelease];
292
293   // TODO: set charset in header!
294   [map setObject:@"text/plain" forKey:@"content-type"];
295   if ((body = [lInfo objectForKey:@"text"]) != nil) {
296     if ([body isKindOfClass:[NSString class]]) {
297       [map setObject:@"text/plain; charset=utf-8" forKey:@"content-type"];
298       body = [body dataUsingEncoding:NSUTF8StringEncoding];
299     }
300   }
301   
302   /* prepare body content */
303   
304   bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease];
305   [bodyPart setBody:body];
306   return bodyPart;
307 }
308
309 - (NGMimeMessage *)mimeMessageForContentWithHeaderMap:(NGMutableHashMap *)map {
310   NSDictionary  *lInfo;
311   NGMimeMessage *message;  
312   WOContext     *ctx;
313   NSString *fromInternetSuffix;
314   BOOL     addSuffix;
315   id       body;
316
317   if ((lInfo = [self fetchInfo]) == nil)
318     return nil;
319   
320   ctx       = [[WOApplication application] context];
321   addSuffix = [ctx isAccessFromIntranet] ? NO : YES;
322   if (addSuffix) {
323     fromInternetSuffix = 
324       [fromInternetSuffixPattern stringByReplacingVariablesWithBindings:
325                                    [ctx request]
326                                  stringForUnknownBindings:@""];
327     
328     addSuffix = [fromInternetSuffix length] > 0 ? YES : NO;
329   }
330   
331   [map setObject:@"text/plain" forKey:@"content-type"];
332   if ((body = [lInfo objectForKey:@"text"]) != nil) {
333     if ([body isKindOfClass:[NSString class]]) {
334       if (addSuffix)
335         body = [body stringByAppendingString:fromInternetSuffix];
336       
337       /* Note: just 'utf8' is displayed wrong in Mail.app */
338       [map setObject:@"text/plain; charset=utf-8" forKey:@"content-type"];
339       body = [body dataUsingEncoding:NSUTF8StringEncoding];
340     }
341     else if ([body isKindOfClass:[NSData class]] && addSuffix) {
342       body = [[body mutableCopy] autorelease];
343       [(NSMutableData *)body appendData:
344                           [fromInternetSuffix dataUsingEncoding:
345                                                 NSUTF8StringEncoding]];
346     }
347     else if (addSuffix) {
348       [self warnWithFormat:@"Note: cannot add Internet marker to body: %@",
349               NSStringFromClass([body class])];
350     }
351   }
352   else if (addSuffix)
353     body = [fromInternetSuffix dataUsingEncoding:NSUTF8StringEncoding];
354   
355   message = [[[NGMimeMessage alloc] initWithHeader:map] autorelease];
356   [message setBody:body];
357   return message;
358 }
359
360 - (NSString *)mimeTypeForExtension:(NSString *)_ext {
361   // TODO: make configurable
362   // TODO: use /etc/mime-types
363   if ([_ext isEqualToString:@"txt"])  return @"text/plain";
364   if ([_ext isEqualToString:@"html"]) return @"text/html";
365   if ([_ext isEqualToString:@"htm"])  return @"text/html";
366   if ([_ext isEqualToString:@"gif"])  return @"image/gif";
367   if ([_ext isEqualToString:@"jpg"])  return @"image/jpeg";
368   if ([_ext isEqualToString:@"jpeg"]) return @"image/jpeg";
369   if ([_ext isEqualToString:@"mail"]) return @"message/rfc822";
370   return @"application/octet-stream";
371 }
372
373 - (NSString *)contentTypeForAttachmentWithName:(NSString *)_name {
374   NSString *s;
375   
376   s = [self mimeTypeForExtension:[_name pathExtension]];
377   if ([_name length] > 0) {
378     s = [s stringByAppendingString:@"; name=\""];
379     s = [s stringByAppendingString:_name];
380     s = [s stringByAppendingString:@"\""];
381   }
382   return s;
383 }
384 - (NSString *)contentDispositionForAttachmentWithName:(NSString *)_name {
385   NSString *type;
386   NSString *cdtype;
387   NSString *cd;
388   
389   type = [self contentTypeForAttachmentWithName:_name];
390   
391   if ([type hasPrefix:@"text/"])
392     cdtype = showTextAttachmentsInline ? @"inline" : @"attachment";
393   else if ([type hasPrefix:@"image/"] || [type hasPrefix:@"message"])
394     cdtype = @"inline";
395   else
396     cdtype = @"attachment";
397   
398   cd = [cdtype stringByAppendingString:@"; filename=\""];
399   cd = [cd stringByAppendingString:_name];
400   cd = [cd stringByAppendingString:@"\""];
401   
402   // TODO: add size parameter (useful addition, RFC 2183)
403   return cd;
404 }
405
406 - (NGMimeBodyPart *)bodyPartForAttachmentWithName:(NSString *)_name {
407   NSFileManager    *fm;
408   NGMutableHashMap *map;
409   NGMimeBodyPart   *bodyPart;
410   NSString         *s;
411   NSData           *content;
412   BOOL             attachAsString, is7bit;
413   NSString         *p;
414   id body;
415
416   if (_name == nil) return nil;
417
418   /* check attachment */
419   
420   fm = [self spoolFileManager];
421   p  = [self pathToAttachmentWithName:_name];
422   if (![fm isReadableFileAtPath:p]) {
423     [self errorWithFormat:@"did not find attachment: '%@'", _name];
424     return nil;
425   }
426   attachAsString = NO;
427   is7bit         = NO;
428   
429   /* prepare header of body part */
430
431   map = [[[NGMutableHashMap alloc] initWithCapacity:4] autorelease];
432
433   if ((s = [self contentTypeForAttachmentWithName:_name]) != nil) {
434     [map setObject:s forKey:@"content-type"];
435     if ([s hasPrefix:@"text/"])
436       attachAsString = YES;
437     else if ([s hasPrefix:@"message/rfc822"])
438       is7bit = YES;
439   }
440   if ((s = [self contentDispositionForAttachmentWithName:_name]))
441     [map setObject:s forKey:@"content-disposition"];
442   
443   /* prepare body content */
444   
445   if (attachAsString) { // TODO: is this really necessary?
446     NSString *s;
447     
448     content = [[NSData alloc] initWithContentsOfMappedFile:p];
449     
450     s = [[NSString alloc] initWithData:content
451                           encoding:[NSString defaultCStringEncoding]];
452     if (s != nil) {
453       body = s;
454       [content release]; content = nil;
455     }
456     else {
457       [self warnWithFormat:
458               @"could not get text attachment as string: '%@'", _name];
459       body = content;
460       content = nil;
461     }
462   }
463   else if (is7bit) {
464     /* 
465        Note: Apparently NGMimeFileData objects are not processed by the MIME
466              generator!
467     */
468     body = [[NGMimeFileData alloc] initWithPath:p removeFile:NO];
469     [map setObject:@"7bit" forKey:@"content-transfer-encoding"];
470     [map setObject:[NSNumber numberWithInt:[body length]] 
471          forKey:@"content-length"];
472   }
473   else {
474     /* 
475        Note: in OGo this is done in LSWImapMailEditor.m:2477. Apparently
476              NGMimeFileData objects are not processed by the MIME generator!
477     */
478     NSData *encoded;
479     
480     content = [[NSData alloc] initWithContentsOfMappedFile:p];
481     encoded = [content dataByEncodingBase64];
482     [content release]; content = nil;
483     
484     [map setObject:@"base64" forKey:@"content-transfer-encoding"];
485     [map setObject:[NSNumber numberWithInt:[encoded length]] 
486          forKey:@"content-length"];
487     
488     /* Note: the -init method will create a temporary file! */
489     body = [[NGMimeFileData alloc] initWithBytes:[encoded bytes]
490                                    length:[encoded length]];
491   }
492   
493   bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease];
494   [bodyPart setBody:body];
495   
496   [body release]; body = nil;
497   return bodyPart;
498 }
499
500 - (NSArray *)bodyPartsForAllAttachments {
501   /* returns nil on error */
502   NSMutableArray *bodyParts;
503   NSArray  *names;
504   unsigned i, count;
505   
506   names = [self fetchAttachmentNames];
507   if ((count = [names count]) == 0)
508     return [NSArray array];
509   
510   bodyParts = [NSMutableArray arrayWithCapacity:count];
511   for (i = 0; i < count; i++) {
512     NGMimeBodyPart *bodyPart;
513     
514     bodyPart = [self bodyPartForAttachmentWithName:[names objectAtIndex:i]];
515     if (bodyPart == nil)
516       return nil;
517     
518     [bodyParts addObject:bodyPart];
519   }
520   return bodyParts;
521 }
522
523 - (NGMimeMessage *)mimeMultiPartMessageWithHeaderMap:(NGMutableHashMap *)map
524   andBodyParts:(NSArray *)_bodyParts
525 {
526   NGMimeMessage       *message;  
527   NGMimeMultipartBody *mBody;
528   NGMimeBodyPart      *part;
529   NSEnumerator        *e;
530   
531   [map addObject:MultiMixedType forKey:@"content-type"];
532     
533   message = [[[NGMimeMessage alloc] initWithHeader:map] autorelease];
534   mBody   = [[NGMimeMultipartBody alloc] initWithPart:message];
535   
536   part = [self bodyPartForText];
537   [mBody addBodyPart:part];
538   
539   e = [_bodyParts objectEnumerator];
540   while ((part = [e nextObject]) != nil)
541     [mBody addBodyPart:part];
542   
543   [message setBody:mBody];
544   [mBody release]; mBody = nil;
545   return message;
546 }
547
548 - (void)_addHeaders:(NSDictionary *)_h toHeaderMap:(NGMutableHashMap *)_map {
549   NSEnumerator *names;
550   NSString *name;
551
552   if ([_h count] == 0)
553     return;
554     
555   names = [_h keyEnumerator];
556   while ((name = [names nextObject]) != nil) {
557     id value;
558       
559     value = [_h objectForKey:name];
560     [_map addObject:value forKey:name];
561   }
562 }
563
564 - (BOOL)isEmptyValue:(id)_value {
565   if (![_value isNotNull])
566     return YES;
567   
568   if ([_value isKindOfClass:[NSArray class]])
569     return [_value count] == 0 ? YES : NO;
570   
571   if ([_value isKindOfClass:[NSString class]])
572     return [_value length] == 0 ? YES : NO;
573   
574   return NO;
575 }
576
577 - (NGMutableHashMap *)mimeHeaderMapWithHeaders:(NSDictionary *)_headers {
578   NGMutableHashMap *map;
579   NSDictionary *lInfo; // TODO: this should be some kind of object?
580   NSArray      *emails;
581   NSString     *s;
582   id           from, replyTo;
583   
584   if ((lInfo = [self fetchInfo]) == nil)
585     return nil;
586   
587   map = [[[NGMutableHashMap alloc] initWithCapacity:16] autorelease];
588   
589   /* add recipients */
590   
591   if ((emails = [lInfo objectForKey:@"to"]) != nil) {
592     if ([emails count] == 0) {
593       [self errorWithFormat:@"missing 'to' recipient in email!"];
594       return nil;
595     }
596     [map setObjects:emails forKey:@"to"];
597   }
598   if ((emails = [lInfo objectForKey:@"cc"]) != nil)
599     [map setObjects:emails forKey:@"cc"];
600   if ((emails = [lInfo objectForKey:@"bcc"]) != nil)
601     [map setObjects:emails forKey:@"bcc"];
602   
603   /* add senders */
604   
605   from    = [lInfo objectForKey:@"from"];
606   replyTo = [lInfo objectForKey:@"replyTo"];
607   
608   if (![self isEmptyValue:from]) {
609     if ([from isKindOfClass:[NSArray class]])
610       [map setObjects:from forKey:@"from"];
611     else
612       [map setObject:from forKey:@"from"];
613   }
614   
615   if (![self isEmptyValue:replyTo]) {
616     if ([from isKindOfClass:[NSArray class]])
617       [map setObjects:from forKey:@"reply-to"];
618     else
619       [map setObject:from forKey:@"reply-to"];
620   }
621   else if (![self isEmptyValue:from])
622     [map setObjects:[map objectsForKey:@"from"] forKey:@"reply-to"];
623   
624   /* add subject */
625   
626   if ([(s = [lInfo objectForKey:@"subject"]) length] > 0)
627     [map setObject:s forKey:@"subject"];
628   
629   /* add standard headers */
630   
631   [map addObject:[NSCalendarDate date] forKey:@"date"];
632   [map addObject:@"1.0"                forKey:@"MIME-Version"];
633   [map addObject:userAgent             forKey:@"X-Mailer"];
634
635   /* add custom headers */
636   
637   [self _addHeaders:[lInfo objectForKey:@"headers"] toHeaderMap:map];
638   [self _addHeaders:_headers                        toHeaderMap:map];
639   
640   return map;
641 }
642
643 - (NGMimeMessage *)mimeMessageWithHeaders:(NSDictionary *)_headers {
644   NSAutoreleasePool *pool;
645   NGMutableHashMap  *map;
646   NSArray           *bodyParts;
647   NGMimeMessage     *message;
648   
649   pool = [[NSAutoreleasePool alloc] init];
650   
651   if ([self fetchInfo] == nil) {
652     [self errorWithFormat:@"could not locate draft fetch info!"];
653     return nil;
654   }
655   
656   if ((map = [self mimeHeaderMapWithHeaders:_headers]) == nil)
657     return nil;
658   [self debugWithFormat:@"MIME Envelope: %@", map];
659   
660   if ((bodyParts = [self bodyPartsForAllAttachments]) == nil) {
661     [self errorWithFormat:
662             @"could not create body parts for attachments!"];
663     return nil; // TODO: improve error handling, return exception
664   }
665   [self debugWithFormat:@"attachments: %@", bodyParts];
666   
667   if ([bodyParts count] == 0) {
668     /* no attachments */
669     message = [self mimeMessageForContentWithHeaderMap:map];
670   }
671   else {
672     /* attachments, create multipart/mixed */
673     message = [self mimeMultiPartMessageWithHeaderMap:map 
674                     andBodyParts:bodyParts];
675   }
676   [self debugWithFormat:@"message: %@", message];
677
678   message = [message retain];
679   [pool release];
680   return [message autorelease];
681 }
682 - (NGMimeMessage *)mimeMessage {
683   return [self mimeMessageWithHeaders:nil];
684 }
685
686 - (NSString *)saveMimeMessageToTemporaryFileWithHeaders:(NSDictionary *)_h {
687   NGMimeMessageGenerator *gen;
688   NSAutoreleasePool *pool;
689   NGMimeMessage *message;
690   NSString      *tmpPath;
691
692   pool = [[NSAutoreleasePool alloc] init];
693   
694   message = [self mimeMessageWithHeaders:_h];
695   if (![message isNotNull])
696     return nil;
697   if ([message isKindOfClass:[NSException class]]) {
698     [self errorWithFormat:@"error: %@", message];
699     return nil;
700   }
701   
702   gen     = [[NGMimeMessageGenerator alloc] init];
703   tmpPath = [[gen generateMimeFromPartToFile:message] copy];
704   [gen release]; gen = nil;
705   
706   [pool release];
707   return [tmpPath autorelease];
708 }
709 - (NSString *)saveMimeMessageToTemporaryFile {
710   return [self saveMimeMessageToTemporaryFileWithHeaders:nil];
711 }
712
713 - (void)deleteTemporaryMessageFile:(NSString *)_path {
714   NSFileManager *fm;
715   
716   if (![_path isNotNull])
717     return;
718
719   fm = [NSFileManager defaultManager];
720   if (![fm fileExistsAtPath:_path])
721     return;
722   
723   [fm removeFileAtPath:_path handler:nil];
724 }
725
726 - (NSArray *)allRecipients {
727   NSDictionary   *lInfo;
728   NSMutableArray *ma;
729   NSArray        *tmp;
730   
731   if ((lInfo = [self fetchInfo]) == nil)
732     return nil;
733   
734   ma = [NSMutableArray arrayWithCapacity:16];
735   if ((tmp = [lInfo objectForKey:@"to"]) != nil)
736     [ma addObjectsFromArray:tmp];
737   if ((tmp = [lInfo objectForKey:@"cc"]) != nil)
738     [ma addObjectsFromArray:tmp];
739   if ((tmp = [lInfo objectForKey:@"bcc"]) != nil)
740     [ma addObjectsFromArray:tmp];
741   return ma;
742 }
743
744 - (NSException *)sendMimeMessageAtPath:(NSString *)_path {
745   static NGSendMail *mailer = nil;
746   NSArray  *recipients;
747   NSString *from;
748   
749   /* validate */
750   
751   recipients = [self allRecipients];
752   from       = [self sender];
753   if ([recipients count] == 0) {
754     return [NSException exceptionWithHTTPStatus:500 /* server error */
755                         reason:@"draft has no recipients set!"];
756   }
757   if ([from length] == 0) {
758     return [NSException exceptionWithHTTPStatus:500 /* server error */
759                         reason:@"draft has no sender (from) set!"];
760   }
761   
762   /* setup mailer object */
763   
764   if (mailer == nil)
765     mailer = [[NGSendMail sharedSendMail] retain];
766   if (![mailer isSendMailAvailable]) {
767     [self errorWithFormat:@"missing sendmail binary!"];
768     return [NSException exceptionWithHTTPStatus:500 /* server error */
769                         reason:@"did not find sendmail binary!"];
770   }
771   
772   /* send mail */
773   
774   return [mailer sendMailAtPath:_path toRecipients:recipients sender:from];
775 }
776
777 - (NSException *)sendMail {
778   NSException *error;
779   NSString    *tmpPath;
780   
781   /* save MIME mail to file */
782   
783   tmpPath = [self saveMimeMessageToTemporaryFile];
784   if (![tmpPath isNotNull]) {
785     return [NSException exceptionWithHTTPStatus:500 /* server error */
786                         reason:@"could not save MIME message for draft!"];
787   }
788   
789   /* send mail */
790   error = [self sendMimeMessageAtPath:tmpPath];
791   
792   /* delete temporary file */
793   [self deleteTemporaryMessageFile:tmpPath];
794
795   return error;
796 }
797
798 /* operations */
799
800 - (NSException *)delete {
801   NSFileManager *fm;
802   NSString      *p, *sp;
803   NSEnumerator  *e;
804   
805   if ((fm = [self spoolFileManager]) == nil) {
806     [self errorWithFormat:@"missing spool file manager!"];
807     return [NSException exceptionWithHTTPStatus:500 /* server error */
808                         reason:@"missing spool file manager!"];
809   }
810   
811   p = [self draftFolderPath];
812   if (![fm fileExistsAtPath:p]) {
813     return [NSException exceptionWithHTTPStatus:404 /* not found */
814                         reason:@"did not find draft!"];
815   }
816   
817   e = [[fm directoryContentsAtPath:p] objectEnumerator];
818   while ((sp = [e nextObject])) {
819     sp = [p stringByAppendingPathComponent:sp];
820     if (draftDeleteDisabled) {
821       [self logWithFormat:@"should delete draft file %@ ...", sp];
822       continue;
823     }
824     
825     if (![fm removeFileAtPath:sp handler:nil]) {
826       return [NSException exceptionWithHTTPStatus:500 /* server error */
827                           reason:@"failed to delete draft!"];
828     }
829   }
830
831   if (draftDeleteDisabled) {
832     [self logWithFormat:@"should delete draft directory: %@", p];
833   }
834   else {
835     if (![fm removeFileAtPath:p handler:nil]) {
836       return [NSException exceptionWithHTTPStatus:500 /* server error */
837                           reason:@"failed to delete draft directory!"];
838     }
839   }
840   return nil;
841 }
842
843 - (NSData *)content {
844   /* Note: does not cache, expensive operation */
845   NSData   *data;
846   NSString *p;
847   
848   if ((p = [self saveMimeMessageToTemporaryFile]) == nil)
849     return nil;
850   
851   data = [NSData dataWithContentsOfMappedFile:p];
852   
853   /* delete temporary file */
854   [self deleteTemporaryMessageFile:p];
855
856   return data;
857 }
858 - (NSString *)contentAsString {
859   NSString *str;
860   NSData   *data;
861   
862   if ((data = [self content]) == nil)
863     return nil;
864   
865   str = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
866   if (str == nil) {
867     [self errorWithFormat:@"could not load draft as ASCII (data size=%d)",
868           [data length]];
869     return nil;
870   }
871
872   return [str autorelease];
873 }
874
875 /* actions */
876
877 - (id)DELETEAction:(id)_ctx {
878   NSException *error;
879
880   if ((error = [self delete]) != nil)
881     return error;
882   
883   return [NSNumber numberWithBool:YES]; /* delete worked out ... */
884 }
885
886 - (id)GETAction:(id)_ctx {
887   /* 
888      Override, because SOGoObject's GETAction uses the less efficient
889      -contentAsString method.
890   */
891   WORequest *rq;
892
893   rq = [_ctx request];
894   if ([rq isSoWebDAVRequest]) {
895     WOResponse *r;
896     NSData     *content;
897     
898     if ((content = [self content]) == nil) {
899       return [NSException exceptionWithHTTPStatus:500
900                           reason:@"Could not generate MIME content!"];
901     }
902     r = [_ctx response];
903     [r setHeader:@"message/rfc822" forKey:@"content-type"];
904     [r setContent:content];
905     return r;
906   }
907   
908   return [super GETAction:_ctx];
909 }
910
911 /* fake being a SOGoMailObject */
912
913 - (id)fetchParts:(NSArray *)_parts {
914   return [NSDictionary dictionaryWithObject:self forKey:@"fetch"];
915 }
916
917 - (NSString *)uid {
918   return [self nameInContainer];
919 }
920 - (NSArray *)flags {
921   static NSArray *seenFlags = nil;
922   seenFlags = [[NSArray alloc] initWithObjects:@"seen", nil];
923   return seenFlags;
924 }
925 - (unsigned)size {
926   // TODO: size, hard to support, we would need to generate MIME?
927   return 0;
928 }
929
930 - (NSArray *)imap4EnvelopeAddressesForStrings:(NSArray *)_emails {
931   NSMutableArray *ma;
932   unsigned i, count;
933   
934   if (_emails == nil)
935     return nil;
936   if ((count = [_emails count]) == 0)
937     return [NSArray array];
938
939   ma = [NSMutableArray arrayWithCapacity:count];
940   for (i = 0; i < count; i++) {
941     NGImap4EnvelopeAddress *envaddr;
942
943     envaddr = [[NGImap4EnvelopeAddress alloc] 
944                 initWithString:[_emails objectAtIndex:i]];
945     if ([envaddr isNotNull])
946       [ma addObject:envaddr];
947     [envaddr release];
948   }
949   return ma;
950 }
951
952 - (NGImap4Envelope *)envelope {
953   NSDictionary *lInfo;
954   id from, replyTo;
955   
956   if (self->envelope != nil)
957     return self->envelope;
958   if ((lInfo = [self fetchInfo]) == nil)
959     return nil;
960   
961   if ((from = [self sender]) != nil)
962     from = [NSArray arrayWithObjects:&from count:1];
963
964   if ((replyTo = [lInfo objectForKey:@"replyTo"]) != nil) {
965     if (![replyTo isKindOfClass:[NSArray class]])
966       replyTo = [NSArray arrayWithObjects:&replyTo count:1];
967   }
968   
969   self->envelope = 
970     [[NGImap4Envelope alloc] initWithMessageID:[self nameInContainer]
971                              subject:[lInfo objectForKey:@"subject"]
972                              from:from replyTo:replyTo
973                              to:[lInfo objectForKey:@"to"]
974                              cc:[lInfo objectForKey:@"cc"]
975                              bcc:[lInfo objectForKey:@"bcc"]];
976   return self->envelope;
977 }
978
979 /* debugging */
980
981 - (BOOL)isDebuggingEnabled {
982   return debugOn;
983 }
984
985 @end /* SOGoDraftObject */