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