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