1/*
2  Copyright (C) 2007-2021 Inverse inc.
3
4  This file is part of SOGo.
5
6  SOGo 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  SOGo 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#if defined(HAVE_OPENSSL) || defined(HAVE_GNUTLS)
23#include <openssl/ssl.h>
24#include <openssl/bio.h>
25#include <openssl/err.h>
26#include <openssl/pkcs7.h>
27#include <openssl/x509.h>
28#include <openssl/x509v3.h>
29#endif
30
31#import <Foundation/NSURL.h>
32#import <Foundation/NSValue.h>
33
34#import <NGObjWeb/NSException+HTTP.h>
35#import <NGObjWeb/SoObject+SoDAV.h>
36#import <NGObjWeb/WOContext+SoObjects.h>
37#import <NGObjWeb/WORequest+So.h>
38#import <NGExtensions/NGBase64Coding.h>
39#import <NGExtensions/NSFileManager+Extensions.h>
40#import <NGExtensions/NGHashMap.h>
41#import <NGExtensions/NSNull+misc.h>
42#import <NGExtensions/NSObject+Logs.h>
43#import <NGExtensions/NSString+misc.h>
44#import <NGImap4/NGImap4Connection.h>
45#import <NGImap4/NGImap4Client.h>
46#import <NGImap4/NGImap4Envelope.h>
47#import <NGImap4/NGImap4EnvelopeAddress.h>
48#import <NGMail/NGMailAddress.h>
49#import <NGMail/NGMailAddressParser.h>
50#import <NGMail/NGMimeMessage.h>
51#import <NGMail/NGMimeMessageGenerator.h>
52#import <NGMail/NGMimeMessageParser.h>
53#import <NGMime/NGMimeBodyPart.h>
54#import <NGMime/NGMimeFileData.h>
55#import <NGMime/NGMimeMultipartBody.h>
56#import <NGMime/NGMimeType.h>
57#import <NGMime/NGMimeHeaderFields.h>
58
59#import <SOGo/NSArray+Utilities.h>
60#import <SOGo/NSCalendarDate+SOGo.h>
61#import <SOGo/NSDictionary+Utilities.h>
62#import <SOGo/NSString+Utilities.h>
63#import <SOGo/SOGoBuild.h>
64#import <SOGo/SOGoDomainDefaults.h>
65#import <SOGo/SOGoMailer.h>
66#import <SOGo/SOGoUser.h>
67#import <SOGo/SOGoUserFolder.h>
68#import <SOGo/SOGoUserDefaults.h>
69#import <SOGo/SOGoSystemDefaults.h>
70
71#import <NGCards/NGVCard.h>
72
73#import <Contacts/SOGoContactFolder.h>
74#import <Contacts/SOGoContactFolders.h>
75#import <Contacts/SOGoContactGCSEntry.h>
76
77#import "NSData+Mail.h"
78#import "NSData+SMIME.h"
79#import "NSString+Mail.h"
80#import "SOGoDraftsFolder.h"
81#import "SOGoMailAccount.h"
82#import "SOGoMailObject+Draft.h"
83#import "SOGoSentFolder.h"
84
85#import "SOGoDraftObject.h"
86
87
88static NSString *contentTypeValue = @"text/plain; charset=utf-8";
89static NSString *htmlContentTypeValue = @"text/html; charset=utf-8";
90static NSString *headerKeys[] = {@"subject", @"to", @"cc", @"bcc",
91                                 @"from", @"replyTo", @"message-id",
92                                 nil};
93
94#warning -[NGImap4Connection postData:flags:toFolderURL:] should be enhanced \
95  to return at least the new uid
96@interface NGImap4Connection (SOGoHiddenMethods)
97
98- (NSString *) imap4FolderNameForURL: (NSURL *) url;
99
100@end
101
102//
103//
104//
105@implementation SOGoDraftObject
106
107static NGMimeType  *MultiMixedType = nil;
108static NGMimeType  *MultiAlternativeType = nil;
109static NGMimeType  *MultiRelatedType = nil;
110static NSString    *userAgent      = nil;
111
112+ (void) initialize
113{
114  MultiMixedType = [NGMimeType mimeType: @"multipart" subType: @"mixed"];
115  [MultiMixedType retain];
116
117  MultiAlternativeType = [NGMimeType mimeType: @"multipart" subType: @"alternative"];
118  [MultiAlternativeType retain];
119
120  MultiRelatedType = [NGMimeType mimeType: @"multipart" subType: @"related"];
121  [MultiRelatedType retain];
122
123  userAgent      = [NSString stringWithFormat: @"SOGoMail %@",
124                             SOGoVersion];
125  [userAgent retain];
126}
127
128- (id) init
129{
130  if ((self = [super init]))
131    {
132      sourceIMAP4ID = -1;
133      IMAP4ID = -1;
134      headers = [[NSMutableDictionary alloc] init];
135      certificates = [[NSMutableDictionary alloc] init];
136      text = @"";
137      path = nil;
138      sourceURL = nil;
139      sourceFlag = nil;
140      inReplyTo = nil;
141      isHTML = NO;
142      sign = NO;
143      encrypt = NO;
144    }
145
146  return self;
147}
148
149- (void) dealloc
150{
151  [headers release];
152  [certificates release];
153  [text release];
154  [path release];
155  [sourceURL release];
156  [sourceFlag release];
157  [inReplyTo release];
158  [super dealloc];
159}
160
161/* draft folder functionality */
162
163- (NSString *) userSpoolFolderPath
164{
165  return [[self container] userSpoolFolderPath];
166}
167
168/* draft object functionality */
169
170- (NSString *) draftFolderPath
171{
172  if (!path)
173    {
174      path = [[self userSpoolFolderPath] stringByAppendingPathComponent: nameInContainer];
175      [path retain];
176    }
177
178  return path;
179}
180
181- (BOOL) _ensureDraftFolderPath
182{
183  NSFileManager *fm;
184
185  fm = [NSFileManager defaultManager];
186
187  return ([fm createDirectoriesAtPath: [container userSpoolFolderPath]
188                           attributes: nil]
189	  && [fm createDirectoriesAtPath: [self draftFolderPath]
190                              attributes:nil]);
191}
192
193- (NSString *) infoPath
194{
195  return [[self draftFolderPath]
196	   stringByAppendingPathComponent: @".info.plist"];
197}
198
199/* contents */
200
201- (void) setHeaders: (NSDictionary *) newHeaders
202{
203  id headerValue;
204  unsigned int count;
205  NSString *messageID, *priority, *pureSender, *replyTo, *receipt;
206
207  for (count = 0; count < 8; count++)
208    {
209      headerValue = [newHeaders objectForKey: headerKeys[count]];
210      if (headerValue)
211	[headers setObject: headerValue
212                    forKey: headerKeys[count]];
213      else if ([headers objectForKey: headerKeys[count]])
214	[headers removeObjectForKey: headerKeys[count]];
215    }
216
217  messageID = [headers objectForKey: @"message-id"];
218  if (!messageID)
219    {
220      messageID = [NSString generateMessageID];
221      [headers setObject: messageID forKey: @"message-id"];
222    }
223
224  priority = [newHeaders objectForKey: @"X-Priority"];
225  if (priority)
226    {
227      // newHeaders come from MIME message; convert X-Priority to Web representation
228      [headers setObject: priority  forKey: @"X-Priority"];
229      [headers removeObjectForKey: @"priority"];
230      if ([priority isEqualToString: @"1 (Highest)"])
231        {
232          [headers setObject: @"HIGHEST"  forKey: @"priority"];
233        }
234      else if ([priority isEqualToString: @"2 (High)"])
235        {
236          [headers setObject: @"HIGH"  forKey: @"priority"];
237        }
238      else if ([priority isEqualToString: @"4 (Low)"])
239        {
240          [headers setObject: @"LOW"  forKey: @"priority"];
241        }
242      else if ([priority isEqualToString: @"5 (Lowest)"])
243        {
244          [headers setObject: @"LOWEST"  forKey: @"priority"];
245        }
246    }
247  else
248    {
249      // newHeaders come from Web form; convert priority to MIME header representation
250      priority = [newHeaders objectForKey: @"priority"];
251      if ([priority intValue] == 1)
252        {
253          [headers setObject: @"1 (Highest)"  forKey: @"X-Priority"];
254        }
255      else if ([priority intValue] == 2)
256        {
257          [headers setObject: @"2 (High)"  forKey: @"X-Priority"];
258        }
259      else if ([priority intValue] == 4)
260        {
261          [headers setObject: @"4 (Low)"  forKey: @"X-Priority"];
262        }
263      else if ([priority intValue] == 5)
264        {
265          [headers setObject: @"5 (Lowest)"  forKey: @"X-Priority"];
266        }
267      else
268        {
269          [headers removeObjectForKey: @"X-Priority"];
270        }
271      if (priority)
272        {
273          [headers setObject: priority  forKey: @"priority"];
274        }
275    }
276
277  replyTo = [headers objectForKey: @"replyTo"];
278  if ([replyTo length] > 0)
279    {
280      [headers setObject: replyTo forKey: @"reply-to"];
281    }
282  [headers removeObjectForKey: @"replyTo"];
283
284  receipt = [newHeaders objectForKey: @"Disposition-Notification-To"];
285  if ([receipt length] > 0)
286    {
287      [headers setObject: @"true"  forKey: @"receipt"];
288      [headers setObject: receipt forKey: @"Disposition-Notification-To"];
289    }
290  else
291    {
292      receipt = [newHeaders objectForKey: @"receipt"];
293      if ([receipt boolValue])
294        {
295          [headers setObject: receipt  forKey: @"receipt"];
296          pureSender = [[newHeaders objectForKey: @"from"] pureEMailAddress];
297          if (pureSender)
298            {
299              [headers setObject: pureSender forKey: @"Disposition-Notification-To"];
300            }
301        }
302      else
303        {
304          [headers removeObjectForKey: @"receipt"];
305          [headers removeObjectForKey: @"Disposition-Notification-To"];
306        }
307    }
308}
309
310- (NSDictionary *) headers
311{
312  return headers;
313}
314
315- (void) setText: (NSString *) newText
316{
317  ASSIGN (text, newText);
318}
319
320- (NSString *) text
321{
322  return text;
323}
324
325- (void) setIsHTML: (BOOL) aBool
326{
327  isHTML = aBool;
328}
329
330- (BOOL) isHTML
331{
332  return isHTML;
333}
334
335- (void) setSign: (BOOL) aBool
336{
337  sign = aBool;
338}
339- (BOOL) sign
340{
341  return sign;
342}
343
344- (void) setEncrypt: (BOOL) aBool
345{
346  encrypt = aBool;
347}
348
349- (BOOL) encrypt
350{
351  return encrypt;
352}
353
354- (NSString *) inReplyTo
355{
356  return inReplyTo;
357}
358
359- (void) setInReplyTo: (NSString *) newInReplyTo
360{
361  ASSIGN (inReplyTo, newInReplyTo);
362}
363
364- (void) setSourceURL: (NSString *) newSourceURL
365{
366  ASSIGN (sourceURL, newSourceURL);
367}
368
369- (void) setSourceFlag: (NSString *) newSourceFlag
370{
371  ASSIGN (sourceFlag, newSourceFlag);
372}
373
374- (void) setSourceFolder: (NSString *) newSourceFolder
375{
376  ASSIGN (sourceFolder, newSourceFolder);
377}
378
379- (void) setSourceFolderWithMailObject: (SOGoMailObject *) sourceMail
380{
381  NSMutableArray *paths;
382  id parent;
383
384  parent = [sourceMail container];
385  paths = [NSMutableArray arrayWithCapacity: 1];
386  while (parent && ![parent isKindOfClass: [SOGoMailAccount class]])
387    {
388      [paths insertObject: [parent nameInContainer] atIndex: 0];
389      parent = [parent container];
390    }
391  if (parent)
392    [paths insertObject: [NSString stringWithFormat: @"/%@", [parent nameInContainer]]
393                atIndex: 0];
394
395  [self setSourceFolder: [paths componentsJoinedByString: @"/"]];
396}
397
398//
399//
400//
401- (NSString *) sourceFolder
402{
403  return sourceFolder;
404}
405
406//
407// Store the message definition in a plist file (.info.plist) in the spool directory
408//
409- (NSException *) storeInfo
410{
411  NSMutableDictionary *infos;
412  NSException *error;
413
414  if ([self _ensureDraftFolderPath])
415    {
416      infos = [NSMutableDictionary dictionary];
417      [infos setObject: headers forKey: @"headers"];
418      if (text)
419	[infos setObject: text forKey: @"text"];
420      [infos setObject: [NSNumber numberWithBool: isHTML]
421                forKey: @"isHTML"];
422      if (inReplyTo)
423	[infos setObject: inReplyTo forKey: @"inReplyTo"];
424      if (sourceIMAP4ID > -1)
425	[infos setObject: [NSString stringWithFormat: @"%i", sourceIMAP4ID]
426                  forKey: @"sourceIMAP4ID"];
427      if (IMAP4ID > -1)
428	[infos setObject: [NSString stringWithFormat: @"%i", IMAP4ID]
429                  forKey: @"IMAP4ID"];
430      if (sourceURL && sourceFlag && sourceFolder)
431	{
432	  [infos setObject: sourceURL forKey: @"sourceURL"];
433	  [infos setObject: sourceFlag forKey: @"sourceFlag"];
434	  [infos setObject: sourceFolder forKey: @"sourceFolder"];
435	}
436
437      if ([infos writeToFile: [self infoPath]  atomically: YES])
438	error = nil;
439      else
440	{
441	  [self errorWithFormat: @"could not write info: '%@'",
442                [self infoPath]];
443	  error = [NSException exceptionWithHTTPStatus:500 /* server error */
444                                                reason: @"could not write draft info!"];
445	}
446    }
447  else
448    {
449      [self errorWithFormat: @"could not create folder for draft: '%@'",
450            [self draftFolderPath]];
451      error = [NSException exceptionWithHTTPStatus:500 /* server error */
452                                            reason: @"could not create folder for draft!"];
453    }
454
455  return error;
456}
457
458//
459//
460//
461- (void) _loadInfosFromDictionary: (NSDictionary *) infoDict
462{
463  id value;
464
465  value = [infoDict objectForKey: @"headers"];
466  if (value)
467    [self setHeaders: value];
468
469  value = [infoDict objectForKey: @"text"];
470  if ([value length] > 0)
471    [self setText: value];
472  isHTML = [[infoDict objectForKey: @"isHTML"] boolValue];
473
474  value = [infoDict objectForKey: @"sourceIMAP4ID"];
475  if (value)
476    [self setSourceIMAP4ID: [value intValue]];
477
478  value = [infoDict objectForKey: @"IMAP4ID"];
479  if (value)
480    [self setIMAP4ID: [value intValue]];
481
482  value = [infoDict objectForKey: @"sourceURL"];
483  if (value)
484    [self setSourceURL: value];
485  value = [infoDict objectForKey: @"sourceFlag"];
486  if (value)
487    [self setSourceFlag: value];
488  value = [infoDict objectForKey: @"sourceFolder"];
489  if (value)
490    [self setSourceFolder: value];
491
492  value = [infoDict objectForKey: @"inReplyTo"];
493  if (value)
494    [self setInReplyTo: value];
495}
496
497//
498//
499//
500- (NSString *) relativeImap4Name
501{
502  return [NSString stringWithFormat: @"%d", IMAP4ID];
503}
504
505//
506//
507//
508- (void) fetchInfo
509{
510  NSString *p;
511  NSDictionary *infos;
512  NSFileManager *fm;
513
514  p = [self infoPath];
515
516  fm = [NSFileManager defaultManager];
517  if ([fm fileExistsAtPath: p])
518    {
519      infos = [NSDictionary dictionaryWithContentsOfFile: p];
520      if (infos)
521	[self _loadInfosFromDictionary: infos];
522//       else
523// 	[self errorWithFormat: @"draft info dictionary broken at path: %@", p];
524    }
525  else
526    [self debugWithFormat: @"Note: info object does not yet exist: %@", p];
527}
528
529//
530//
531//
532- (void) setSourceIMAP4ID: (int) newSourceIMAP4ID
533{
534  sourceIMAP4ID = newSourceIMAP4ID;
535}
536
537//
538//
539//
540- (int) sourceIMAP4ID
541{
542  return sourceIMAP4ID;
543}
544
545//
546//
547//
548- (void) setIMAP4ID: (int) newIMAP4ID
549{
550  IMAP4ID = newIMAP4ID;
551}
552
553//
554//
555//
556- (int) IMAP4ID
557{
558  return IMAP4ID;
559}
560
561//
562//
563//
564- (NSException *) save
565{
566  NGImap4Client *client;
567  NSException *error;
568  NSData *message;
569  NSString *folder;
570  id result;
571
572  error = nil;
573  message = [self mimeMessageForRecipient: nil];
574
575  if (!message)
576    {
577      error = [NSException exceptionWithHTTPStatus: 500 /* Server Error */
578                                            reason: @"Message is too big"];
579      return error;
580    }
581
582  client = [[self imap4Connection] client];
583
584  if (![imap4 doesMailboxExistAtURL: [container imap4URL]])
585    {
586      [[self imap4Connection] createMailbox: [[self imap4Connection] imap4FolderNameForURL: [container imap4URL]]
587                                      atURL: [[self mailAccountFolder] imap4URL]];
588      [imap4 flushFolderHierarchyCache];
589    }
590
591  folder = [imap4 imap4FolderNameForURL: [container imap4URL]];
592  result = [client append: message toFolder: folder
593                withFlags: [NSArray arrayWithObjects: @"draft", nil]];
594  if ([[result objectForKey: @"result"] boolValue])
595    {
596      if (IMAP4ID > -1)
597	error = [imap4 markURLDeleted: [self imap4URL]];
598      [self setIMAP4ID: [self IMAP4IDFromAppendResult: result]];
599      if (imap4URL)
600        {
601          // Invalidate the IMAP message URL since the message ID has changed
602          [imap4URL release];
603          imap4URL = nil;
604        }
605    }
606  else
607    error = [NSException exceptionWithHTTPStatus: 500 /* Server Error */
608                                          reason: [result objectForKey: @"reason"]];
609
610  return error;
611}
612
613//
614//
615//
616- (void) _addEMailsOfAddresses: (NSArray *) _addrs
617                       toArray: (NSMutableArray *) _ma
618{
619  NSEnumerator *addresses;
620  NGImap4EnvelopeAddress *currentAddress;
621
622  addresses = [_addrs objectEnumerator];
623  while ((currentAddress = [addresses nextObject]))
624    if ([currentAddress email])
625      [_ma addObject: [currentAddress email]];
626}
627
628//
629//
630//
631- (void) _addRecipients: (NSArray *) recipients
632	        toArray: (NSMutableArray *) array
633{
634  NSEnumerator *addresses;
635  NGImap4EnvelopeAddress *currentAddress;
636
637  addresses = [recipients objectEnumerator];
638  while ((currentAddress = [addresses nextObject]))
639    if ([currentAddress baseEMail])
640      [array addObject: [currentAddress baseEMail]];
641}
642
643//
644//
645//
646- (void) _purgeRecipients: (NSArray *) recipients
647	    fromAddresses: (NSMutableArray *) addresses
648{
649  NSEnumerator *allRecipients;
650  NSString *currentRecipient;
651  NGImap4EnvelopeAddress *currentAddress;
652  int count, max;
653
654  max = [addresses count];
655
656  allRecipients = [recipients objectEnumerator];
657  while (max > 0
658	 && ((currentRecipient = [allRecipients nextObject])))
659    for (count = max - 1; count >= 0; count--)
660      {
661	currentAddress = [addresses objectAtIndex: count];
662	if (![currentAddress baseEMail] ||
663            [currentRecipient
664              caseInsensitiveCompare: [currentAddress baseEMail]]
665            == NSOrderedSame)
666	  {
667	    [addresses removeObjectAtIndex: count];
668	    max--;
669	  }
670      }
671}
672
673//
674//
675//
676- (NSString *) _emailFromIdentity: (NSDictionary *) identity
677{
678  NSString *fullName, *format;
679
680  fullName = [identity objectForKey: @"fullName"];
681  if ([fullName length])
682    format = @"%{fullName} <%{email}>";
683  else
684    format = @"%{email}";
685
686  return [identity keysWithFormat: format];
687}
688
689- (void) _fillInFromAddress: (NSMutableDictionary *) _info
690            fromSentMailbox: (BOOL) _fromSentMailbox
691                   envelope: (NGImap4Envelope *) _envelope
692{
693  NSDictionary *identity;
694  NSMutableArray *addrs;
695  NSString *email;
696  SOGoMailAccount *account;
697  int i;
698
699  identity = nil;
700  account = [[self container] mailAccountFolder];
701  if (![account forceDefaultIdentity])
702    {
703      /* Pick the first email matching one of the account's identities */
704      addrs = [NSMutableArray array];
705      if (_fromSentMailbox)
706        [self _addRecipients: [_envelope from] toArray: addrs];
707      else
708        {
709          [self _addRecipients: [_envelope to] toArray: addrs];
710          [self _addRecipients: [_envelope cc] toArray: addrs];
711          [self _addRecipients: [_envelope bcc] toArray: addrs];
712        }
713
714      if ([addrs count])
715        {
716          for (i = 0; !identity && i < [addrs count]; i++)
717            {
718              email = [addrs objectAtIndex: i];
719              identity = [[[self container] mailAccountFolder] identityForEmail: email];
720            }
721          if (identity)
722            {
723              [_info setObject: [self _emailFromIdentity: identity]  forKey: @"from"];
724            }
725        }
726    }
727  if (!identity)
728    {
729      identity = [account defaultIdentity];
730      if (identity)
731        [_info setObject: [self _emailFromIdentity: identity]  forKey: @"from"];
732    }
733}
734
735//
736//
737//
738- (void) _fillInReplyAddresses: (NSMutableDictionary *) _info
739                    replyToAll: (BOOL) _replyToAll
740               fromSentMailbox: (BOOL) _fromSentMailbox
741                      envelope: (NGImap4Envelope *) _envelope
742{
743  /*
744    The rules as implemented by Thunderbird:
745    - if there is a 'reply-to' header, only include that (as TO)
746    - if we reply to all, all non-from addresses are added as CC
747    - the from is always the lone TO (except for reply-to)
748
749    Note: we cannot check reply-to, because Cyrus even sets a reply-to in the
750          envelope if none is contained in the message itself! (bug or
751          feature?)
752  */
753  NSMutableArray *to, *addrs, *allRecipients;
754  NSArray *envelopeAddresses;
755
756  allRecipients = [NSMutableArray array];
757
758  //
759  // When we do a Reply-To or a Reply-To-All, we strip our own addresses
760  // from the list of recipients so we don't reply to ourself! We check
761  // which addresses we should use - that is the ones for the current
762  // user if we're dealing with the default "SOGo mail account" or
763  // the ones specified in the auxiliary IMAP accounts
764  //
765  if ([[[self->container mailAccountFolder] nameInContainer] intValue] == 0)
766    {
767      NSArray *userEmails;
768
769      userEmails = [[context activeUser] allEmails];
770      [allRecipients addObjectsFromArray: userEmails];
771    }
772  else
773    {
774      NSArray *identities;
775      NSString *email;
776      int i;
777
778      identities = [[[self container] mailAccountFolder] identities];
779      for (i = 0; i < [identities count]; i++)
780        {
781          email = [[identities objectAtIndex: i] objectForKey: @"email"];
782          if (email)
783            [allRecipients addObject: email];
784        }
785    }
786
787  to = [NSMutableArray arrayWithCapacity: 2];
788
789  addrs = [NSMutableArray array];
790  envelopeAddresses = [_envelope replyTo];
791  if (_fromSentMailbox)
792    [addrs setArray: [_envelope to]];
793  else if ([envelopeAddresses count])
794    [addrs setArray: envelopeAddresses];
795  else
796    [addrs setArray: [_envelope from]];
797
798  [self _purgeRecipients: allRecipients  fromAddresses: addrs]; // addrs contain the recipient addresses without the any of the sender's addresses
799  [self _addEMailsOfAddresses: addrs  toArray: to];
800  [self _addRecipients: addrs  toArray: allRecipients];
801  [_info setObject: to  forKey: @"to"];
802
803  /* If "to" is empty, we add at least ourself as a recipient!
804     This is for emails in the "Sent" folder that we reply to... */
805  if (![to count])
806    {
807      if ([[_envelope replyTo] count])
808	[self _addEMailsOfAddresses: [_envelope replyTo]  toArray: to];
809      else
810	[self _addEMailsOfAddresses: [_envelope from]  toArray: to];
811    }
812
813  /* Pick the first email matching one of the account's identities */
814  [self _fillInFromAddress: _info
815           fromSentMailbox: _fromSentMailbox
816                  envelope: _envelope];
817
818  /* If we have no To but we have Cc recipients, let's move the Cc
819     to the To bucket... */
820  if ([[_info objectForKey: @"to"] count] == 0 && [_info objectForKey: @"cc"])
821    {
822      id o;
823
824      o = [_info objectForKey: @"cc"];
825      [_info setObject: o  forKey: @"to"];
826      [_info removeObjectForKey: @"cc"];
827    }
828
829  /* CC processing if we reply-to-all: - we add all 'to', 'cc' and 'bcc' fields */
830  if (_replyToAll)
831    {
832      to = [NSMutableArray array];
833
834      [addrs setArray: [_envelope to]];
835      [self _purgeRecipients: allRecipients
836               fromAddresses: addrs];
837      [self _addEMailsOfAddresses: addrs toArray: to];
838      [self _addRecipients: addrs toArray: allRecipients];
839
840      [addrs setArray: [_envelope cc]];
841      [self _purgeRecipients: allRecipients
842               fromAddresses: addrs];
843      [self _addEMailsOfAddresses: addrs toArray: to];
844      [self _addRecipients: addrs toArray: allRecipients];
845      [_info setObject: to forKey: @"cc"];
846
847      if ([[_envelope bcc] count])
848        {
849          to = [NSMutableArray array];
850          [addrs setArray: [_envelope bcc]];
851          [self _purgeRecipients: allRecipients
852                   fromAddresses: addrs];
853          [self _addEMailsOfAddresses: addrs toArray: to];
854          [_info setObject: to forKey: @"bcc"];
855        }
856    }
857}
858
859//
860//
861//
862- (void) _fetchAttachmentsFromMail: (SOGoMailObject *) sourceMail
863{
864  NSMutableDictionary *currentInfo;
865  NSArray *attachments;
866
867  unsigned int max, count;
868
869  attachments = [sourceMail fetchFileAttachments];
870  max = [attachments count];
871  for (count = 0; count < max; count++)
872    {
873      currentInfo = [attachments objectAtIndex: count];
874      [self saveAttachment: [currentInfo objectForKey: @"body"]
875              withMetadata: currentInfo];
876    }
877}
878
879//
880//
881//
882- (void) _fileAttachmentsFromPart: (id) thePart
883{
884  // Small hack to avoid SOPE's stupid behavior to wrap a multipart
885  // object in a NGMimeBodyPart.
886  if ([thePart isKindOfClass: [NGMimeBodyPart class]] &&
887      [[[thePart contentType] type] isEqualToString: @"multipart"])
888    thePart = [thePart body];
889
890  if ([thePart isKindOfClass: [NGMimeBodyPart class]])
891    {
892      NSString *filename, *mimeType;
893      id body;
894
895      mimeType = [[thePart contentType] stringValue];
896      body = [thePart body];
897      filename = [(NGMimeContentDispositionHeaderField *)[thePart headerForKey: @"content-disposition"] filename];
898
899      if (!filename)
900        filename = [mimeType asPreferredFilenameUsingPath: nil];
901
902      if (filename)
903        {
904          NSMutableDictionary *currentInfo;
905
906          currentInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
907                                               filename, @"filename",
908                                             mimeType, @"mimetype",
909                                             nil];
910          [self saveAttachment: body
911                  withMetadata: currentInfo];
912        }
913    }
914  else if ([thePart isKindOfClass: [NGMimeMultipartBody class]])
915    {
916      NSArray *parts;
917      int i;
918
919      parts = [thePart parts];
920      for (i = 0; i < [parts count]; i++)
921        {
922          [self _fileAttachmentsFromPart: [parts objectAtIndex: i]];
923        }
924    }
925}
926
927
928//
929//
930//
931- (void) _fetchAttachmentsFromEncryptedMail: (SOGoMailObject *) sourceMail
932{
933  NSData *certificate;
934
935  certificate = [[self mailAccountFolder] certificate];
936
937  // If we got a user certificate, let's use it. Otherwise we fallback we
938  // don't try to get any attachments from the encrypted content
939  if (certificate)
940    {
941      NGMimeMessage *m;
942
943      m = [[sourceMail content] messageFromEncryptedDataAndCertificate: certificate];
944      [self _fileAttachmentsFromPart: [m body]];
945    }
946}
947
948
949//
950//
951//
952- (void) _fetchAttachmentsFromOpaqueSignedMail: (SOGoMailObject *) sourceMail
953{
954  NGMimeMessage *m;
955
956  m = [[sourceMail content] messageFromOpaqueSignedData];
957  [self _fileAttachmentsFromPart: [m body]];
958}
959
960
961//
962//
963//
964- (void) fetchMailForEditing: (SOGoMailObject *) sourceMail
965{
966  NSString *subject, *msgid;
967  NSMutableDictionary *info;
968  NSDictionary *h;
969  NSMutableArray *addresses;
970  NGImap4Envelope *sourceEnvelope;
971  SOGoUserDefaults *ud;
972  id priority, receipt;
973
974  [sourceMail fetchCoreInfos];
975
976  [self _fetchAttachmentsFromMail: sourceMail];
977  info = [NSMutableDictionary dictionaryWithCapacity: 16];
978  subject = [sourceMail subject];
979  if ([subject length] > 0)
980    [info setObject: subject forKey: @"subject"];
981
982  sourceEnvelope = [sourceMail envelope];
983  msgid = [sourceEnvelope messageID];
984  if ([msgid length] > 0)
985    [info setObject: msgid forKey: @"message-id"];
986
987  addresses = [NSMutableArray array];
988  [self _addEMailsOfAddresses: [sourceEnvelope from] toArray: addresses];
989  if ([addresses count])
990    [info setObject: [addresses objectAtIndex: 0] forKey: @"from"];
991  addresses = [NSMutableArray array];
992  [self _addEMailsOfAddresses: [sourceEnvelope to] toArray: addresses];
993  [info setObject: addresses forKey: @"to"];
994  addresses = [NSMutableArray array];
995  [self _addEMailsOfAddresses: [sourceEnvelope cc] toArray: addresses];
996  if ([addresses count] > 0)
997    [info setObject: addresses forKey: @"cc"];
998  addresses = [NSMutableArray array];
999  [self _addEMailsOfAddresses: [sourceEnvelope bcc] toArray: addresses];
1000  if ([addresses count] > 0)
1001    [info setObject: addresses forKey: @"bcc"];
1002  addresses = [NSMutableArray array];
1003  [self _addEMailsOfAddresses: [sourceEnvelope replyTo] toArray: addresses];
1004  if ([addresses count] > 0)
1005    [info setObject: addresses forKey: @"replyTo"];
1006
1007  h = [sourceMail mailHeaders];
1008  priority = [h objectForKey: @"x-priority"];
1009  if ([priority isNotEmpty] && [priority isKindOfClass: [NSString class]])
1010    [info setObject: (NSString*)priority forKey: @"X-Priority"];
1011  receipt = [h objectForKey: @"disposition-notification-to"];
1012  if ([receipt isNotEmpty] && [receipt isKindOfClass: [NSString class]])
1013    [info setObject: (NSString*)receipt forKey: @"Disposition-Notification-To"];
1014
1015  ud = [[context activeUser] userDefaults];
1016
1017  [self setHeaders: info];
1018  [self setText: [sourceMail contentForEditing]];
1019  [self setIMAP4ID: [[sourceMail nameInContainer] intValue]];
1020  [self setIsHTML: [[ud mailComposeMessageType] isEqualToString: @"html"]];
1021}
1022
1023//
1024//
1025//
1026- (void) fetchMailForReplying: (SOGoMailObject *) sourceMail
1027                        toAll: (BOOL) toAll
1028{
1029  BOOL fromSentMailbox;
1030  NSString *msgID;
1031  NSMutableDictionary *info;
1032  NGImap4Envelope *sourceEnvelope;
1033  SOGoUserDefaults *ud;
1034
1035  fromSentMailbox = [[sourceMail container] isKindOfClass: [SOGoSentFolder class]];
1036  [sourceMail fetchCoreInfos];
1037
1038  info = [NSMutableDictionary dictionaryWithCapacity: 16];
1039  [info setObject: [sourceMail subjectForReply] forKey: @"subject"];
1040
1041  sourceEnvelope = [sourceMail envelope];
1042  [self _fillInReplyAddresses: info
1043                   replyToAll: toAll
1044              fromSentMailbox: fromSentMailbox
1045                     envelope: sourceEnvelope];
1046  msgID = [sourceEnvelope messageID];
1047  if ([msgID length] > 0)
1048    [self setInReplyTo: msgID];
1049
1050  ud = [[context activeUser] userDefaults];
1051
1052  [self setText: [sourceMail contentForReply]];
1053  [self setHeaders: info];
1054  [self setIsHTML: [[ud mailComposeMessageType] isEqualToString: @"html"]];
1055  [self setSourceURL: [sourceMail imap4URLString]];
1056  [self setSourceFlag: @"Answered"];
1057  [self setSourceIMAP4ID: [[sourceMail nameInContainer] intValue]];
1058  [self setSourceFolderWithMailObject: sourceMail];
1059
1060  [self storeInfo];
1061}
1062
1063- (void) fetchMailForForwarding: (SOGoMailObject *) sourceMail
1064{
1065  BOOL fromSentMailbox;
1066  NGImap4Envelope *sourceEnvelope;
1067  NSMutableDictionary *attachment, *info;
1068  NSString *signature, *nl, *space;
1069  SOGoUserDefaults *ud;
1070
1071  fromSentMailbox = [[sourceMail container] isKindOfClass: [SOGoSentFolder class]];
1072  [sourceMail fetchCoreInfos];
1073  sourceEnvelope = [sourceMail envelope];
1074  info = [NSMutableDictionary dictionaryWithCapacity: 2];
1075
1076  if ([sourceMail subjectForForward])
1077    {
1078      [info setObject: [sourceMail subjectForForward] forKey: @"subject"];
1079    }
1080
1081  [self _fillInFromAddress: info
1082           fromSentMailbox: fromSentMailbox
1083                  envelope: sourceEnvelope];
1084  [self setHeaders: info];
1085
1086  [self setSourceURL: [sourceMail imap4URLString]];
1087  [self setSourceFlag: @"$Forwarded"];
1088  [self setSourceIMAP4ID: [[sourceMail nameInContainer] intValue]];
1089  [self setSourceFolderWithMailObject: sourceMail];
1090
1091  /* attach message */
1092  ud = [[context activeUser] userDefaults];
1093  if ([[ud mailMessageForwarding] isEqualToString: @"inline"])
1094    {
1095      [self setText: [sourceMail contentForInlineForward]];
1096      if ([sourceMail isEncrypted])
1097        [self _fetchAttachmentsFromEncryptedMail: sourceMail];
1098      else if ([sourceMail isOpaqueSigned])
1099        [self _fetchAttachmentsFromOpaqueSignedMail: sourceMail];
1100      else
1101        [self _fetchAttachmentsFromMail: sourceMail];
1102    }
1103  else
1104    {
1105      // TODO: use subject for filename?
1106      // error = [newDraft saveAttachment:content withName:@"forward.eml"];
1107      signature = [[self mailAccountFolder] signature];
1108      if ([signature length])
1109        {
1110          nl = (isHTML ? @"<br />" : @"\n");
1111          space = (isHTML ? @"&nbsp;" : @" ");
1112          [self setText: [NSString stringWithFormat: @"%@%@--%@%@%@", nl, nl, space, nl, signature]];
1113        }
1114      attachment = [NSMutableDictionary dictionaryWithObjectsAndKeys:
1115                                          [sourceMail filenameForForward], @"filename",
1116                                        @"message/rfc822", @"mimetype",
1117                                        nil];
1118      [self saveAttachment: [sourceMail content]
1119              withMetadata: attachment];
1120    }
1121
1122  // Save the message to the IMAP store so the user can eventually view the attached file(s)
1123  // from the Web interface
1124  [self save];
1125
1126  [self storeInfo];
1127}
1128
1129/* accessors */
1130
1131- (NSString *) sender
1132{
1133  id tmp;
1134
1135  if ((tmp = [headers objectForKey: @"from"]) == nil)
1136    return nil;
1137  if ([tmp isKindOfClass:[NSArray class]])
1138    return [tmp count] > 0 ? [tmp objectAtIndex: 0] : nil;
1139
1140  return tmp;
1141}
1142
1143/* attachments */
1144
1145//
1146// Return the attributes (name, size and mime body part) of the files found in the draft folder
1147// on the local filesystem
1148//
1149- (NSArray *) fetchAttachmentAttrs
1150{
1151  NSMutableArray *ma;
1152  NSFileManager *fm;
1153  NSArray *files;
1154  NSString *filename;
1155  NSDictionary *fileAttrs;
1156  NGMimeBodyPart *bodyPart;
1157  unsigned count, max;
1158
1159  fm = [NSFileManager defaultManager];
1160  files = [fm directoryContentsAtPath: [self draftFolderPath]];
1161
1162  max = [files count];
1163  ma = [NSMutableArray arrayWithCapacity: max];
1164  for (count = 0; count < max; count++)
1165    {
1166      filename = [files objectAtIndex: count];
1167      if (![filename hasPrefix: @"."])
1168        {
1169          fileAttrs = [fm fileAttributesAtPath: [self pathToAttachmentWithName: filename] traverseLink: YES];
1170          bodyPart = [self bodyPartForAttachmentWithName: filename];
1171          [ma addObject: [NSDictionary dictionaryWithObjectsAndKeys: filename, @"filename",
1172                                            [fileAttrs objectForKey: @"NSFileSize"], @"size",
1173                                       bodyPart, @"part", nil]];
1174        }
1175    }
1176
1177  return ma;
1178}
1179
1180
1181- (NSString *) pathToAttachmentWithName: (NSString *) _name
1182{
1183  if ([_name length] == 0)
1184    return nil;
1185
1186  return [[self draftFolderPath] stringByAppendingPathComponent:_name];
1187}
1188
1189
1190/**
1191 * Write attachment file to the spool directory of the draft and write a dot
1192 * file with its mime type.
1193 */
1194- (NSException *) saveAttachment: (NSData *) _attach
1195                    withMetadata: (NSMutableDictionary *) metadata
1196{
1197  NSFileManager *fm;
1198  NSString *p, *pmime, *name, *baseName, *extension, *mimeType;
1199  int i;
1200
1201  if (![_attach isNotNull])
1202    {
1203      return [NSException exceptionWithHTTPStatus: 400 /* Bad Request */
1204                                           reason: @"Missing attachment content!"];
1205    }
1206
1207  if (![self _ensureDraftFolderPath])
1208    {
1209      return [NSException exceptionWithHTTPStatus: 500 /* Server Error */
1210                                           reason: @"Could not create folder for draft!"];
1211    }
1212
1213  name = [[metadata objectForKey: @"filename"] asSafeFilename];
1214  baseName = [name stringByDeletingPathExtension];
1215  extension = [name pathExtension];
1216  fm = [NSFileManager defaultManager];
1217  p = [self pathToAttachmentWithName: name];
1218  i = 1;
1219
1220  while ([fm isReadableFileAtPath: p])
1221    {
1222      name = [NSString stringWithFormat: @"%@-%x", baseName, i];
1223      if ([extension length])
1224        name = [NSString stringWithFormat: @"%@.%@", name, extension];
1225      p = [self pathToAttachmentWithName: name];
1226      [metadata setObject: name forKey: @"filename"];
1227      i++;
1228    }
1229
1230  if (![_attach writeToFile: p atomically: YES])
1231    {
1232      return [NSException exceptionWithHTTPStatus: 500 /* Server Error */
1233                                           reason: @"Could not write attachment to draft!"];
1234    }
1235
1236  mimeType = [metadata objectForKey: @"mimetype"];
1237  if ([mimeType length] > 0)
1238    {
1239      pmime = [self pathToAttachmentWithName: [NSString stringWithFormat: @".%@.mime", name]];
1240      if (![[mimeType dataUsingEncoding: NSUTF8StringEncoding] writeToFile: pmime  atomically: YES])
1241        {
1242          [[NSFileManager defaultManager] removeFileAtPath: p  handler: nil];
1243          return [NSException exceptionWithHTTPStatus: 500 /* Server Error */
1244                                               reason: @"Could not write attachment to draft!"];
1245        }
1246    }
1247
1248  return nil; /* everything OK */
1249}
1250
1251- (NSException *) deleteAttachmentWithName: (NSString *) _name
1252{
1253  NSFileManager *fm;
1254  NSString *p;
1255  NSException *error;
1256
1257  error = nil;
1258  fm = [NSFileManager defaultManager];
1259  p = [self pathToAttachmentWithName: [_name asSafeFilename]];
1260  if ([fm fileExistsAtPath: p])
1261    if (![fm removeFileAtPath: p handler: nil])
1262      error = [NSException exceptionWithHTTPStatus: 500 /* Server Error */
1263                                            reason: @"Could not delete attachment from draft!"];
1264
1265  return error;
1266}
1267
1268//
1269// Only called when converting text/html to text/plain parts
1270//
1271- (NGMimeBodyPart *) plainTextBodyPartForText
1272{
1273  NGMutableHashMap *map;
1274  NGMimeBodyPart   *bodyPart;
1275  NSString *plainText;
1276
1277  /* prepare header of body part */
1278  map = [[[NGMutableHashMap alloc] initWithCapacity: 1] autorelease];
1279
1280  [map setObject: contentTypeValue forKey: @"content-type"];
1281
1282  /* prepare body content */
1283  bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease];
1284
1285  plainText = [text htmlToText];
1286  [bodyPart setBody: plainText];
1287
1288  return bodyPart;
1289}
1290
1291
1292//
1293//
1294//
1295- (NGMimeBodyPart *) bodyPartForText
1296{
1297  /*
1298    This add the text typed by the user (the primary plain/text part).
1299  */
1300  NGMutableHashMap *map;
1301  NGMimeBodyPart   *bodyPart;
1302
1303  /* prepare header of body part */
1304  map = [[[NGMutableHashMap alloc] initWithCapacity: 1] autorelease];
1305
1306  // TODO: set charset in header!
1307  if (text)
1308    [map setObject: (isHTML ? htmlContentTypeValue : contentTypeValue)
1309            forKey: @"content-type"];
1310
1311  /* prepare body content */
1312  bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease];
1313  [bodyPart setBody: text];
1314
1315  return bodyPart;
1316}
1317
1318- (NGMimeMessage *) mimeMessageForContentWithHeaderMap: (NGMutableHashMap *) map
1319{
1320  NGMimeMessage *message;
1321  id body;
1322
1323  message = [[[NGMimeMessage alloc] initWithHeader: map] autorelease];
1324
1325  if (!isHTML)
1326    {
1327      [message setHeader: contentTypeValue  forKey: @"content-type"];
1328      body = text;
1329    }
1330  else
1331    {
1332      body = [[[NGMimeMultipartBody alloc] initWithPart: message] autorelease];
1333      [message setHeader: MultiAlternativeType forKey: @"content-type"];
1334
1335      // Get the text part from it and add it
1336      [body addBodyPart: [self plainTextBodyPartForText]];
1337
1338      // Add the HTML part
1339      [body addBodyPart: [self bodyPartForText]];
1340    }
1341
1342  [message setBody: body];
1343
1344  return message;
1345}
1346
1347- (NSString *) mimeTypeForExtension: (NSString *) _ext
1348{
1349  // TODO: make configurable
1350  // TODO: use /etc/mime-types
1351  if ([_ext isEqualToString: @"txt"])  return @"text/plain";
1352  if ([_ext isEqualToString: @"html"]) return @"text/html";
1353  if ([_ext isEqualToString: @"htm"])  return @"text/html";
1354  if ([_ext isEqualToString: @"gif"])  return @"image/gif";
1355  if ([_ext isEqualToString: @"jpg"])  return @"image/jpeg";
1356  if ([_ext isEqualToString: @"jpeg"]) return @"image/jpeg";
1357  if ([_ext isEqualToString: @"eml"]) return @"message/rfc822";
1358  return @"application/octet-stream";
1359}
1360
1361- (NSString *) contentTypeForAttachmentWithName: (NSString *) _name
1362{
1363  NSString *s, *p;
1364  NSData *mimeData;
1365
1366  p = [self pathToAttachmentWithName: [NSString stringWithFormat: @".%@.mime", _name]];
1367  mimeData = [NSData dataWithContentsOfFile: p];
1368  if (mimeData)
1369    {
1370      s = [[NSString alloc] initWithData: mimeData
1371                                encoding: NSUTF8StringEncoding];
1372      [s autorelease];
1373    }
1374  else
1375    {
1376      s = [self mimeTypeForExtension:[_name pathExtension]];
1377      if ([_name length] > 0)
1378	s = [s stringByAppendingFormat: @"; name=\"%@\"", [_name stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""]];
1379    }
1380
1381  return s;
1382}
1383
1384- (NSString *) contentDispositionForAttachmentWithName: (NSString *) _name
1385                                        andContentType: (NSString *) _type
1386{
1387  NSString *cdtype;
1388  NSString *cd;
1389  SOGoDomainDefaults *dd;
1390
1391  if ([_type hasPrefix: @"text/"])
1392    {
1393      dd = [[context activeUser] domainDefaults];
1394      cdtype = [dd mailAttachTextDocumentsInline] ? @"inline" : @"attachment";
1395    }
1396  else if ([_type hasPrefix: @"image/"] || [_type hasPrefix: @"message"])
1397    cdtype = @"inline";
1398  else
1399    cdtype = @"attachment";
1400
1401  cd = [cdtype stringByAppendingString: @"; filename=\""];
1402  cd = [cd stringByAppendingString: [_name stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""]];
1403  cd = [cd stringByAppendingString: @"\""];
1404
1405  // TODO: add size parameter (useful addition, RFC 2183)
1406  return cd;
1407}
1408
1409- (NGMimeBodyPart *) bodyPartForAttachmentWithName: (NSString *) _name
1410{
1411  NSFileManager    *fm;
1412  NGMutableHashMap *map;
1413  NGMimeBodyPart   *bodyPart;
1414  NSString         *s;
1415  NSData           *content;
1416  BOOL             attachAsString, attachAsRFC822;
1417  NSString         *p;
1418  id body;
1419
1420  if (_name == nil) return nil;
1421
1422  /* check attachment */
1423
1424  fm = [NSFileManager defaultManager];
1425  p  = [self pathToAttachmentWithName: _name];
1426  if (![fm isReadableFileAtPath: p]) {
1427    [self errorWithFormat: @"did not find attachment: '%@'", _name];
1428    return nil;
1429  }
1430  attachAsString = NO;
1431  attachAsRFC822 = NO;
1432
1433  /* prepare header of body part */
1434
1435  map = [[[NGMutableHashMap alloc] initWithCapacity: 4] autorelease];
1436
1437  if ((s = [self contentTypeForAttachmentWithName:_name]) != nil) {
1438    [map setObject: s forKey: @"content-type"];
1439    if ([s hasPrefix: @"text/plain"] || [s hasPrefix: @"text/html"])
1440      attachAsString = YES;
1441    else if ([s hasPrefix: @"message/rfc822"])
1442      attachAsRFC822 = YES;
1443  }
1444  if ((s = [self contentDispositionForAttachmentWithName: _name andContentType: s]))
1445    {
1446      NGMimeContentDispositionHeaderField *o;
1447
1448      o = [[NGMimeContentDispositionHeaderField alloc] initWithString: s];
1449      [map setObject: o forKey: @"content-disposition"];
1450      [o release];
1451    }
1452
1453  /* prepare body content */
1454
1455  if (attachAsString) { // TODO: is this really necessary?
1456    NSString *s;
1457
1458    content = [[NSData alloc] initWithContentsOfMappedFile:p];
1459
1460    s = [[NSString alloc] initWithData: content
1461                              encoding: [NSString defaultCStringEncoding]];
1462    if (s != nil) {
1463      body = s;
1464      [content release]; content = nil;
1465    }
1466    else {
1467      [self warnWithFormat:
1468              @"could not get text attachment as string: '%@'", _name];
1469      body = content;
1470      content = nil;
1471    }
1472  }
1473  else {
1474    /*
1475      Note: in OGo this is done in LSWImapMailEditor.m:2477. Apparently
1476      NGMimeFileData objects are not processed by the MIME generator!
1477    */
1478    content = [[NSData alloc] initWithContentsOfMappedFile:p];
1479    [content autorelease];
1480
1481    if (attachAsRFC822)
1482      {
1483        [map setObject: @"8bit" forKey: @"content-transfer-encoding"];
1484      }
1485    else
1486      {
1487	content = [content dataByEncodingBase64];
1488        [map setObject: @"base64" forKey: @"content-transfer-encoding"];
1489      }
1490    [map setObject: [NSNumber numberWithInt: [content length]]
1491            forKey: @"content-length"];
1492
1493    /* Note: the -init method will create a temporary file! */
1494    body = [[NGMimeFileData alloc] initWithBytes:[content bytes]
1495                                          length:[content length]];
1496  }
1497
1498  bodyPart = [[[NGMimeBodyPart alloc] initWithHeader:map] autorelease];
1499  [bodyPart setBody:body];
1500
1501  [body release]; body = nil;
1502  return bodyPart;
1503}
1504
1505//
1506// returns nil on error
1507//
1508- (NSArray *) bodyPartsForAllAttachments
1509{
1510  NGMimeBodyPart *bodyPart;
1511  NSMutableArray *bodyParts;
1512  NSArray  *attrs;
1513  unsigned i, count, size, limit;
1514
1515  attrs = [self fetchAttachmentAttrs];
1516  count = [attrs count];
1517  size = 0;
1518
1519  // We first check if we don't go over our message size limit
1520  limit = [[SOGoSystemDefaults sharedSystemDefaults] maximumMessageSizeLimit] * 1024;
1521  for (i = 0; i < count; i++)
1522    size += [[[attrs objectAtIndex: i] objectForKey: @"size"] intValue];
1523
1524  if (limit && size > limit)
1525    return nil;
1526
1527  bodyParts = [NSMutableArray arrayWithCapacity: count];
1528
1529  for (i = 0; i < count; i++)
1530    {
1531      bodyPart = [self bodyPartForAttachmentWithName: [[attrs objectAtIndex: i] objectForKey: @"filename"]];
1532      [bodyParts addObject: bodyPart];
1533    }
1534
1535  return bodyParts;
1536}
1537
1538//
1539//
1540//
1541- (NGMimeBodyPart *) mimeMultipartAlternative: (NSArray *) extractedBodyParts
1542{
1543  NGMimeMultipartBody *textParts;
1544  NGMutableHashMap *header;
1545  NGMimeBodyPart *part;
1546
1547  header = [NGMutableHashMap hashMap];
1548  [header addObject: MultiAlternativeType forKey: @"content-type"];
1549
1550  part = [NGMimeBodyPart bodyPartWithHeader: header];
1551
1552  textParts = [[NGMimeMultipartBody alloc] initWithPart: part];
1553
1554  // Get the text part from it and add it
1555  [textParts addBodyPart: [self plainTextBodyPartForText]];
1556
1557  if ([extractedBodyParts count])
1558    {
1559      // Create a multipart/related part and add this.
1560      // We have inline image to avoid Thunderbird bug #61815 (https://bugzilla.mozilla.org/show_bug.cgi?id=61815)
1561      NGMutableHashMap *relatedHeader;
1562      NGMimeBodyPart *relatedPart;
1563      NGMimeMultipartBody *relatedParts;
1564      int i;
1565
1566      relatedHeader = [NGMutableHashMap hashMap];
1567      [relatedHeader addObject: MultiRelatedType forKey: @"content-type"];
1568      relatedPart = [NGMimeBodyPart bodyPartWithHeader: relatedHeader];
1569      relatedParts = [[NGMimeMultipartBody alloc] initWithPart: relatedPart];
1570
1571      [relatedParts addBodyPart: [self bodyPartForText]];
1572
1573      for (i = 0; i < [extractedBodyParts count]; i++)
1574        {
1575          [relatedParts addBodyPart: [extractedBodyParts objectAtIndex: i]];
1576        }
1577
1578      [relatedPart setBody: relatedParts];
1579      [textParts addBodyPart: relatedPart];
1580    }
1581  else
1582    {
1583      // Add the HTML part
1584      [textParts addBodyPart: [self bodyPartForText]];
1585    }
1586
1587  [part setBody: textParts];
1588  RELEASE(textParts);
1589
1590  return part;
1591}
1592
1593//
1594//
1595//
1596- (NGMimeMessage *) mimeMultiPartMessageWithHeaderMap: (NGMutableHashMap *) map
1597                                   extractedBodyParts: (NSArray *) extractedBodyParts
1598                                         andBodyParts: (NSArray *) _bodyParts
1599                                             bodyOnly: (BOOL) _bodyOnly
1600{
1601  NGMimeMessage       *message;
1602  NGMimeMultipartBody *mBody;
1603  NSEnumerator        *e;
1604  id                  part;
1605
1606  [map addObject: MultiMixedType forKey: @"content-type"];
1607
1608  message = [[NGMimeMessage alloc] initWithHeader: map];
1609  [message autorelease];
1610  mBody = [[NGMimeMultipartBody alloc] initWithPart: message];
1611
1612  if (!isHTML)
1613    part = [self bodyPartForText];
1614  else
1615    part = [self mimeMultipartAlternative: extractedBodyParts];
1616
1617  [mBody addBodyPart: part];
1618
1619  e = [_bodyParts objectEnumerator];
1620  part = [e nextObject];
1621  while (part)
1622    {
1623      [mBody addBodyPart: part];
1624      part = [e nextObject];
1625    }
1626
1627  [message setBody: mBody];
1628  [mBody release];
1629
1630  return message;
1631}
1632
1633//
1634//
1635//
1636- (void) _addHeaders: (NSDictionary *) _h
1637         toHeaderMap: (NGMutableHashMap *) _map
1638{
1639  NSEnumerator *names;
1640  NSString *name;
1641
1642  if ([_h count] == 0)
1643    return;
1644
1645  names = [_h keyEnumerator];
1646  while ((name = [names nextObject]) != nil) {
1647    id value;
1648
1649    value = [_h objectForKey:name];
1650    [_map addObject:value forKey:name];
1651  }
1652}
1653
1654- (BOOL) isEmptyValue: (id) _value
1655{
1656  if (![_value isNotNull])
1657    return YES;
1658
1659  if ([_value isKindOfClass: [NSArray class]])
1660    return [_value count] == 0 ? YES : NO;
1661
1662  if ([_value isKindOfClass: [NSString class]])
1663    return [_value length] == 0 ? YES : NO;
1664
1665  return NO;
1666}
1667
1668- (NSString *) _quoteSpecials: (NSString *) address
1669{
1670  NSString *result, *part, *s2;
1671  int i, len;
1672
1673  // We want to correctly send mails to recipients such as :
1674  // foo.bar
1675  // foo (bar) <foo@zot.com>
1676  // bar, foo <foo@zot.com>
1677  if ([address indexOf: '('] >= 0 || [address indexOf: ')'] >= 0
1678      || [address indexOf: '<'] >= 0 || [address indexOf: '>'] >= 0
1679      || [address indexOf: '@'] >= 0 || [address indexOf: ','] >= 0
1680      || [address indexOf: ';'] >= 0 || [address indexOf: ':'] >= 0
1681      || [address indexOf: '\\'] >= 0 || [address indexOf: '"'] >= 0
1682      || [address indexOf: '.'] >= 0
1683      || [address indexOf: '['] >= 0 || [address indexOf: ']'] >= 0)
1684    {
1685      // We search for the first instance of < from the end
1686      // and we quote what was before if we need to
1687      len = [address length];
1688      i = -1;
1689      while (len--)
1690        if ([address characterAtIndex: len] == '<')
1691          {
1692            i = len;
1693            break;
1694          }
1695
1696      if (i > 0)
1697        {
1698          part = [address substringToIndex: i - 1];
1699          s2 = [[part stringByReplacingString: @"\\" withString: @"\\\\"]
1700                     stringByReplacingString: @"\"" withString: @"\\\""];
1701          result = [NSString stringWithFormat: @"\"%@\" %@", s2, [address substringFromIndex: i]];
1702        }
1703      else
1704        {
1705          s2 = [[address stringByReplacingString: @"\\" withString: @"\\\\"]
1706                     stringByReplacingString: @"\"" withString: @"\\\""];
1707          result = [NSString stringWithFormat: @"\"%@\"", s2];
1708        }
1709    }
1710  else
1711    result = address;
1712
1713  return result;
1714}
1715
1716- (NSArray *) _quoteSpecialsInArray: (NSArray *) addresses
1717{
1718  NSMutableArray *result;
1719  NSString *address;
1720  int count, max;
1721
1722  max = [addresses count];
1723  result = [NSMutableArray arrayWithCapacity: max];
1724  for (count = 0; count < max; count++)
1725    {
1726      address = [self _quoteSpecials: [addresses objectAtIndex: count]];
1727      [result addObject: address];
1728    }
1729
1730  return result;
1731}
1732
1733- (NGMutableHashMap *) mimeHeaderMapWithHeaders: (NSDictionary *) _headers
1734                                      excluding: (NSArray *) _exclude
1735{
1736  NSString *s, *dateString;
1737  NGMutableHashMap *map;
1738  id emails, from, replyTo;
1739
1740  map = [[[NGMutableHashMap alloc] initWithCapacity:16] autorelease];
1741
1742  /* add recipients */
1743  if ((emails = [headers objectForKey: @"to"]) != nil && [emails isKindOfClass: [NSArray class]])
1744    [map setObjects: [self _quoteSpecialsInArray: emails] forKey: @"to"];
1745  if ((emails = [headers objectForKey: @"cc"]) != nil && [emails isKindOfClass: [NSArray class]])
1746    [map setObjects: [self _quoteSpecialsInArray: emails] forKey: @"cc"];
1747  if ((emails = [headers objectForKey: @"bcc"]) != nil && [emails isKindOfClass: [NSArray class]])
1748    [map setObjects: [self _quoteSpecialsInArray: emails] forKey: @"bcc"];
1749
1750  /* add senders */
1751  from = [headers objectForKey: @"from"];
1752
1753  if (![self isEmptyValue:from]) {
1754    if ([from isKindOfClass:[NSArray class]])
1755      [map setObjects: [self _quoteSpecialsInArray: from] forKey: @"from"];
1756    else
1757      [map setObject: [self _quoteSpecials: from] forKey: @"from"];
1758  }
1759
1760  if ((replyTo = [headers objectForKey: @"reply-to"]))
1761    [map setObject: replyTo forKey: @"reply-to"];
1762
1763  if (inReplyTo)
1764    [map setObject: inReplyTo forKey: @"in-reply-to"];
1765
1766  /* add subject */
1767  if ([(s = [headers objectForKey: @"subject"]) length] > 0)
1768    [map setObject: [s asQPSubjectString: @"utf-8"]
1769            forKey: @"subject"];
1770
1771  if ([(s = [headers objectForKey: @"message-id"]) length] > 0)
1772    [map setObject: s
1773            forKey: @"message-id"];
1774
1775  /* add standard headers */
1776  dateString = [[NSCalendarDate date] rfc822DateString];
1777  [map addObject: dateString forKey: @"date"];
1778  [map addObject: @"1.0" forKey: @"MIME-Version"];
1779  [map addObject: userAgent forKey: @"User-Agent"];
1780
1781  /* add custom headers */
1782  if ([(s = [[context request] headerForKey:@"x-webobjects-remote-host"]) length] > 0 &&
1783      [s compare: @"localhost"] != NSOrderedSame)
1784    [map addObject: s
1785	    forKey: @"X-Forward"];
1786  if ([(s = [headers objectForKey: @"X-Priority"]) length] > 0)
1787    [map setObject: s
1788            forKey: @"X-Priority"];
1789  if ([(s = [headers objectForKey: @"Disposition-Notification-To"]) length] > 0)
1790    [map setObject: s
1791            forKey: @"Disposition-Notification-To"];
1792
1793  [self _addHeaders: _headers toHeaderMap: map];
1794
1795  // We remove what we have to...
1796  if (_exclude)
1797    {
1798      int i;
1799
1800      for (i = 0; i < [_exclude count]; i++)
1801	[map removeAllObjectsForKey: [_exclude objectAtIndex: i]];
1802    }
1803
1804  return map;
1805}
1806
1807//
1808//
1809//
1810- (NGMimeMessage *) mimeMessageWithHeaders: (NSDictionary *) _headers
1811                                 excluding: (NSArray *) _exclude
1812                          extractingImages: (BOOL) _extractImages
1813                                  bodyOnly: (BOOL) _bodyOnly
1814{
1815  NSMutableArray *extractedBodyParts;
1816  NGMimeMessage *message;
1817  NSArray *allBodyParts;
1818  NGMutableHashMap *map;
1819  NSString *newText;
1820
1821  message = nil;
1822  extractedBodyParts = [NSMutableArray array];
1823
1824  if (_extractImages)
1825    {
1826      newText = [text htmlByExtractingImages: extractedBodyParts];
1827      if ([extractedBodyParts count])
1828        [self setText: newText];
1829    }
1830
1831  map = [self mimeHeaderMapWithHeaders: _headers
1832                             excluding: _exclude];
1833
1834  if (map)
1835    {
1836      //[self debugWithFormat: @"MIME Envelope: %@", map];
1837      allBodyParts = [self bodyPartsForAllAttachments];
1838
1839      if (!allBodyParts)
1840	return nil;
1841
1842      //[self debugWithFormat: @"attachments: %@", bodyParts];
1843
1844      if ([extractedBodyParts count] == 0 && [allBodyParts count] == 0)
1845        {
1846          // no attachment
1847          message = [self mimeMessageForContentWithHeaderMap: (_bodyOnly ? nil : map)];
1848        }
1849      else
1850        {
1851          message = [self mimeMultiPartMessageWithHeaderMap: (_bodyOnly ? [NGMutableHashMap hashMap] : map)
1852                                         extractedBodyParts: extractedBodyParts
1853                                               andBodyParts: allBodyParts
1854                                                   bodyOnly: _bodyOnly];
1855          //[self debugWithFormat: @"message: %@", message];
1856        }
1857    }
1858
1859  return message;
1860}
1861
1862//
1863// Return a NGMimeMessage object with inline HTML images (<img src=data>) extracted as attachments (<img src=cid>).
1864//
1865- (NSData *) mimeMessageForRecipient: (NSString *) theRecipient
1866{
1867  NGMimeMessageGenerator *generator, *partGenerator;
1868  NGMimeMessage *mimeMessage;
1869  NSData *certificate, *content;
1870  NGMutableHashMap *hashMap;
1871  NGMimeMessage *message;
1872  NSMutableData *d;
1873
1874  // Nothing to sign or encrypt, let's generate the message and return immediately
1875  if (![self sign] && ![self encrypt])
1876    {
1877      mimeMessage = [self mimeMessageWithHeaders: nil  excluding: nil  extractingImages: YES  bodyOnly: NO];
1878      if (mimeMessage)
1879        {
1880          generator = [[[NGMimeMessageGenerator alloc] init] autorelease];
1881          return [generator generateMimeFromPart: mimeMessage];
1882        }
1883      else
1884        return nil;
1885    }
1886
1887  // We'll sign and/or encrypt our message. Let's generate the actual body of the message to work with
1888  partGenerator = [[[NGMimePartGenerator alloc] init] autorelease];
1889  content = [partGenerator generateMimeFromPart: [self mimeMessageWithHeaders: nil  excluding: nil  extractingImages: YES  bodyOnly: YES]];
1890
1891  if ([self sign])
1892    {
1893      certificate = [[self mailAccountFolder] certificate];
1894      content = [content signUsingCertificateAndKey: certificate];
1895
1896      if (!content)
1897        return nil;
1898
1899      if (![self encrypt])
1900        goto finish_smime;
1901    }
1902
1903  if ([self encrypt])
1904    {
1905      if (theRecipient)
1906        {
1907          SOGoContactFolders *contactFolders;
1908
1909          contactFolders = [[[context activeUser] homeFolderInContext: context]
1910                                  lookupName: @"Contacts"
1911                                   inContext: context
1912                                     acquire: NO];
1913          certificate = [[contactFolders certificateForEmail: theRecipient] signersFromPKCS7];
1914        }
1915      else
1916        certificate =  [[self mailAccountFolder] certificate];
1917
1918      // We check if we have a valid certificate. We can have nil here coming from [[self mailAccountFolder] certificate].
1919      // This can happen if one sends an encrypted mail, but actually never uploaded
1920      // a PKCS#12 file to SOGo for his/her own usage and we're trying to save an encrypted
1921      // version of the message in the current user's Sent folder
1922      if (certificate)
1923        content = [content encryptUsingCertificate: certificate];
1924    }
1925
1926 finish_smime:
1927  // We got our mime part, let's add our mail headers
1928  hashMap = [self mimeHeaderMapWithHeaders: nil
1929                                 excluding: [NSArray arrayWithObjects: @"MIME-Version", @"Content-Type", @"Content-Transfer-Encoding", nil]];
1930  message = [NGMimeMessage messageWithHeader: hashMap];
1931  generator = [[[NGMimeMessageGenerator alloc] init] autorelease];
1932  d = [NSMutableData dataWithData: [generator generateMimeFromPart: message]];
1933  [d replaceBytesInRange: NSMakeRange([d length]-4, 4)
1934               withBytes: NULL
1935                  length: 0];
1936  [d appendData: content];
1937
1938  return d;
1939}
1940
1941//
1942//
1943//
1944- (NSArray *) allRecipients
1945{
1946  NSMutableArray *allRecipients;
1947  NSArray *recipients;
1948  NSString *fieldNames[] = {@"to", @"cc", @"bcc"};
1949  unsigned int count;
1950
1951  allRecipients = [NSMutableArray arrayWithCapacity: 16];
1952
1953  for (count = 0; count < 3; count++)
1954    {
1955      recipients = [headers objectForKey: fieldNames[count]];
1956      if ([recipients count] > 0)
1957        [allRecipients addObjectsFromArray: recipients];
1958    }
1959
1960  return allRecipients;
1961}
1962
1963//
1964//
1965//
1966- (NSArray *) allBareRecipients
1967{
1968  NSMutableArray *bareRecipients;
1969  NSEnumerator *allRecipients;
1970  NSString *recipient;
1971
1972  bareRecipients = [NSMutableArray array];
1973
1974  allRecipients = [[self allRecipients] objectEnumerator];
1975  while ((recipient = [allRecipients nextObject]))
1976    [bareRecipients addObject: [recipient pureEMailAddress]];
1977
1978  return bareRecipients;
1979}
1980
1981//
1982//
1983//
1984- (NSException *) sendMail
1985{
1986  NGMailAddress *parsedSender, *parsedRecipient;
1987  NGMailAddressParser *parser;
1988  NSArray *recipients;
1989  NSData *certificate;
1990  NSMutableArray *emails;
1991  NSString *recipient, *emailAddress;
1992  SOGoContactFolders *contactFolders;
1993  SOGoUserDefaults *ud;
1994  int i;
1995
1996  ud = [[context activeUser] userDefaults];
1997
1998  if ([self sign])
1999    {
2000      BIO *tbio = NULL;
2001      X509 *scert = NULL;
2002      STACK_OF(OPENSSL_STRING) *emlst;
2003      unsigned int len;
2004      const char* bytes;
2005
2006      certificate = [[self mailAccountFolder] certificate];
2007      if (!certificate)
2008        {
2009          // If we are trying to sign an email but we don't have a S/MIME certificate for that
2010          // IMAP account, we abort
2011          return [NSException exceptionWithHTTPStatus: 500 /* server error */
2012                                               reason: @"cannot sign email without certificate"];
2013        }
2014
2015      // Verify if the certificate contains the sender email
2016      bytes = [certificate bytes];
2017      len = [certificate length];
2018      tbio = BIO_new_mem_buf((void *)bytes, len);
2019      scert = PEM_read_bio_X509(tbio, NULL, 0, NULL);
2020
2021      if (!scert)
2022        {
2023          NSLog(@"FATAL: failed to read certificate for signing.");
2024          return [NSException exceptionWithHTTPStatus: 500 /* server error */
2025                                               reason: @"cannot sign message because the certificate can't be read"];
2026        }
2027
2028      emails = [NSMutableArray array];
2029      emlst = X509_get1_email(scert);
2030      for (i = 0; i < sk_OPENSSL_STRING_num(emlst); i++)
2031        [emails addObject: [[NSString stringWithUTF8String: sk_OPENSSL_STRING_value(emlst, i)] lowercaseString]];
2032      X509_email_free(emlst);
2033
2034      parser = [NGMailAddressParser mailAddressParserWithString: [self sender]];
2035      parsedSender = [parser parse];
2036      emailAddress = [parsedSender address];
2037
2038      if (![emails containsObject: emailAddress])
2039        {
2040          return [NSException exceptionWithHTTPStatus: 500 /* server error */
2041                                               reason: @"cannot sign message because the certificate doesn't include the specified sender address"];
2042        }
2043    }
2044
2045  // If we are encrypting emails, we must make sure that we have the certificate
2046  // for all recipients otherwise we cannot, of course, encrypt the email.
2047  if ([self encrypt])
2048    {
2049      NSData *certificate;
2050
2051      contactFolders = [[[context activeUser] homeFolderInContext: context]
2052                               lookupName: @"Contacts"
2053                                inContext: context
2054                                  acquire: NO];
2055      recipients = [self allBareRecipients];
2056      for (i = 0; i < [recipients count]; i++)
2057        {
2058          recipient = [recipients objectAtIndex: i];
2059
2060          if ([[context activeUser] hasEmail: recipient])
2061            certificate = [[self mailAccountFolder] certificate];
2062          else
2063            certificate = [contactFolders certificateForEmail: recipient];
2064
2065          if (!certificate)
2066            return [NSException exceptionWithHTTPStatus: 500 /* server error */
2067                                                 reason: @"cannot encrypt email without recipient certificate"];
2068
2069          [certificates setObject: certificate  forKey: recipient];
2070        }
2071    }
2072
2073  if ([ud mailAddOutgoingAddresses])
2074    {
2075      NSString *addressBook, *uid;
2076      NSArray *matchingContacts;
2077      SOGoContactGCSEntry *newContact;
2078      SOGoFolder <SOGoContactFolder> *folder;
2079      NGVCard *card;
2080
2081      // Get all the addressbooks
2082      contactFolders = [[[context activeUser] homeFolderInContext: context]
2083                         lookupName: @"Contacts"
2084                          inContext: context
2085                            acquire: NO];
2086      // Get the selected addressbook from the user preferences where the new address will be added
2087      addressBook = [ud selectedAddressBook];
2088      folder = [contactFolders lookupName: addressBook inContext: context  acquire: NO];
2089      // Get all the recipients from the current email
2090      recipients = [self allRecipients];
2091      for (i = 0; i < [recipients count]; i++)
2092        {
2093          // The address contains a string. ex: "John Doe <sogo1@exemple.com>"
2094          recipient = [recipients objectAtIndex: i];
2095          parser = [NGMailAddressParser mailAddressParserWithString: recipient];
2096          parsedRecipient = [parser parse];
2097          emailAddress = [parsedRecipient address];
2098
2099          matchingContacts = [contactFolders allContactsFromFilter: emailAddress
2100                                                     excludeGroups: YES
2101                                                      excludeLists: YES];
2102
2103          // If we don't get any results from the autocompletion code, we add it..
2104          if ([matchingContacts count] == 0)
2105            {
2106              uid = [folder globallyUniqueObjectId];
2107
2108              if (folder && uid)
2109                {
2110                  card = [NGVCard cardWithUid: uid];
2111                  [card addEmail: emailAddress types: nil];
2112                  [card setFn: [parsedRecipient displayName]];
2113
2114                  newContact = [SOGoContactGCSEntry objectWithName: uid  inContainer: folder];
2115                  [newContact setIsNew: YES];
2116                  [newContact saveComponent: card];
2117                }
2118            }
2119        }
2120    }
2121
2122  return [self sendMailAndCopyToSent: YES];
2123}
2124
2125//
2126//
2127//
2128- (NSException *) sendMailAndCopyToSent: (BOOL) copyToSent
2129{
2130  NSData *message, *messageForSent;
2131  SOGoMailFolder *sentFolder;
2132  SOGoDomainDefaults *dd;
2133  NSURL *sourceIMAP4URL;
2134  NSException *error;
2135
2136  dd = [[context activeUser] domainDefaults];
2137  messageForSent = nil;
2138
2139  // If we are encrypting mails, let's generate and
2140  // send them individually
2141  if ([self encrypt])
2142    {
2143      NSArray *recipients;
2144      NSString *recipient;
2145      int i;
2146
2147      recipients = [self allBareRecipients];
2148
2149      for (i = 0; i < [recipients count]; i++)
2150        {
2151          recipient = [recipients objectAtIndex: i];
2152
2153          if ([[context activeUser] hasEmail: recipient])
2154            message = messageForSent = [self mimeMessageForRecipient: nil];
2155          else
2156            message = [self mimeMessageForRecipient: recipient];
2157
2158          if (!message)
2159            return  [NSException exceptionWithHTTPStatus: 500
2160                                                  reason: @"could not generate message content"];
2161
2162          error = [[SOGoMailer mailerWithDomainDefaults: dd]
2163                    sendMailData: message
2164                    toRecipients: [NSArray arrayWithObject: recipient]
2165                          sender: [self sender]
2166                    withAuthenticator: [self authenticatorInContext: context]
2167                       inContext: context];
2168
2169          if (error)
2170            return error;
2171        }
2172
2173      // If the current user isn't part of the recipient list for encrypted emails
2174      // let's generate a crypted email for its sent folder.
2175      if (!messageForSent)
2176        messageForSent = [self mimeMessageForRecipient: nil];
2177    }
2178  else
2179    {
2180      // Encryption is done or not, if we didn't have to.
2181      message = messageForSent = [self mimeMessageForRecipient: nil];
2182
2183      if (!message)
2184        return  [NSException exceptionWithHTTPStatus: 500
2185                                              reason: @"could not generate message content"];
2186
2187      error = [[SOGoMailer mailerWithDomainDefaults: dd]
2188                  sendMailData: message
2189                  toRecipients: [self allBareRecipients]
2190                        sender: [self sender]
2191                withAuthenticator: [self authenticatorInContext: context]
2192                     inContext: context];
2193    }
2194
2195  if (!error && copyToSent)
2196    {
2197      sentFolder = [[self mailAccountFolder] sentFolderInContext: context];
2198      if ([sentFolder isKindOfClass: [NSException class]])
2199        error = (NSException *) sentFolder;
2200      else
2201        {
2202          error = [sentFolder postData: messageForSent flags: @"seen"];
2203          if (!error)
2204            {
2205              [self imap4Connection];
2206              if (IMAP4ID > -1 && ![dd mailKeepDraftsAfterSend])
2207                [imap4 markURLDeleted: [self imap4URL]];
2208              if (sourceURL && sourceFlag)
2209                {
2210                  sourceIMAP4URL = [NSURL URLWithString: sourceURL];
2211                  [imap4 addFlags: sourceFlag toURL: sourceIMAP4URL];
2212                }
2213            }
2214        }
2215    }
2216
2217  // Expunge Drafts mailbox if
2218  //  - message was sent and saved to Sent mailbox if necessary;
2219  //  - SOGoMailKeepDraftsAfterSend is not set;
2220  //  - draft is successfully deleted;
2221  //  - drafts mailbox exists.
2222  if (!error &&
2223      ![dd mailKeepDraftsAfterSend] &&
2224      ![self delete] &&
2225      [imap4 doesMailboxExistAtURL: [container imap4URL]])
2226    [(SOGoDraftsFolder *) container expunge];
2227
2228  return error;
2229}
2230
2231- (NSException *) delete
2232{
2233  NSException *error;
2234
2235  if ([[NSFileManager defaultManager]
2236	removeFileAtPath: [self draftFolderPath]
2237                 handler: nil])
2238    error = nil;
2239  else
2240    error = [NSException exceptionWithHTTPStatus: 500 /* server error */
2241                                          reason: @"could not delete draft"];
2242
2243  return error;
2244}
2245
2246/* operations */
2247
2248- (NSString *) contentAsString
2249{
2250  NSString *str;
2251  NSData *message;
2252
2253  message = [self mimeMessageForRecipient: nil];
2254  if (message)
2255    {
2256      str = [[NSString alloc] initWithData: message
2257                                  encoding: NSUTF8StringEncoding];
2258      if (!str)
2259	[self errorWithFormat: @"could not load draft as UTF-8 (data size=%d)",
2260	      [message length]];
2261      else
2262	[str autorelease];
2263    }
2264  else
2265    {
2266      [self errorWithFormat: @"message data is empty"];
2267      str = nil;
2268    }
2269
2270  return str;
2271}
2272
2273@end /* SOGoDraftObject */
2274