]> err.no Git - scalable-opengroupware.org/blob - SoObjects/Mailer/SOGoDraftObject.m
git-svn-id: http://svn.opengroupware.org/SOGo/inverse/trunk@1155 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/NSURL.h>
27 #import <Foundation/NSUserDefaults.h>
28 #import <Foundation/NSValue.h>
29
30 #import <NGObjWeb/NSException+HTTP.h>
31 #import <NGObjWeb/SoObject+SoDAV.h>
32 #import <NGObjWeb/WOContext+SoObjects.h>
33 #import <NGObjWeb/WORequest+So.h>
34 #import <NGObjWeb/WOResponse.h>
35 #import <NGExtensions/NGBase64Coding.h>
36 #import <NGExtensions/NSFileManager+Extensions.h>
37 #import <NGExtensions/NGHashMap.h>
38 #import <NGExtensions/NSNull+misc.h>
39 #import <NGExtensions/NSObject+Logs.h>
40 #import <NGExtensions/NGQuotedPrintableCoding.h>
41 #import <NGExtensions/NSString+misc.h>
42 #import <NGImap4/NGImap4Connection.h>
43 #import <NGImap4/NGImap4Client.h>
44 #import <NGImap4/NGImap4Envelope.h>
45 #import <NGImap4/NGImap4EnvelopeAddress.h>
46 #import <NGMail/NGMimeMessage.h>
47 #import <NGMail/NGMimeMessageGenerator.h>
48 #import <NGMime/NGMimeBodyPart.h>
49 #import <NGMime/NGMimeFileData.h>
50 #import <NGMime/NGMimeMultipartBody.h>
51 #import <NGMime/NGMimeType.h>
52 #import <NGMime/NGMimeHeaderFieldGenerator.h>
53
54 #import <SoObjects/SOGo/NSArray+Utilities.h>
55 #import <SoObjects/SOGo/NSCalendarDate+SOGo.h>
56 #import <SoObjects/SOGo/NSString+Utilities.h>
57 #import <SoObjects/SOGo/SOGoMailer.h>
58 #import <SoObjects/SOGo/SOGoUser.h>
59 #import "SOGoMailAccount.h"
60 #import "SOGoMailFolder.h"
61 #import "SOGoMailObject.h"
62 #import "SOGoMailObject+Draft.h"
63
64 #import "SOGoDraftObject.h"
65
66 static NSString *contentTypeValue = @"text/plain; charset=utf-8";
67 static NSString *headerKeys[] = {@"subject", @"to", @"cc", @"bcc", 
68                                  @"from", @"replyTo", nil};
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
79 + (void) initialize
80 {
81   NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
82   
83   /* Note: be aware of the charset issues before enabling this! */
84   showTextAttachmentsInline = [ud boolForKey: @"SOGoShowTextAttachmentsInline"];
85   
86   if ((draftDeleteDisabled = [ud boolForKey: @"SOGoNoDraftDeleteAfterSend"]))
87     NSLog(@"WARNING: draft delete is disabled! (SOGoNoDraftDeleteAfterSend)");
88   
89   TextPlainType  = [[NGMimeType mimeType: @"text" subType: @"plain"]  copy];
90   MultiMixedType = [[NGMimeType mimeType: @"multipart" subType: @"mixed"]  copy];
91 }
92
93 - (id) init
94 {
95   if ((self = [super init]))
96     {
97       IMAP4ID = -1;
98       headers = [NSMutableDictionary new];
99       text = @"";
100       sourceURL = nil;
101       sourceFlag = nil;
102     }
103
104   return self;
105 }
106
107 - (void) dealloc
108 {
109   [headers release];
110   [text release];
111   [envelope release];
112   [path release];
113   [sourceURL release];
114   [sourceFlag release];
115   [super dealloc];
116 }
117
118 /* draft folder functionality */
119
120 - (NSString *) userSpoolFolderPath
121 {
122   return [[self container] userSpoolFolderPath];
123 }
124
125 /* draft object functionality */
126
127 - (NSString *) draftFolderPath
128 {
129   if (!path)
130     {
131       path = [[self userSpoolFolderPath] stringByAppendingPathComponent:
132                                            nameInContainer];
133       [path retain];
134     }
135
136   return path;
137 }
138
139 - (BOOL) _ensureDraftFolderPath
140 {
141   NSFileManager *fm;
142
143   fm = [NSFileManager defaultManager];
144   
145   return ([fm createDirectoriesAtPath: [container userSpoolFolderPath]
146               attributes: nil]
147           && [fm createDirectoriesAtPath: [self draftFolderPath]
148                  attributes:nil]);
149 }
150
151 - (NSString *) infoPath
152 {
153   return [[self draftFolderPath]
154            stringByAppendingPathComponent: @".info.plist"];
155 }
156
157 /* contents */
158
159 - (void) setHeaders: (NSDictionary *) newHeaders
160 {
161   id headerValue;
162   unsigned int count;
163
164   for (count = 0; count < 6; count++)
165     {
166       headerValue = [newHeaders objectForKey: headerKeys[count]];
167       if (headerValue)
168         [headers setObject: headerValue
169                  forKey: headerKeys[count]];
170       else
171         [headers removeObjectForKey: headerKeys[count]];
172     }
173 }
174
175 - (NSDictionary *) headers
176 {
177   return headers;
178 }
179
180 - (void) setText: (NSString *) newText
181 {
182   ASSIGN (text, newText);
183 }
184
185 - (NSString *) text
186 {
187   return text;
188 }
189
190 - (void) setSourceURL: (NSString *) newSourceURL
191 {
192   ASSIGN (sourceURL, newSourceURL);
193 }
194
195 - (void) setSourceFlag: (NSString *) newSourceFlag
196 {
197   ASSIGN (sourceFlag, newSourceFlag);
198 }
199
200 - (NSException *) storeInfo
201 {
202   NSMutableDictionary *infos;
203   NSException *error;
204
205   if ([self _ensureDraftFolderPath])
206     {
207       infos = [NSMutableDictionary new];
208       [infos setObject: headers forKey: @"headers"];
209       if (text)
210         [infos setObject: text forKey: @"text"];
211       if (IMAP4ID > -1)
212         [infos setObject: [NSNumber numberWithInt: IMAP4ID]
213                forKey: @"IMAP4ID"];
214       if (sourceURL && sourceFlag)
215         {
216           [infos setObject: sourceURL forKey: @"sourceURL"];
217           [infos setObject: sourceFlag forKey: @"sourceFlag"];
218         }
219
220       if ([infos writeToFile: [self infoPath] atomically:YES])
221         error = nil;
222       else
223         {
224           [self errorWithFormat: @"could not write info: '%@'",
225                 [self infoPath]];
226           error = [NSException exceptionWithHTTPStatus:500 /* server error */
227                                reason: @"could not write draft info!"];
228         }
229
230       [infos release];
231     }
232   else
233     {
234       [self errorWithFormat: @"could not create folder for draft: '%@'",
235             [self draftFolderPath]];
236       error = [NSException exceptionWithHTTPStatus:500 /* server error */
237                            reason: @"could not create folder for draft!"];
238     }
239
240   return error;
241 }
242
243 - (void) _loadInfosFromDictionary: (NSDictionary *) infoDict
244 {
245   id value;
246
247   value = [infoDict objectForKey: @"headers"];
248   if (value)
249     [self setHeaders: value];
250
251   value = [infoDict objectForKey: @"text"];
252   if ([value length] > 0)
253     [self setText: value];
254
255   value = [infoDict objectForKey: @"IMAP4ID"];
256   if (value)
257     [self setIMAP4ID: [value intValue]];
258
259   value = [infoDict objectForKey: @"sourceURL"];
260   if (value)
261     [self setSourceURL: value];
262   value = [infoDict objectForKey: @"sourceFlag"];
263   if (value)
264     [self setSourceFlag: value];
265 }
266
267 - (NSString *) relativeImap4Name
268 {
269   return [NSString stringWithFormat: @"%d", IMAP4ID];
270 }
271
272 - (void) fetchInfo
273 {
274   NSString *p;
275   NSDictionary *infos;
276   NSFileManager *fm;
277
278   p = [self infoPath];
279
280   fm = [NSFileManager defaultManager];
281   if ([fm fileExistsAtPath: p])
282     {
283       infos = [NSDictionary dictionaryWithContentsOfFile: p];
284       if (infos)
285         [self _loadInfosFromDictionary: infos];
286 //       else
287 //      [self errorWithFormat: @"draft info dictionary broken at path: %@", p];
288     }
289   else
290     [self debugWithFormat: @"Note: info object does not yet exist: %@", p];
291 }
292
293 - (void) setIMAP4ID: (int) newIMAP4ID
294 {
295   IMAP4ID = newIMAP4ID;
296 }
297
298 - (int) IMAP4ID
299 {
300   return IMAP4ID;
301 }
302
303 - (int) _IMAP4IDFromAppendResult: (NSDictionary *) result
304 {
305   NSDictionary *results;
306   NSString *flag, *newIdString;
307
308   results = [[result objectForKey: @"RawResponse"]
309               objectForKey: @"ResponseResult"];
310   flag = [results objectForKey: @"flag"];
311   newIdString = [[flag componentsSeparatedByString: @" "] objectAtIndex: 2];
312
313   return [newIdString intValue];
314 }
315
316 - (NSException *) save
317 {
318   NGImap4Client *client;
319   NSException *error;
320   NSData *message;
321   NSString *folder;
322   id result;
323
324   error = nil;
325   message = [self mimeMessageAsData];
326
327   client = [[self imap4Connection] client];
328   folder = [imap4 imap4FolderNameForURL: [container imap4URL]];
329   result
330     = [client append: message toFolder: folder
331               withFlags: [NSArray arrayWithObjects: @"seen", @"draft", nil]];
332   if ([[result objectForKey: @"result"] boolValue])
333     {
334       if (IMAP4ID > -1)
335         error = [imap4 markURLDeleted: [self imap4URL]];
336       IMAP4ID = [self _IMAP4IDFromAppendResult: result];
337       [self storeInfo];
338     }
339   else
340     error = [NSException exceptionWithHTTPStatus:500 /* Server Error */
341                          reason: @"Failed to store message"];
342
343   return error;
344 }
345
346 - (void) _addEMailsOfAddresses: (NSArray *) _addrs
347                        toArray: (NSMutableArray *) _ma
348 {
349   unsigned i, count;
350
351   for (i = 0, count = [_addrs count]; i < count; i++)
352     [_ma addObject:
353            [(NGImap4EnvelopeAddress *) [_addrs objectAtIndex: i] email]];
354 }
355
356 - (void) _fillInReplyAddresses: (NSMutableDictionary *) _info
357                     replyToAll: (BOOL) _replyToAll
358                       envelope: (NGImap4Envelope *) _envelope
359 {
360   /*
361     The rules as implemented by Thunderbird:
362     - if there is a 'reply-to' header, only include that (as TO)
363     - if we reply to all, all non-from addresses are added as CC
364     - the from is always the lone TO (except for reply-to)
365     
366     Note: we cannot check reply-to, because Cyrus even sets a reply-to in the
367           envelope if none is contained in the message itself! (bug or
368           feature?)
369     
370     TODO: what about sender (RFC 822 3.6.2)
371   */
372   NSMutableArray *to;
373   NSArray *addrs;
374   
375   to = [NSMutableArray arrayWithCapacity:2];
376
377   /* first check for "reply-to" */
378   
379   addrs = [_envelope replyTo];
380   if ([addrs count] == 0)
381     /* no "reply-to", try "from" */
382     addrs = [_envelope from];
383
384   [self _addEMailsOfAddresses: addrs toArray: to];
385   [_info setObject: to forKey: @"to"];
386
387   /* CC processing if we reply-to-all: add all 'to' and 'cc'  */
388   
389   if (_replyToAll)
390     {
391       to = [NSMutableArray arrayWithCapacity:8];
392
393       [self _addEMailsOfAddresses: [_envelope to] toArray: to];
394       [self _addEMailsOfAddresses: [_envelope cc] toArray: to];
395     
396       [_info setObject: to forKey: @"cc"];
397     }
398 }
399
400 - (void) _fetchAttachments: (NSArray *) parts
401                   fromMail: (SOGoMailObject *) sourceMail
402 {
403   unsigned int count, max;
404   NSDictionary *currentPart, *attachment, *body;
405   NSArray *paths, *result;
406
407   max = [parts count];
408   if (max > 0)
409     {
410       paths = [parts keysWithFormat: @"BODY[%{path}]"];
411       result = [[sourceMail fetchParts: paths] objectForKey: @"fetch"];
412       for (count = 0; count < max; count++)
413         {
414           currentPart = [parts objectAtIndex: count];
415           body = [[result objectAtIndex: count] objectForKey: @"body"];
416           attachment = [NSDictionary dictionaryWithObjectsAndKeys:
417                                        [currentPart objectForKey: @"filename"],
418                                      @"filename",
419                                      [currentPart objectForKey: @"mimetype"],
420                                      @"mime-type",
421                                      nil];
422           [self saveAttachment: [body objectForKey: @"data"]
423                 withMetadata: attachment];
424         }
425     }
426 }
427
428 - (void) fetchMailForEditing: (SOGoMailObject *) sourceMail
429 {
430   NSString *subject;
431   NSMutableDictionary *info;
432   NSMutableArray *addresses;
433   NGImap4Envelope *sourceEnvelope;
434
435   [sourceMail fetchCoreInfos];
436
437   [self _fetchAttachments: [sourceMail fetchFileAttachmentKeys]
438         fromMail: sourceMail];
439   info = [NSMutableDictionary dictionaryWithCapacity: 16];
440   subject = [sourceMail subject];
441   if ([subject length] > 0)
442     [info setObject: subject forKey: @"subject"];
443
444   sourceEnvelope = [sourceMail envelope];
445   addresses = [NSMutableArray array];
446   [self _addEMailsOfAddresses: [sourceEnvelope to] toArray: addresses];
447   [info setObject: addresses forKey: @"to"];
448   addresses = [NSMutableArray array];
449   [self _addEMailsOfAddresses: [sourceEnvelope cc] toArray: addresses];
450   if ([addresses count] > 0)
451     [info setObject: addresses forKey: @"cc"];
452   addresses = [NSMutableArray array];
453   [self _addEMailsOfAddresses: [sourceEnvelope bcc] toArray: addresses];
454   if ([addresses count] > 0)
455     [info setObject: addresses forKey: @"bcc"];
456   addresses = [NSMutableArray array];
457   [self _addEMailsOfAddresses: [sourceEnvelope replyTo] toArray: addresses];
458   if ([addresses count] > 0)
459     [info setObject: addresses forKey: @"replyTo"];
460   [self setHeaders: info];
461
462   [self setText: [sourceMail contentForEditing]];
463   [self setSourceURL: [sourceMail imap4URLString]];
464   IMAP4ID = [[sourceMail nameInContainer] intValue];
465
466   [self storeInfo];
467 }
468
469 - (void) fetchMailForReplying: (SOGoMailObject *) sourceMail
470                         toAll: (BOOL) toAll
471 {
472   NSString *contentForReply;
473   NSMutableDictionary *info;
474
475   [sourceMail fetchCoreInfos];
476
477   info = [NSMutableDictionary dictionaryWithCapacity: 16];
478   [info setObject: [sourceMail subjectForReply] forKey: @"subject"];
479   [self _fillInReplyAddresses: info replyToAll: toAll
480         envelope: [sourceMail envelope]];
481   contentForReply = [sourceMail contentForReply];
482   [self setText: contentForReply];
483   [self setHeaders: info];
484   [self setSourceURL: [sourceMail imap4URLString]];
485   [self setSourceFlag: @"Answered"];
486   [self storeInfo];
487 }
488
489 - (void) fetchMailForForwarding: (SOGoMailObject *) sourceMail
490 {
491   NSDictionary *info, *attachment;
492   SOGoUser *currentUser;
493
494   [sourceMail fetchCoreInfos];
495
496   info = [NSDictionary dictionaryWithObject: [sourceMail subjectForForward]
497                        forKey: @"subject"];
498   [self setHeaders: info];
499   [self setSourceURL: [sourceMail imap4URLString]];
500   [self setSourceFlag: @"$Forwarded"];
501
502   /* attach message */
503   currentUser = [context activeUser];
504   if ([[currentUser messageForwarding] isEqualToString: @"inline"])
505     [self setText: [sourceMail contentForInlineForward]];
506   else
507     {
508   // TODO: use subject for filename?
509 //   error = [newDraft saveAttachment:content withName:@"forward.mail"];
510       attachment = [NSDictionary dictionaryWithObjectsAndKeys:
511                                    [sourceMail filenameForForward], @"filename",
512                                  @"message/rfc822", @"mime-type",
513                                  nil];
514       [self saveAttachment: [sourceMail content]
515             withMetadata: attachment];
516     }
517   [self storeInfo];
518 }
519
520 /* accessors */
521
522 - (NSString *) sender
523 {
524   id tmp;
525   
526   if ((tmp = [headers objectForKey: @"from"]) == nil)
527     return nil;
528   if ([tmp isKindOfClass:[NSArray class]])
529     return [tmp count] > 0 ? [tmp objectAtIndex: 0] : nil;
530
531   return tmp;
532 }
533
534 /* attachments */
535
536 - (NSArray *) fetchAttachmentNames
537 {
538   NSMutableArray *ma;
539   NSFileManager *fm;
540   NSArray *files;
541   unsigned count, max;
542   NSString *filename;
543
544   fm = [NSFileManager defaultManager];
545   files = [fm directoryContentsAtPath: [self draftFolderPath]];
546
547   max = [files count];
548   ma = [NSMutableArray arrayWithCapacity: max];
549   for (count = 0; count < max; count++)
550     {
551       filename = [files objectAtIndex: count];
552       if (![filename hasPrefix: @"."])
553         [ma addObject: filename];
554     }
555
556   return ma;
557 }
558
559 - (BOOL) isValidAttachmentName: (NSString *) _name
560 {
561   static NSString *sescape[] = { @"/", @"..", @"~", @"\"", @"'", @" ", nil };
562   unsigned i;
563   NSRange  r;
564
565   if (![_name isNotNull])     return NO;
566   if ([_name length] == 0)    return NO;
567   if ([_name hasPrefix: @"."]) return NO;
568   
569   for (i = 0; sescape[i] != nil; i++) {
570     r = [_name rangeOfString:sescape[i]];
571     if (r.length > 0) return NO;
572   }
573   return YES;
574 }
575
576 - (NSString *) pathToAttachmentWithName: (NSString *) _name
577 {
578   if ([_name length] == 0)
579     return nil;
580   
581   return [[self draftFolderPath] stringByAppendingPathComponent:_name];
582 }
583
584 - (NSException *) invalidAttachmentNameError: (NSString *) _name
585 {
586   return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
587                       reason: @"Invalid attachment name!"];
588 }
589
590 - (NSException *) saveAttachment: (NSData *) _attach
591                     withMetadata: (NSDictionary *) metadata
592 {
593   NSString *p, *name, *mimeType;
594   
595   if (![_attach isNotNull]) {
596     return [NSException exceptionWithHTTPStatus:400 /* Bad Request */
597                         reason: @"Missing attachment content!"];
598   }
599   
600   if (![self _ensureDraftFolderPath]) {
601     return [NSException exceptionWithHTTPStatus:500 /* Server Error */
602                         reason: @"Could not create folder for draft!"];
603   }
604   name = [metadata objectForKey: @"filename"];
605   if (![self isValidAttachmentName: name])
606     return [self invalidAttachmentNameError: name];
607   
608   p = [self pathToAttachmentWithName: name];
609   if (![_attach writeToFile: p atomically: YES])
610     {
611       return [NSException exceptionWithHTTPStatus:500 /* Server Error */
612                           reason: @"Could not write attachment to draft!"];
613     }
614
615   mimeType = [metadata objectForKey: @"mime-type"];
616   if ([mimeType length] > 0)
617     {
618       p = [self pathToAttachmentWithName:
619                   [NSString stringWithFormat: @".%@.mime", name]];
620       if (![[mimeType dataUsingEncoding: NSUTF8StringEncoding]
621              writeToFile: p atomically: YES])
622         {
623           return [NSException exceptionWithHTTPStatus:500 /* Server Error */
624                               reason: @"Could not write attachment to draft!"];
625         }
626     }
627   
628   return nil; /* everything OK */
629 }
630
631 - (NSException *) deleteAttachmentWithName: (NSString *) _name
632 {
633   NSFileManager *fm;
634   NSString *p;
635   NSException *error;
636
637   error = nil;
638
639   if ([self isValidAttachmentName:_name]) 
640     {
641       fm = [NSFileManager defaultManager];
642       p = [self pathToAttachmentWithName:_name];
643       if ([fm fileExistsAtPath: p])
644         if (![fm removeFileAtPath: p handler: nil])
645           error
646             = [NSException exceptionWithHTTPStatus: 500 /* Server Error */
647                            reason: @"Could not delete attachment from draft!"];
648     }
649   else
650     error = [self invalidAttachmentNameError:_name];
651
652   return error;  
653 }
654
655 /* NGMime representations */
656
657 - (NGMimeBodyPart *) bodyPartForText
658 {
659   /*
660     This add the text typed by the user (the primary plain/text part).
661   */
662   NGMutableHashMap *map;
663   NGMimeBodyPart   *bodyPart;
664   
665   /* prepare header of body part */
666
667   map = [[[NGMutableHashMap alloc] initWithCapacity: 1] autorelease];
668
669   // TODO: set charset in header!
670   [map setObject: @"text/plain" forKey: @"content-type"];
671   if (text)
672     [map setObject: contentTypeValue forKey: @"content-type"];
673
674 //   if ((body = text) != nil) {
675 //     if ([body isKindOfClass: [NSString class]]) {
676 //       [map setObject: contentTypeValue
677 //         forKey: @"content-type"];
678 // //       body = [body dataUsingEncoding:NSUTF8StringEncoding];
679 //     }
680 //   }
681   
682   /* prepare body content */
683   
684   bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease];
685   [bodyPart setBody: text];
686
687   return bodyPart;
688 }
689
690 - (NGMimeMessage *) mimeMessageForContentWithHeaderMap: (NGMutableHashMap *) map
691 {
692   NGMimeMessage *message;  
693 //   BOOL     addSuffix;
694   id       body;
695
696   [map setObject: @"text/plain" forKey: @"content-type"];
697   body = text;
698   if (body)
699     {
700 //       if ([body isKindOfClass:[NSString class]])
701         /* Note: just 'utf8' is displayed wrong in Mail.app */
702         [map setObject: contentTypeValue
703              forKey: @"content-type"];
704 //       body = [body dataUsingEncoding:NSUTF8StringEncoding];
705 //       else if ([body isKindOfClass:[NSData class]] && addSuffix) {
706 //      body = [[body mutableCopy] autorelease];
707 //       }
708 //       else if (addSuffix) {
709 //      [self warnWithFormat: @"Note: cannot add Internet marker to body: %@",
710 //            NSStringFromClass([body class])];
711 //       }
712
713         message = [[[NGMimeMessage alloc] initWithHeader:map] autorelease];
714         [message setBody: body];
715     }
716   else
717     message = nil;
718
719
720   return message;
721 }
722
723 - (NSString *) mimeTypeForExtension: (NSString *) _ext
724 {
725   // TODO: make configurable
726   // TODO: use /etc/mime-types
727   if ([_ext isEqualToString: @"txt"])  return @"text/plain";
728   if ([_ext isEqualToString: @"html"]) return @"text/html";
729   if ([_ext isEqualToString: @"htm"])  return @"text/html";
730   if ([_ext isEqualToString: @"gif"])  return @"image/gif";
731   if ([_ext isEqualToString: @"jpg"])  return @"image/jpeg";
732   if ([_ext isEqualToString: @"jpeg"]) return @"image/jpeg";
733   if ([_ext isEqualToString: @"mail"]) return @"message/rfc822";
734   return @"application/octet-stream";
735 }
736
737 - (NSString *) contentTypeForAttachmentWithName: (NSString *) _name
738 {
739   NSString *s, *p;
740   NSData *mimeData;
741   
742   p = [self pathToAttachmentWithName:
743               [NSString stringWithFormat: @".%@.mime", _name]];
744   mimeData = [NSData dataWithContentsOfFile: p];
745   if (mimeData)
746     {
747       s = [[NSString alloc] initWithData: mimeData
748                             encoding: NSUTF8StringEncoding];
749       [s autorelease];
750     }
751   else
752     {
753       s = [self mimeTypeForExtension:[_name pathExtension]];
754       if ([_name length] > 0)
755         s = [s stringByAppendingFormat: @"; name=\"%@\"", _name];
756     }
757
758   return s;
759 }
760
761 - (NSString *) contentDispositionForAttachmentWithName: (NSString *) _name
762 {
763   NSString *type;
764   NSString *cdtype;
765   NSString *cd;
766   
767   type = [self contentTypeForAttachmentWithName:_name];
768   
769   if ([type hasPrefix: @"text/"])
770     cdtype = showTextAttachmentsInline ? @"inline" : @"attachment";
771   else if ([type hasPrefix: @"image/"] || [type hasPrefix: @"message"])
772     cdtype = @"inline";
773   else
774     cdtype = @"attachment";
775   
776   cd = [cdtype stringByAppendingString: @"; filename=\""];
777   cd = [cd stringByAppendingString: _name];
778   cd = [cd stringByAppendingString: @"\""];
779
780   // TODO: add size parameter (useful addition, RFC 2183)
781   return cd;
782 }
783
784 - (NGMimeBodyPart *) bodyPartForAttachmentWithName: (NSString *) _name
785 {
786   NSFileManager    *fm;
787   NGMutableHashMap *map;
788   NGMimeBodyPart   *bodyPart;
789   NSString         *s;
790   NSData           *content;
791   BOOL             attachAsString, is7bit;
792   NSString         *p;
793   id body;
794
795   if (_name == nil) return nil;
796
797   /* check attachment */
798   
799   fm = [NSFileManager defaultManager];
800   p  = [self pathToAttachmentWithName:_name];
801   if (![fm isReadableFileAtPath:p]) {
802     [self errorWithFormat: @"did not find attachment: '%@'", _name];
803     return nil;
804   }
805   attachAsString = NO;
806   is7bit         = NO;
807   
808   /* prepare header of body part */
809
810   map = [[[NGMutableHashMap alloc] initWithCapacity:4] autorelease];
811
812   if ((s = [self contentTypeForAttachmentWithName:_name]) != nil) {
813     [map setObject:s forKey: @"content-type"];
814     if ([s hasPrefix: @"text/"])
815       attachAsString = YES;
816     else if ([s hasPrefix: @"message/rfc822"])
817       is7bit = YES;
818   }
819   if ((s = [self contentDispositionForAttachmentWithName:_name]))
820     [map setObject:s forKey: @"content-disposition"];
821   
822   /* prepare body content */
823   
824   if (attachAsString) { // TODO: is this really necessary?
825     NSString *s;
826     
827     content = [[NSData alloc] initWithContentsOfMappedFile:p];
828     
829     s = [[NSString alloc] initWithData:content
830                           encoding:[NSString defaultCStringEncoding]];
831     if (s != nil) {
832       body = s;
833       [content release]; content = nil;
834     }
835     else {
836       [self warnWithFormat:
837               @"could not get text attachment as string: '%@'", _name];
838       body = content;
839       content = nil;
840     }
841   }
842   else if (is7bit) {
843     /* 
844        Note: Apparently NGMimeFileData objects are not processed by the MIME
845              generator!
846     */
847     body = [[NGMimeFileData alloc] initWithPath:p removeFile:NO];
848     [map setObject: @"7bit" forKey: @"content-transfer-encoding"];
849     [map setObject:[NSNumber numberWithInt:[body length]] 
850          forKey: @"content-length"];
851   }
852   else {
853     /* 
854        Note: in OGo this is done in LSWImapMailEditor.m:2477. Apparently
855              NGMimeFileData objects are not processed by the MIME generator!
856     */
857     NSData *encoded;
858     
859     content = [[NSData alloc] initWithContentsOfMappedFile:p];
860     encoded = [content dataByEncodingBase64];
861     [content release]; content = nil;
862     
863     [map setObject: @"base64" forKey: @"content-transfer-encoding"];
864     [map setObject:[NSNumber numberWithInt:[encoded length]] 
865          forKey: @"content-length"];
866     
867     /* Note: the -init method will create a temporary file! */
868     body = [[NGMimeFileData alloc] initWithBytes:[encoded bytes]
869                                    length:[encoded length]];
870   }
871   
872   bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease];
873   [bodyPart setBody:body];
874   
875   [body release]; body = nil;
876   return bodyPart;
877 }
878
879 - (NSArray *) bodyPartsForAllAttachments
880 {
881   /* returns nil on error */
882   NSArray  *names;
883   unsigned i, count;
884   NGMimeBodyPart *bodyPart;
885   NSMutableArray *bodyParts;
886
887   names = [self fetchAttachmentNames];
888   count = [names count];
889   bodyParts = [NSMutableArray arrayWithCapacity: count];
890
891   for (i = 0; i < count; i++)
892     {
893       bodyPart = [self bodyPartForAttachmentWithName: [names objectAtIndex: i]];
894       [bodyParts addObject: bodyPart];
895     }
896
897   return bodyParts;
898 }
899
900 - (NGMimeMessage *) mimeMultiPartMessageWithHeaderMap: (NGMutableHashMap *) map
901                                          andBodyParts: (NSArray *) _bodyParts
902 {
903   NGMimeMessage       *message;  
904   NGMimeMultipartBody *mBody;
905   NGMimeBodyPart      *part;
906   NSEnumerator        *e;
907   
908   [map addObject: MultiMixedType forKey: @"content-type"];
909
910   message = [[NGMimeMessage alloc] initWithHeader: map];
911   [message autorelease];
912   mBody = [[NGMimeMultipartBody alloc] initWithPart: message];
913
914   part = [self bodyPartForText];
915   [mBody addBodyPart: part];
916
917   e = [_bodyParts objectEnumerator];
918   part = [e nextObject];
919   while (part)
920     {
921       [mBody addBodyPart: part];
922       part = [e nextObject];
923     }
924
925   [message setBody: mBody];
926   [mBody release];
927
928   return message;
929 }
930
931 - (void) _addHeaders: (NSDictionary *) _h
932          toHeaderMap: (NGMutableHashMap *) _map
933 {
934   NSEnumerator *names;
935   NSString *name;
936
937   if ([_h count] == 0)
938     return;
939     
940   names = [_h keyEnumerator];
941   while ((name = [names nextObject]) != nil) {
942     id value;
943       
944     value = [_h objectForKey:name];
945     [_map addObject:value forKey:name];
946   }
947 }
948
949 - (BOOL) isEmptyValue: (id) _value
950 {
951   if (![_value isNotNull])
952     return YES;
953   
954   if ([_value isKindOfClass: [NSArray class]])
955     return [_value count] == 0 ? YES : NO;
956   
957   if ([_value isKindOfClass: [NSString class]])
958     return [_value length] == 0 ? YES : NO;
959
960   return NO;
961 }
962
963 - (NGMutableHashMap *) mimeHeaderMapWithHeaders: (NSDictionary *) _headers
964 {
965   NGMutableHashMap *map;
966   NSArray      *emails;
967   NSString     *s, *dateString;
968   id           from, replyTo;
969   
970   map = [[[NGMutableHashMap alloc] initWithCapacity:16] autorelease];
971   
972   /* add recipients */
973   
974   if ((emails = [headers objectForKey: @"to"]) != nil)
975     [map setObjects: emails forKey: @"to"];
976   if ((emails = [headers objectForKey: @"cc"]) != nil)
977     [map setObjects:emails forKey: @"cc"];
978   if ((emails = [headers objectForKey: @"bcc"]) != nil)
979     [map setObjects:emails forKey: @"bcc"];
980
981   /* add senders */
982   
983   from = [headers objectForKey: @"from"];
984   replyTo = [headers objectForKey: @"replyTo"];
985   
986   if (![self isEmptyValue:from]) {
987     if ([from isKindOfClass:[NSArray class]])
988       [map setObjects: from forKey: @"from"];
989     else
990       [map setObject: from forKey: @"from"];
991   }
992   
993   if (![self isEmptyValue: replyTo]) {
994     if ([from isKindOfClass:[NSArray class]])
995       [map setObjects:from forKey: @"reply-to"];
996     else
997       [map setObject:from forKey: @"reply-to"];
998   }
999   else if (![self isEmptyValue:from])
1000     [map setObjects:[map objectsForKey: @"from"] forKey: @"reply-to"];
1001   
1002   /* add subject */
1003   
1004   if ([(s = [headers objectForKey: @"subject"]) length] > 0)
1005     [map setObject: [s asQPSubjectString: @"utf-8"]
1006          forKey: @"subject"];
1007 //     [map setObject: [s asQPSubjectString: @"utf-8"] forKey: @"subject"];
1008   
1009   /* add standard headers */
1010
1011   dateString = [[NSCalendarDate date] rfc822DateString];
1012   [map addObject: dateString forKey: @"date"];
1013   [map addObject: @"1.0" forKey: @"MIME-Version"];
1014   [map addObject: userAgent forKey: @"X-Mailer"];
1015
1016   /* add custom headers */
1017   
1018 //   [self _addHeaders: [lInfo objectForKey: @"headers"] toHeaderMap:map];
1019   [self _addHeaders: _headers toHeaderMap: map];
1020   
1021   return map;
1022 }
1023
1024 - (NGMimeMessage *) mimeMessageWithHeaders: (NSDictionary *) _headers
1025 {
1026   NGMutableHashMap  *map;
1027   NSArray           *bodyParts;
1028   NGMimeMessage     *message;
1029
1030   message = nil;
1031
1032   map = [self mimeHeaderMapWithHeaders: _headers];
1033   if (map)
1034     {
1035       [self debugWithFormat: @"MIME Envelope: %@", map];
1036   
1037       bodyParts = [self bodyPartsForAllAttachments];
1038       if (bodyParts)
1039         {
1040           [self debugWithFormat: @"attachments: %@", bodyParts];
1041   
1042           if ([bodyParts count] == 0)
1043             /* no attachments */
1044             message = [self mimeMessageForContentWithHeaderMap: map];
1045           else
1046             /* attachments, create multipart/mixed */
1047             message = [self mimeMultiPartMessageWithHeaderMap: map 
1048                             andBodyParts: bodyParts];
1049           [self debugWithFormat: @"message: %@", message];
1050         }
1051       else
1052         [self errorWithFormat:
1053                 @"could not create body parts for attachments!"];
1054     }
1055
1056   return message;
1057 }
1058
1059 - (NGMimeMessage *) mimeMessage
1060 {
1061   return [self mimeMessageWithHeaders: nil];
1062 }
1063
1064 - (NSData *) mimeMessageAsData
1065 {
1066   NGMimeMessageGenerator *generator;
1067   NSData *message;
1068
1069   generator = [NGMimeMessageGenerator new];
1070   message = [generator generateMimeFromPart: [self mimeMessage]];
1071   [generator release];
1072
1073   return message;
1074 }
1075
1076 - (NSArray *) allRecipients
1077 {
1078   NSMutableArray *allRecipients;
1079   NSArray *recipients;
1080   NSString *fieldNames[] = {@"to", @"cc", @"bcc"};
1081   unsigned int count;
1082
1083   allRecipients = [NSMutableArray arrayWithCapacity: 16];
1084
1085   for (count = 0; count < 3; count++)
1086     {
1087       recipients = [headers objectForKey: fieldNames[count]];
1088       if ([recipients count] > 0)
1089         [allRecipients addObjectsFromArray: recipients];
1090     }
1091
1092   return allRecipients;
1093 }
1094
1095 - (NSException *) sendMail
1096 {
1097   NSException *error;
1098   SOGoMailFolder *sentFolder;
1099   NSData *message;
1100   NSURL *sourceIMAP4URL;
1101   
1102   /* send mail */
1103   sentFolder = [[self mailAccountFolder] sentFolderInContext: context];
1104   if ([sentFolder isKindOfClass: [NSException class]])
1105     error = (NSException *) sentFolder;
1106   else
1107     {
1108       message = [self mimeMessageAsData];
1109       error = [[SOGoMailer sharedMailer] sendMailData: message
1110                                          toRecipients: [self allRecipients]
1111                                          sender: [self sender]];
1112       if (!error)
1113         {
1114           error = [sentFolder postData: message flags: @"seen"];
1115           if (!error)
1116             {
1117               [self imap4Connection];
1118               if (IMAP4ID > -1)
1119                 [imap4 markURLDeleted: [self imap4URL]];
1120               if (sourceURL && sourceFlag)
1121                 {
1122                   sourceIMAP4URL = [NSURL URLWithString: sourceURL];
1123                   [imap4 addFlags: sourceFlag toURL: sourceIMAP4URL];
1124                 }
1125               if (!draftDeleteDisabled)
1126                 error = [self delete];
1127             }
1128         }
1129     }
1130
1131   return error;
1132 }
1133
1134 - (NSException *) delete
1135 {
1136   NSException *error;
1137
1138   if ([[NSFileManager defaultManager]
1139         removeFileAtPath: [self draftFolderPath]
1140         handler: nil])
1141     error = nil;
1142   else
1143     error = [NSException exceptionWithHTTPStatus: 500 /* server error */
1144                          reason: @"could not delete draft"];
1145
1146   return error;
1147 }
1148
1149 /* operations */
1150
1151 - (NSString *) contentAsString
1152 {
1153   NSString *str;
1154   NSData *message;
1155
1156   message = [self mimeMessageAsData];
1157   if (message)
1158     {
1159       str = [[NSString alloc] initWithData: message
1160                               encoding: NSUTF8StringEncoding];
1161       if (!str)
1162         [self errorWithFormat: @"could not load draft as UTF-8 (data size=%d)",
1163               [message length]];
1164       else
1165         [str autorelease];
1166     }
1167   else
1168     {
1169       [self errorWithFormat: @"message data is empty"];
1170       str = nil;
1171     }
1172
1173   return str;
1174 }
1175
1176 /* debugging */
1177
1178 - (BOOL) isDebuggingEnabled
1179 {
1180   return debugOn;
1181 }
1182
1183 @end /* SOGoDraftObject */