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