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