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