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