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 ? @" " : @" "); 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