1/* 2 Copyright (C) 2007-2017 Inverse inc. 3 Copyright (C) 2004-2005 SKYRIX Software AG 4 5 This file is part of SOGo. 6 7 SOGo is free software; you can redistribute it and/or modify it under 8 the terms of the GNU Lesser General Public License as published by the 9 Free Software Foundation; either version 2, or (at your option) any 10 later version. 11 12 SOGo is distributed in the hope that it will be useful, but WITHOUT ANY 13 WARRANTY; without even the implied warranty of MERCHANTABILITY or 14 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 15 License for more details. 16 17 You should have received a copy of the GNU Lesser General Public 18 License along with OGo; see the file COPYING. If not, write to the 19 Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA 20 02111-1307, USA. 21*/ 22 23#import <Foundation/NSURL.h> 24#import <Foundation/NSValue.h> 25#import <Foundation/NSFileHandle.h> 26 27#import <NGObjWeb/WOContext+SoObjects.h> 28#import <NGObjWeb/WORequest.h> 29#import <NGObjWeb/NSException+HTTP.h> 30#import <NGExtensions/NGHashMap.h> 31#import <NGExtensions/NSFileManager+Extensions.h> 32#import <NGExtensions/NSNull+misc.h> 33#import <NGExtensions/NSObject+Logs.h> 34#import <NGExtensions/NSString+Encoding.h> 35#import <NGExtensions/NSString+misc.h> 36#import <NGImap4/NGImap4Connection.h> 37#import <NGImap4/NGImap4Envelope.h> 38#import <NGImap4/NGImap4EnvelopeAddress.h> 39#import <NGMail/NGMimeMessageParser.h> 40#import <NGMime/NGMimeMultipartBody.h> 41#import <NGMime/NGMimeType.h> 42 43 44#import <SOGo/NSArray+Utilities.h> 45#import <SOGo/NSDictionary+Utilities.h> 46#import <SOGo/NSString+Utilities.h> 47#import <SOGo/SOGoPermissions.h> 48#import <SOGo/SOGoSystemDefaults.h> 49#import <SOGo/SOGoUser.h> 50#import <SOGo/SOGoUserDefaults.h> 51#import <SOGo/NSCalendarDate+SOGo.h> 52#import <SOGo/SOGoZipArchiver.h> 53 54#import "NSString+Mail.h" 55#import "NSData+Mail.h" 56#import "NSData+SMIME.h" 57#import "NSDictionary+Mail.h" 58#import "SOGoMailAccount.h" 59#import "SOGoMailFolder.h" 60#import "SOGoMailManager.h" 61#import "SOGoMailBodyPart.h" 62 63#import "SOGoMailObject.h" 64 65@implementation SOGoMailObject 66 67NSArray *SOGoMailCoreInfoKeys = nil; 68static NSString *mailETag = nil; 69static BOOL heavyDebug = NO; 70static BOOL debugOn = NO; 71static BOOL debugBodyStructure = NO; 72static BOOL debugSoParts = NO; 73 74+ (void) initialize 75{ 76 if (!SOGoMailCoreInfoKeys) 77 { 78 /* Note: see SOGoMailManager.m for allowed IMAP4 keys */ 79 SOGoMailCoreInfoKeys 80 = [[NSArray alloc] initWithObjects: 81 @"FLAGS", @"ENVELOPE", @"BODYSTRUCTURE", 82 @"RFC822.SIZE", 83 @"RFC822.HEADER", 84 // not yet supported: @"INTERNALDATE", 85 nil]; 86 87 /* The following disabled code should not be needed, except if we use 88 annotations (see davEntityTag below) */ 89 // if (![[ud objectForKey: @"SOGoMailDisableETag"] boolValue]) { 90 mailETag = [[NSString alloc] initWithFormat: @"\"imap4url_%@_%@_%@\"", 91 UIX_MAILER_MAJOR_VERSION, 92 UIX_MAILER_MINOR_VERSION, 93 UIX_MAILER_SUBMINOR_VERSION]; 94 } 95} 96 97- (id) init 98{ 99 if ((self = [super init])) 100 { 101 headers = nil; 102 headerPart = nil; 103 coreInfos = nil; 104 } 105 106 return self; 107} 108 109- (void) dealloc 110{ 111 [headers release]; 112 [headerPart release]; 113 [coreInfos release]; 114 [super dealloc]; 115} 116 117/* IMAP4 */ 118 119- (NSString *) relativeImap4Name 120{ 121 return [nameInContainer stringByDeletingPathExtension]; 122} 123 124/* hierarchy */ 125 126- (SOGoMailObject *) mailObject 127{ 128 return self; 129} 130 131/* part hierarchy */ 132 133- (NSString *) keyExtensionForPart: (id) _partInfo 134{ 135 NSString *mt, *st; 136 137 if (_partInfo == nil) 138 return nil; 139 140 mt = [_partInfo valueForKey: @"type"]; 141 st = [[_partInfo valueForKey: @"subtype"] lowercaseString]; 142 if ([mt isEqualToString: @"text"]) { 143 if ([st isEqualToString: @"plain"]) return @".txt"; 144 if ([st isEqualToString: @"html"]) return @".html"; 145 if ([st isEqualToString: @"calendar"]) return @".ics"; 146 if ([st isEqualToString: @"x-vcard"]) return @".vcf"; 147 } 148 else if ([mt isEqualToString: @"image"]) 149 return [@"." stringByAppendingString:st]; 150 else if ([mt isEqualToString: @"application"]) { 151 if ([st isEqualToString: @"pgp-signature"]) 152 return @".asc"; 153 } 154 155 return nil; 156} 157 158- (NSArray *)relationshipKeysWithParts:(BOOL)_withParts { 159 /* should return non-multipart children */ 160 NSMutableArray *ma; 161 NSArray *parts; 162 unsigned i, count; 163 164 parts = [[self bodyStructure] valueForKey: @"parts"]; 165 if (![parts isNotNull]) 166 return nil; 167 if ((count = [parts count]) == 0) 168 return nil; 169 170 for (i = 0, ma = nil; i < count; i++) { 171 NSString *key, *ext; 172 id part; 173 BOOL hasParts; 174 175 part = [parts objectAtIndex:i]; 176 hasParts = [part valueForKey: @"parts"] != nil ? YES:NO; 177 if ((hasParts && !_withParts) || (_withParts && !hasParts)) 178 continue; 179 180 if (ma == nil) 181 ma = [NSMutableArray arrayWithCapacity:count - i]; 182 183 ext = [self keyExtensionForPart:part]; 184 key = [[NSString alloc] initWithFormat: @"%d%@", i + 1, ((id)ext?(id)ext: (id)@"")]; 185 [ma addObject:key]; 186 [key release]; 187 } 188 return ma; 189} 190 191- (NSArray *) toOneRelationshipKeys 192{ 193 return [self relationshipKeysWithParts:NO]; 194} 195 196- (NSArray *) toManyRelationshipKeys 197{ 198 return [self relationshipKeysWithParts:YES]; 199} 200 201/* message */ 202 203- (id) fetchParts: (NSArray *) _parts 204{ 205 // TODO: explain what it does 206 /* 207 Called by -fetchPlainTextParts: 208 */ 209 return [[self imap4Connection] fetchURL: [self imap4URL] parts:_parts]; 210} 211 212/* core infos */ 213 214- (BOOL) doesMailExist 215{ 216 static NSArray *existsKey = nil; 217 id msgs; 218 219 if (coreInfos != nil) /* if we have coreinfos, we can use them */ 220 return [coreInfos isNotNull]; 221 222 /* otherwise fetch something really simple */ 223 224 if (existsKey == nil) /* we use size, other suggestions? */ 225 existsKey = [[NSArray alloc] initWithObjects: @"RFC822.SIZE", nil]; 226 227 msgs = [self fetchParts:existsKey]; // returns dict 228 msgs = [msgs valueForKey: @"fetch"]; 229 return [msgs count] > 0 ? YES : NO; 230} 231 232- (id) fetchCoreInfos 233{ 234 id msgs; 235 int i; 236 237 if (!coreInfos) 238 { 239 msgs = [self fetchParts: SOGoMailCoreInfoKeys]; // returns dict 240 if (heavyDebug) 241 [self logWithFormat: @"M: %@", msgs]; 242 msgs = [msgs valueForKey: @"fetch"]; 243 244 // We MUST honor untagged IMAP responses here otherwise we could 245 // return really borken and nasty results. 246 if ([msgs count] > 0) 247 { 248 for (i = 0; i < [msgs count]; i++) 249 { 250 coreInfos = [msgs objectAtIndex: i]; 251 252 if ([[coreInfos objectForKey: @"uid"] intValue] == [[self nameInContainer] intValue]) 253 break; 254 255 coreInfos = nil; 256 } 257 } 258 [coreInfos retain]; 259 } 260 261 return coreInfos; 262} 263 264- (void) setCoreInfos: (NSDictionary *) newCoreInfos 265{ 266 ASSIGN (coreInfos, newCoreInfos); 267} 268 269- (id) bodyStructure 270{ 271 id bodyStructure; 272 273 bodyStructure = [[self fetchCoreInfos] valueForKey: @"bodystructure"]; 274 if (debugBodyStructure) 275 [self logWithFormat: @"BODYSTRUCTURE: %@", bodyStructure]; 276 277 return bodyStructure; 278} 279 280- (NGImap4Envelope *) envelope 281{ 282 return [[self fetchCoreInfos] valueForKey: @"envelope"]; 283} 284 285- (NSString *) subject 286{ 287 return [[self envelope] subject]; 288} 289 290- (NSString *) displayName 291{ 292 return [self decodedSubject]; 293} 294 295- (NSString *) decodedSubject 296{ 297 return [[self subject] decodedHeader]; 298} 299 300- (NSCalendarDate *) date 301{ 302 SOGoUserDefaults *ud; 303 NSCalendarDate *date; 304 305 ud = [[context activeUser] userDefaults]; 306 date = [[self envelope] date]; 307 [date setTimeZone: [ud timeZone]]; 308 309 return date; 310} 311 312- (NSArray *) fromEnvelopeAddresses 313{ 314 return [[self envelope] from]; 315} 316 317- (NSArray *) toEnvelopeAddresses 318{ 319 return [[self envelope] to]; 320} 321 322- (NSArray *) ccEnvelopeAddresses 323{ 324 return [[self envelope] cc]; 325} 326 327- (NSArray *) bccEnvelopeAddresses 328{ 329 return [[self envelope] bcc]; 330} 331 332- (NSArray *) replyToEnvelopeAddresses 333{ 334 return [[self envelope] replyTo]; 335} 336 337- (NSData *) mailHeaderData 338{ 339 return [[self fetchCoreInfos] valueForKey: @"header"]; 340} 341 342- (id) mailHeaderPart 343{ 344 NGMimeMessageParser *parser; 345 NSData *data; 346 347 if (headerPart != nil) 348 return [headerPart isNotNull] ? headerPart : nil; 349 350 if ([(data = [self mailHeaderData]) length] == 0) 351 return nil; 352 353 // TODO: do we need to set some delegate method which stops parsing the body? 354 parser = [[NGMimeMessageParser alloc] init]; 355 headerPart = [[parser parsePartFromData: data] retain]; 356 [parser release]; parser = nil; 357 358 if (headerPart == nil) { 359 headerPart = [[NSNull null] retain]; 360 return nil; 361 } 362 return headerPart; 363} 364 365- (NSDictionary *) mailHeaders 366{ 367 if (!headers) 368 headers = [[[self mailHeaderPart] headers] copy]; 369 370 return headers; 371} 372 373- (id) lookupInfoForBodyPart: (id) _path 374{ 375 NSEnumerator *pe; 376 NSString *p; 377 id info; 378 379 if (![_path isNotNull]) 380 return nil; 381 382 if ((info = [self bodyStructure]) == nil) { 383 [self errorWithFormat: @"got no body part structure!"]; 384 return nil; 385 } 386 387 /* ensure array argument */ 388 389 if ([_path isKindOfClass:[NSString class]]) { 390 if ([_path length] == 0 || [_path isEqualToString: @"text"]) 391 return info; 392 393 _path = [_path componentsSeparatedByString: @"."]; 394 } 395 396 // deal with mails of type text/calendar 397 if ([[[info valueForKey: @"type"] lowercaseString] isEqualToString: @"text"] && 398 [[[info valueForKey: @"subtype"] lowercaseString] isEqualToString: @"calendar"]) 399 return info; 400 401 // deal with mails that contain only an attachment, for example: 402 // application/pkcs7-mime 403 // application/pdf 404 // etc. 405 if ([[[info valueForKey: @"type"] lowercaseString] isEqualToString: @"application"] || 406 [[[info valueForKey: @"type"] lowercaseString] isEqualToString: @"audio"]) 407 return info; 408 409 /* 410 For each path component, eg 1,1,3 411 412 Remember that we need special processing for message/rfc822 which maps the 413 namespace of multiparts directly into the main namespace. 414 415 TODO(hh): no I don't remember, please explain in more detail! 416 */ 417 pe = [_path objectEnumerator]; 418 while ((p = [pe nextObject]) != nil && [info isNotNull]) { 419 unsigned idx; 420 NSArray *parts; 421 NSString *mt; 422 423 [self debugWithFormat: @"check PATH: %@", p]; 424 idx = [p intValue] - 1; 425 426 parts = [info valueForKey: @"parts"]; 427 mt = [[info valueForKey: @"type"] lowercaseString]; 428 if ([mt isEqualToString: @"message"]) { 429 /* we have special behaviour for message types */ 430 id body; 431 432 if ((body = [info valueForKey: @"body"]) != nil) { 433 mt = [body valueForKey: @"type"]; 434 if ([mt isEqualToString: @"multipart"]) 435 parts = [body valueForKey: @"parts"]; 436 else 437 parts = [NSArray arrayWithObject:body]; 438 } 439 } 440 441 if (idx >= [parts count]) { 442 [self errorWithFormat: 443 @"body part index out of bounds(idx=%d vs count=%d): %@", 444 (idx + 1), [parts count], info]; 445 return nil; 446 } 447 info = [parts objectAtIndex:idx]; 448 } 449 return [info isNotNull] ? info : nil; 450} 451 452/* content */ 453 454- (NSData *) content 455{ 456 NSData *content; 457 id result, fullResult; 458 459 // We avoid using RFC822 here as the part name as it'll flag the message as Seen 460 fullResult = [self fetchParts: [NSArray arrayWithObject: @"BODY.PEEK[]"]]; 461 if (fullResult == nil) 462 return nil; 463 464 if ([fullResult isKindOfClass: [NSException class]]) 465 return fullResult; 466 467 /* extract fetch result */ 468 469 result = [fullResult valueForKey: @"fetch"]; 470 if (![result isKindOfClass:[NSArray class]]) { 471 [self logWithFormat: 472 @"ERROR: unexpected IMAP4 result (missing 'fetch'): %@", 473 fullResult]; 474 return [NSException exceptionWithHTTPStatus:500 /* server error */ 475 reason: @"unexpected IMAP4 result"]; 476 } 477 if ([result count] == 0) 478 return nil; 479 480 result = [result objectAtIndex:0]; 481 482 /* extract message */ 483 484 if ((content = [[result valueForKey: @"body[]"] valueForKey: @"data"]) == nil) { 485 [self logWithFormat: 486 @"ERROR: unexpected IMAP4 result (missing 'message'): %@", 487 result]; 488 return [NSException exceptionWithHTTPStatus:500 /* server error */ 489 reason: @"unexpected IMAP4 result"]; 490 } 491 492 return [[content copy] autorelease]; 493} 494 495- (NSString *) davContentType 496{ 497 return @"message/rfc822"; 498} 499 500- (NSString *) contentAsString 501{ 502 id s; 503 NSData *content; 504 505 content = [self content]; 506 if (content) 507 { 508 if ([content isKindOfClass: [NSData class]]) 509 { 510#warning we ignore the charset here? 511 s = [[NSString alloc] initWithData: content 512 encoding: NSISOLatin1StringEncoding]; 513 if (s) 514 [s autorelease]; 515 else 516 [self logWithFormat: 517 @"ERROR: could not convert data of length %d to string", 518 [content length]]; 519 } 520 else 521 s = content; 522 } 523 else 524 s = nil; 525 526 return s; 527} 528 529/* This is defined before the public version without parentMimeType 530 argument to be able to call it recursively */ 531/* bulk fetching of plain/text content */ 532- (void) addRequiredKeysOfStructure: (NSDictionary *) info 533 path: (NSString *) p 534 toArray: (NSMutableArray *) keys 535 acceptedTypes: (NSArray *) types 536 withPeek: (BOOL) withPeek 537 parentMultipart: (NSString *) parentMPart 538{ 539 /* 540 This is used to collect the set of IMAP4 fetch-keys required to fetch 541 the basic parts of the body structure. That is, to fetch all parts which 542 are displayed 'inline' in a single IMAP4 fetch. 543 544 The method calls itself recursively to walk the body structure. 545 */ 546 NSArray *parts; 547 unsigned i, count; 548 NSString *k; 549 id body; 550 NSString *bodyToken, *sp, *mimeType; 551 id childInfo; 552 NSString *multipart; 553 554 bodyToken = (withPeek ? @"body.peek" : @"body"); 555 556 mimeType = [[NSString stringWithFormat: @"%@/%@", 557 [info valueForKey: @"type"], 558 [info valueForKey: @"subtype"]] 559 lowercaseString]; 560 561 if ([[info valueForKey: @"type"] isEqualToString: @"multipart"]) 562 multipart = mimeType; 563 else 564 multipart = parentMPart; 565 566 if ([types containsObject: mimeType]) 567 { 568 if ([p length] > 0) 569 k = [NSString stringWithFormat: @"%@[%@]", bodyToken, p]; 570 else 571 { 572 /* 573 for some reason we need to add ".TEXT" for plain text stuff on root 574 entities? 575 TODO: check with HTML 576 */ 577 k = [NSString stringWithFormat: @"%@[text]", bodyToken]; 578 } 579 [keys addObject: [NSDictionary dictionaryWithObjectsAndKeys: k, @"key", 580 mimeType, @"mimeType", 581 multipart, @"multipart", nil]]; 582 } 583 584 parts = [info objectForKey: @"parts"]; 585 count = [parts count]; 586 for (i = 0; i < count; i++) 587 { 588 sp = (([p length] > 0) 589 ? (id)[p stringByAppendingFormat: @".%d", i + 1] 590 : (id)[NSString stringWithFormat: @"%d", i + 1]); 591 592 childInfo = [parts objectAtIndex: i]; 593 594 [self addRequiredKeysOfStructure: childInfo 595 path: sp 596 toArray: keys 597 acceptedTypes: types 598 withPeek: withPeek 599 parentMultipart: multipart]; 600 } 601 602 /* check body */ 603 body = [info objectForKey: @"body"]; 604 if (body) 605 { 606 /* FIXME: this seems to generate bad mime part keys, which triggers a 607 exceptions such as this: 608 609 ERROR(-[NGImap4Client _processCommandParserException:]): catched 610 IMAP4 parser exception NGImap4ParserException: unsupported fetch key: 611 nil) 612 613 Do we really need to assign p to sp in a multipart body part? Or do 614 we need to do this only when the part in question is the first one in 615 the message? */ 616 617 sp = [[body valueForKey: @"type"] lowercaseString]; 618 if ([sp isEqualToString: @"multipart"]) 619 sp = p; 620 else 621 sp = [p length] > 0 ? (id)[p stringByAppendingString: @".1"] : (id)@"1"; 622 [self addRequiredKeysOfStructure: body 623 path: sp 624 toArray: keys 625 acceptedTypes: types 626 withPeek: withPeek 627 parentMultipart: multipart]; 628 } 629} 630 631- (void) addRequiredKeysOfStructure: (NSDictionary *) info 632 path: (NSString *) p 633 toArray: (NSMutableArray *) keys 634 acceptedTypes: (NSArray *) types 635 withPeek: (BOOL) withPeek 636{ 637 [self addRequiredKeysOfStructure: (NSDictionary *) info 638 path: (NSString *) p 639 toArray: (NSMutableArray *) keys 640 acceptedTypes: (NSArray *) types 641 withPeek: (BOOL) withPeek 642 parentMultipart: @""]; 643} 644 645- (NSArray *) plainTextContentFetchKeys 646{ 647 /* 648 The name is not 100% correct. The method returns all body structure fetch 649 keys which are marked by the -shouldFetchPartOfType:subtype: method. 650 */ 651 NSMutableArray *ma; 652 NSArray *types; 653 654 types = [NSArray arrayWithObjects: @"text/plain", @"text/html", 655 @"text/calendar", @"application/ics", 656 @"application/pgp-signature", nil]; 657 ma = [NSMutableArray arrayWithCapacity: 4]; 658 659 [self addRequiredKeysOfStructure: [self bodyStructure] 660 path: @"" 661 toArray: ma 662 acceptedTypes: types 663 withPeek: YES]; 664 665 return ma; 666} 667 668- (NSDictionary *) fetchPlainTextParts: (NSArray *) _fetchKeys 669{ 670 // TODO: is the name correct or does it also fetch other parts? 671 NSMutableDictionary *flatContents; 672 unsigned i, count; 673 NSArray *results; 674 id result; 675 676 [self debugWithFormat: @"fetch keys: %@", _fetchKeys]; 677 678 result = [self fetchParts: [_fetchKeys objectsForKey: @"key" 679 notFoundMarker: nil]]; 680 result = [result valueForKey: @"RawResponse"]; // hackish 681 682 // Note: -valueForKey: doesn't work! 683 results = [(NGHashMap *)result objectsForKey: @"fetch"]; 684 result = [results flattenedDictionaries]; 685 686 count = [_fetchKeys count]; 687 flatContents = [NSMutableDictionary dictionaryWithCapacity:count]; 688 for (i = 0; i < count; i++) { 689 NSString *key; 690 NSData *data; 691 692 key = [[_fetchKeys objectAtIndex:i] objectForKey: @"key"]; 693 694 // We'll ask for the body.peek[] but SOPE returns us body[] responses 695 // so the key won't ever be found. 696 if ([key hasPrefix: @"body.peek["]) 697 key = [NSString stringWithFormat: @"body[%@", [key substringFromIndex: 10]]; 698 699 data = [(NSDictionary *)[(NSDictionary *)result objectForKey:key] 700 objectForKey: @"data"]; 701 702 if (![data isNotNull]) { 703 [self errorWithFormat: @"got no data for key: %@", key]; 704 continue; 705 } 706 707 if ([key isEqualToString: @"body[text]"]) 708 key = @""; // see key collector for explanation (TODO: where?) 709 else if ([key hasPrefix: @"body["]) { 710 NSRange r; 711 712 key = [key substringFromIndex:5]; 713 r = [key rangeOfString: @"]"]; 714 if (r.length > 0) 715 key = [key substringToIndex:r.location]; 716 } 717 [flatContents setObject:data forKey:key]; 718 } 719 return flatContents; 720} 721 722- (NSDictionary *) fetchPlainTextParts 723{ 724 return [self fetchPlainTextParts: [self plainTextContentFetchKeys]]; 725} 726 727- (NSString *) _urlToPart: (NSDictionary *) infos 728 withPrefix: (NSString *) urlPrefix 729{ 730 NSDictionary *parameters; 731 NSString *urlToPart, *filename; 732 733 parameters = [infos objectForKey: @"parameterList"]; 734 filename = [parameters objectForKey: @"name"]; 735 if (!filename) 736 { 737 parameters = [[infos objectForKey: @"disposition"] 738 objectForKey: @"parameterList"]; 739 filename = [parameters objectForKey: @"filename"]; 740 } 741 742 if ([filename length]) 743 urlToPart = [NSString stringWithFormat: @"%@/%@", urlPrefix, filename]; 744 else 745 urlToPart = urlPrefix; 746 747 return urlToPart; 748} 749 750- (void) _feedFileAttachmentIds: (NSMutableDictionary *) attachmentIds 751 withInfos: (NSDictionary *) infos 752 andPrefix: (NSString *) prefix 753{ 754 NSArray *parts; 755 NSDictionary *currentPart; 756 unsigned int count, max; 757 NSString *url, *cid; 758 759 cid = [infos objectForKey: @"bodyId"]; 760 if ([cid length]) 761 { 762 url = [self _urlToPart: infos withPrefix: prefix]; 763 if (url) 764 [attachmentIds setObject: url forKey: cid]; 765 } 766 767 parts = [infos objectForKey: @"parts"]; 768 max = [parts count]; 769 for (count = 0; count < max; count++) 770 { 771 currentPart = [parts objectAtIndex: count]; 772 [self _feedFileAttachmentIds: attachmentIds 773 withInfos: currentPart 774 andPrefix: [NSString stringWithFormat: @"%@/%d", 775 prefix, count + 1]]; 776 } 777} 778 779- (NSDictionary *) fetchFileAttachmentIds 780{ 781 NSMutableDictionary *attachmentIds; 782 NSString *prefix; 783 784 attachmentIds = [NSMutableDictionary dictionary]; 785 786 [self fetchCoreInfos]; 787 prefix = [[self soURL] absoluteString]; 788 if ([prefix hasSuffix: @"/"]) 789 prefix = [prefix substringToIndex: [prefix length] - 1]; 790 [self _feedFileAttachmentIds: attachmentIds 791 withInfos: [coreInfos objectForKey: @"bodystructure"] 792 andPrefix: prefix]; 793 794 return attachmentIds; 795} 796 797// 798// 799// 800- (void) _fetchFileAttachmentKey: (NSDictionary *) part 801 intoArray: (NSMutableArray *) keys 802 withPath: (NSString *) path 803 andPrefix: (NSString *) prefix 804{ 805 NSString *filename, *mimeType, *filenameURL; 806 NSDictionary *currentFile; 807 808 filename = [part filename]; 809 810 mimeType = [NSString stringWithFormat: @"%@/%@", 811 [part objectForKey: @"type"], 812 [part objectForKey: @"subtype"]]; 813 814 if (!filename) 815 { 816 filename = [mimeType asPreferredFilenameUsingPath: path]; 817 } 818 819 if (filename) 820 { 821 // We replace any slash by a dash since Apache won't allow encoded slashes by default. 822 // See http://httpd.apache.org/docs/2.2/mod/core.html#allowencodedslashes 823 // See [UIxMailPartViewer _filenameForAttachment:] 824 filenameURL = [[filename stringByReplacingString: @"/" withString: @"-"] stringByEscapingURL]; 825 currentFile = [NSDictionary dictionaryWithObjectsAndKeys: 826 [filename stringByUnescapingURL], @"filename", 827 [mimeType lowercaseString], @"mimetype", 828 path, @"path", 829 [part objectForKey: @"encoding"], @"encoding", 830 [part objectForKey:@ "size"], @"size", 831 [part objectForKey: @"bodyId"], @"bodyId", 832 [NSString stringWithFormat: @"%@/%@", prefix, filenameURL], @"url", 833 [NSString stringWithFormat: @"%@/asAttachment/%@", prefix, filenameURL], @"urlAsAttachment", 834 nil]; 835 [keys addObject: currentFile]; 836 } 837} 838 839// 840// 841// 842- (void) _fetchFileAttachmentKeysInPart: (NSDictionary *) part 843 intoArray: (NSMutableArray *) keys 844 withPath: (NSString *) path 845 andPrefix: (NSString *) prefix 846{ 847 NSMutableDictionary *currentPart; 848 NSString *newPath; 849 NSArray *subparts; 850 NSString *type, *subtype; 851 NSUInteger i; 852 853 type = [[part objectForKey: @"type"] lowercaseString]; 854 if ([type isEqualToString: @"multipart"]) 855 { 856 subparts = [part objectForKey: @"parts"]; 857 for (i = 1; i <= [subparts count]; i++) 858 { 859 currentPart = [subparts objectAtIndex: i-1]; 860 if (path) 861 newPath = [NSString stringWithFormat: @"%@.%d", path, (int)i]; 862 else 863 newPath = [NSString stringWithFormat: @"%d", (int)i]; 864 [self _fetchFileAttachmentKeysInPart: currentPart 865 intoArray: keys 866 withPath: newPath 867 andPrefix: [NSString stringWithFormat: @"%@/%i", prefix, (int)i]]; 868 } 869 } 870 else 871 { 872 if (!path) 873 { 874 path = @"1"; 875 876 // We set the path to 0 in case of a S/MIME mail if not provided. 877 subtype = [[part objectForKey: @"subtype"] lowercaseString]; 878 if ([subtype isEqualToString: @"pkcs7-mime"] || [subtype isEqualToString: @"x-pkcs7-mime"]) 879 path = @"0"; 880 } 881 882 [self _fetchFileAttachmentKey: part 883 intoArray: keys 884 withPath: path 885 andPrefix: prefix]; 886 } 887} 888 889// 890// 891// 892#warning we might need to handle parts with a "name" attribute 893- (NSArray *) fetchFileAttachmentKeys 894{ 895 NSString *prefix; 896 NSMutableArray *keys; 897 898 prefix = [[self soURL] absoluteString]; 899 if ([prefix hasSuffix: @"/"]) 900 prefix = [prefix substringToIndex: [prefix length] - 1]; 901 902 keys = [NSMutableArray array]; 903 [self _fetchFileAttachmentKeysInPart: [self bodyStructure] 904 intoArray: keys 905 withPath: nil 906 andPrefix: prefix]; 907 908 return keys; 909} 910 911/** 912 * Returns an array of dictionaries with the following keys: 913 * - encoding 914 * - filename 915 * - mimetype 916 * - path 917 * - size 918 * - url 919 * - urlAsAttachment 920 * - body (NSData) 921 */ 922- (NSArray *) fetchFileAttachments 923{ 924 unsigned int count, max; 925 NGHashMap *response; 926 NSArray *parts, *paths; //, *bodies; 927 NSData *body; 928 NSDictionary *fetch, *currentInfo, *currentBody; 929 NSMutableArray *attachments; 930 NSMutableDictionary *currentAttachment; 931 NSString *currentPath; 932 933 parts = [self fetchFileAttachmentKeys]; 934 max = [parts count]; 935 attachments = [NSMutableArray arrayWithCapacity: max]; 936 if (max > 0) 937 { 938 paths = [parts keysWithFormat: @"BODY[%{path}]"]; 939 response = [[self fetchParts: paths] objectForKey: @"RawResponse"]; 940 fetch = [response objectForKey: @"fetch"]; 941 for (count = 0; count < max; count++) 942 { 943 currentInfo = [parts objectAtIndex: count]; 944 currentPath = [[paths objectAtIndex: count] lowercaseString]; 945 currentBody = [fetch objectForKey: currentPath]; 946 947 if (currentBody) 948 { 949 body = [currentBody objectForKey: @"data"]; 950 body = [body bodyDataFromEncoding: [currentInfo objectForKey: @"encoding"]]; 951 } 952 else 953 body = [NSData data]; 954 955 currentAttachment = [NSMutableDictionary dictionaryWithDictionary: currentInfo]; 956 [currentAttachment setObject: body forKey: @"body"]; 957 [attachments addObject: currentAttachment]; 958 } 959 } 960 961 return attachments; 962} 963 964- (WOResponse *) archiveAllFilesinArchiveNamed: (NSString *) archiveName 965{ 966#warning duplicated code from [SOGoMailFolder archiveUIDs] 967 NSArray *attachments; 968 NSData *body, *zipContent; 969 NSDictionary *currentAttachment; 970 NSException *error; 971 NSFileManager *fm; 972 NSString *spoolPath, *name, *baseName, *extension, *zipPath, *qpFileName; 973 SOGoMailFolder *folder; 974 WOResponse *response; 975 unsigned int max, count; 976 SOGoZipArchiver *archiver; 977 NSFileHandle *zipFileHandle;; 978 979 if (!archiveName) 980 archiveName = @"attachments.zip"; 981 982 folder = [self container]; 983 spoolPath = [folder userSpoolFolderPath]; 984 985 if (![folder ensureSpoolFolderPath]) 986 { 987 [self errorWithFormat: @"spool directory '%@' doesn't exist", spoolPath]; 988 error = [NSException exceptionWithHTTPStatus: 500 989 reason: @"spool directory does not exist"]; 990 return (WOResponse *)error; 991 } 992 993 fm = [NSFileManager defaultManager]; 994 zipPath = [NSString stringWithFormat: @"%@/%@", spoolPath, archiveName]; 995 archiver = [SOGoZipArchiver archiverAtPath: zipPath]; 996 if (archiver == nil) { 997 [self errorWithFormat: @"Failed to create zip archive at %@", spoolPath]; 998 error = [NSException exceptionWithHTTPStatus: 500 999 reason: @"Internal server error"]; 1000 return (WOResponse *)error; 1001 } 1002 1003 // Fetch attachments and write them on disk 1004 attachments = [self fetchFileAttachments]; 1005 max = [attachments count]; 1006 for (count = 0; count < max; count++) 1007 { 1008 currentAttachment = [attachments objectAtIndex: count]; 1009 body = [currentAttachment objectForKey: @"body"]; 1010 name = [[currentAttachment objectForKey: @"filename"] asSafeFilename]; 1011 [archiver putFileWithName: name andData: body]; 1012 } 1013 1014 [archiver close]; 1015 1016 response = [context response]; 1017 1018 // Check if SOPE has support for serving files directly 1019 if ([response respondsToSelector: @selector(setContentFile:)]) { 1020 zipFileHandle = [NSFileHandle fileHandleForReadingAtPath: zipPath]; 1021 [response setContentFile: zipFileHandle]; 1022 } else { 1023 zipContent = [[NSData alloc] initWithContentsOfFile:zipPath]; 1024 [response setContent:zipContent]; 1025 [zipContent release]; 1026 } 1027 1028 [fm removeFileAtPath: zipPath handler: nil]; 1029 1030 baseName = [archiveName stringByDeletingPathExtension]; 1031 extension = [archiveName pathExtension]; 1032 if ([extension length] > 0) 1033 extension = [@"." stringByAppendingString: extension]; 1034 else 1035 extension = @""; 1036 1037 qpFileName = [NSString stringWithFormat: @"%@%@", 1038 [baseName asQPSubjectString: @"utf-8"], extension]; 1039 [response setHeader: [NSString stringWithFormat: @"application/zip;" 1040 @" name=\"%@\"", qpFileName] 1041 forKey: @"content-type"]; 1042 [response setHeader: [NSString stringWithFormat: @"attachment; filename=\"%@\"", 1043 qpFileName] 1044 forKey: @"Content-Disposition"]; 1045 1046 return response; 1047} 1048 1049/* convert parts to strings */ 1050- (NSString *) stringForData: (NSData *) _data 1051 partInfo: (NSDictionary *) _info 1052{ 1053 NSString *charset, *s; 1054 NSData *mailData; 1055 1056 if ([_data isNotNull]) 1057 { 1058 mailData 1059 = [_data bodyDataFromEncoding: [_info objectForKey: @"encoding"]]; 1060 1061 charset = [[_info valueForKey: @"parameterList"] valueForKey: @"charset"]; 1062 if (![charset length]) 1063 { 1064 s = nil; 1065 } 1066 else 1067 { 1068 s = [NSString stringWithData: mailData usingEncodingNamed: charset]; 1069 } 1070 1071 // If it has failed, we try at least using UTF-8. Normally, this can NOT fail. 1072 // Unfortunately, it seems to fail under GNUstep so we try latin1 if that's 1073 // the case 1074 if (!s) 1075 s = [[[NSString alloc] initWithData: mailData encoding: NSUTF8StringEncoding] autorelease]; 1076 1077 if (!s) 1078 s = [[[NSString alloc] initWithData: mailData encoding: NSISOLatin1StringEncoding] autorelease]; 1079 } 1080 else 1081 s = nil; 1082 1083 return s; 1084} 1085 1086- (NSDictionary *) stringifyTextParts: (NSDictionary *) _datas 1087{ 1088 NSMutableDictionary *md; 1089 NSDictionary *info; 1090 NSEnumerator *keys; 1091 NSString *key, *s; 1092 1093 md = [NSMutableDictionary dictionaryWithCapacity:4]; 1094 keys = [_datas keyEnumerator]; 1095 while ((key = [keys nextObject])) 1096 { 1097 info = [self lookupInfoForBodyPart: key]; 1098 s = [self stringForData: [_datas objectForKey:key] partInfo: info]; 1099 if (s) 1100 [md setObject: s forKey: key]; 1101 } 1102 1103 return md; 1104} 1105 1106- (NSDictionary *) fetchPlainTextStrings: (NSArray *) _fetchKeys 1107{ 1108 /* 1109 The fetched parts are NSData objects, this method converts them into 1110 NSString objects based on the information inside the bodystructure. 1111 1112 The fetch-keys are body fetch-keys like: body[text] or body[1.2.3]. 1113 The keys in the result dictionary are "" for 'text' and 1.2.3 for parts. 1114 */ 1115 NSDictionary *datas; 1116 1117 if ((datas = [self fetchPlainTextParts:_fetchKeys]) == nil) 1118 return nil; 1119 if ([datas isKindOfClass:[NSException class]]) 1120 return datas; 1121 1122 return [self stringifyTextParts:datas]; 1123} 1124 1125/* flags */ 1126 1127- (NSException *) addFlags: (id) _flags 1128{ 1129 [coreInfos release]; 1130 coreInfos = nil; 1131 return [[self imap4Connection] addFlags:_flags toURL: [self imap4URL]]; 1132} 1133 1134- (NSException *) removeFlags: (id) _flags 1135{ 1136 [coreInfos release]; 1137 coreInfos = nil; 1138 return [[self imap4Connection] removeFlags:_flags toURL: [self imap4URL]]; 1139} 1140 1141/* permissions */ 1142 1143- (BOOL) isDeletionAllowed 1144{ 1145 NSArray *parentAcl; 1146 NSString *login; 1147 1148 login = [[context activeUser] login]; 1149 parentAcl = [[self container] aclsForUser: login]; 1150 1151 return [parentAcl containsObject: SOGoRole_ObjectEraser]; 1152} 1153 1154/* name lookup */ 1155 1156- (id) lookupImap4BodyPartKey: (NSString *) _key 1157 inContext: (id) _ctx 1158{ 1159 // TODO: we might want to check for existence prior controller creation 1160 NSDictionary *partDesc; 1161 NSString *mimeType; 1162 NSArray *parts; 1163 Class clazz; 1164 1165 int part; 1166 1167 if ([self isEncrypted]) 1168 { 1169 NSData *certificate; 1170 1171 certificate = [[self mailAccountFolder] certificate]; 1172 1173 // If we got a user certificate, let's use it. Otherwise we fallback 1174 // to the current parts fetching code. 1175 if (certificate) 1176 { 1177 NGMimeMessage *m; 1178 id part; 1179 1180 m = [[self content] messageFromEncryptedDataAndCertificate: certificate]; 1181 1182 part = [[[m body] parts] objectAtIndex: ([_key intValue]-1)]; 1183 mimeType = [[part contentType] stringValue]; 1184 clazz = [SOGoMailBodyPart bodyPartClassForMimeType: mimeType 1185 inContext: _ctx]; 1186 return [clazz objectWithName:_key inContainer: self]; 1187 } 1188 } 1189 else if ([self isOpaqueSigned]) 1190 { 1191 NGMimeMessage *m; 1192 id part; 1193 1194 m = [[self content] messageFromOpaqueSignedData]; 1195 1196 part = [[[m body] parts] objectAtIndex: ([_key intValue]-1)]; 1197 mimeType = [[part contentType] stringValue]; 1198 clazz = [SOGoMailBodyPart bodyPartClassForMimeType: mimeType 1199 inContext: _ctx]; 1200 return [clazz objectWithName:_key inContainer: self]; 1201 } 1202 1203 parts = [[self bodyStructure] objectForKey: @"parts"]; 1204 1205 /* We don't have parts here but we're trying to download the message's 1206 content that could be an image/jpeg, as an example */ 1207 if ([parts count] == 0 && ![_key intValue]) 1208 { 1209 partDesc = [self bodyStructure]; 1210 _key = @"1"; 1211 } 1212 else 1213 { 1214 part = [_key intValue] - 1; 1215 if (part > -1 && part < [parts count]) 1216 partDesc = [parts objectAtIndex: part]; 1217 else 1218 partDesc = nil; 1219 } 1220 1221 if (partDesc) 1222 { 1223 mimeType = [[partDesc keysWithFormat: @"%{type}/%{subtype}"] lowercaseString]; 1224 clazz = [SOGoMailBodyPart bodyPartClassForMimeType: mimeType 1225 inContext: _ctx]; 1226 } 1227 else 1228 clazz = Nil; 1229 1230 return [clazz objectWithName:_key inContainer: self]; 1231} 1232 1233- (id) lookupName: (NSString *) _key 1234 inContext: (id) _ctx 1235 acquire: (BOOL) _flag 1236{ 1237 id obj; 1238 1239 /* first check attributes directly bound to the application */ 1240 if ((obj = [super lookupName:_key inContext:_ctx acquire:NO]) != nil) 1241 return obj; 1242 1243 /* lookup body part */ 1244 1245 if ([self isBodyPartKey:_key]) { 1246 if ((obj = [self lookupImap4BodyPartKey:_key inContext:_ctx]) != nil) { 1247 if (debugSoParts) 1248 [self logWithFormat: @"mail looked up part %@: %@", _key, obj]; 1249 return obj; 1250 } 1251 } 1252 // Handles cases where the email is itself an attachment, so its Content-Type 1253 // is application/*, image/* etc. 1254 else if ([_key isEqualToString: @"asAttachment"] && 1255 (obj = [self lookupImap4BodyPartKey: @"0" inContext:_ctx]) != nil) 1256 { 1257 [obj setAsAttachment]; 1258 return obj; 1259 } 1260 1261 /* return 404 to stop acquisition */ 1262 return [NSException exceptionWithHTTPStatus:404 /* Not Found */ 1263 reason: @"Did not find mail method or part-reference!"]; 1264} 1265 1266/* WebDAV */ 1267 1268- (BOOL) davIsCollection 1269{ 1270 /* while a mail has child objects, it should appear as a file in WebDAV */ 1271 return NO; 1272} 1273 1274- (NSString *) davContentLength 1275{ 1276 return [NSString stringWithFormat: @"%@", [[self fetchCoreInfos] valueForKey: @"size"]]; 1277} 1278 1279- (NSDate *) davCreationDate 1280{ 1281 // TODO: use INTERNALDATE once NGImap4 supports that 1282 return nil; 1283} 1284 1285- (NSDate *) davLastModified 1286{ 1287 return [self davCreationDate]; 1288} 1289 1290- (NSException *) davMoveToTargetObject: (id) _target 1291 newName: (NSString *) _name 1292 inContext: (id)_ctx 1293{ 1294 [self logWithFormat: @"TODO: should move mail as '%@' to: %@", 1295 _name, _target]; 1296 return [NSException exceptionWithHTTPStatus: 501 /* Not Implemented */ 1297 reason: @"not implemented"]; 1298} 1299 1300- (NSException *) davCopyToTargetObject: (id) _target 1301 newName: (NSString *) _name 1302 inContext: (id)_ctx 1303{ 1304 /* 1305 Note: this is special because we create SOGoMailObject's even if they do 1306 not exist (for performance reasons). 1307 1308 Also: we cannot really take a target resource, the ID will be assigned by 1309 the IMAP4 server. 1310 We even cannot return a 'location' header instead because IMAP4 1311 doesn't tell us the new ID. 1312 */ 1313 NSURL *destImap4URL; 1314 NGImap4ConnectionManager *manager; 1315 NSException *exc; 1316 NSString *password; 1317 1318 destImap4URL = ([_name length] == 0) 1319 ? [[_target container] imap4URL] 1320 : [_target imap4URL]; 1321 1322 manager = [self mailManager]; 1323 [self imap4URL]; 1324 password = [self imap4PasswordRenewed: NO]; 1325 if (password) 1326 { 1327 exc = [manager copyMailURL: imap4URL 1328 toFolderURL: destImap4URL 1329 password: password]; 1330 if (exc) 1331 { 1332 [self 1333 logWithFormat: @"failure. Attempting with renewed imap4 password"]; 1334 password = [self imap4PasswordRenewed: YES]; 1335 if (password) 1336 exc = [manager copyMailURL: imap4URL 1337 toFolderURL: destImap4URL 1338 password: password]; 1339 } 1340 } 1341 else 1342 exc = nil; 1343 1344 return exc; 1345} 1346 1347/* actions */ 1348 1349- (id) GETAction: (id) _ctx 1350{ 1351 NSException *error; 1352 WOResponse *r; 1353 NSData *content; 1354 1355 if ((error = [self matchesRequestConditionInContext:_ctx]) != nil) { 1356 /* check whether the mail still exists */ 1357 if (![self doesMailExist]) { 1358 return [NSException exceptionWithHTTPStatus:404 /* Not Found */ 1359 reason: @"mail was deleted"]; 1360 } 1361 return error; /* return 304 or 416 */ 1362 } 1363 1364 content = [self content]; 1365 if ([content isKindOfClass:[NSException class]]) 1366 return content; 1367 if (content == nil) { 1368 return [NSException exceptionWithHTTPStatus:404 /* Not Found */ 1369 reason: @"did not find IMAP4 message"]; 1370 } 1371 1372 r = [(WOContext *)_ctx response]; 1373 [r setHeader: @"message/rfc822" forKey: @"content-type"]; 1374 [r setContent:content]; 1375 return r; 1376} 1377 1378/* operations */ 1379 1380- (NSException *) copyToFolderNamed: (NSString *) folderName 1381 inContext: (id)_ctx 1382{ 1383 SOGoMailFolder *destFolder; 1384 NSEnumerator *folders; 1385 NSString *currentFolderName, *reason; 1386 1387 // TODO: check for safe HTTP method 1388 1389 destFolder = (SOGoMailFolder *) [self mailAccountsFolder]; 1390 folders = [[folderName componentsSeparatedByString: @"/"] objectEnumerator]; 1391 currentFolderName = [folders nextObject]; 1392 currentFolderName = [folders nextObject]; 1393 1394 while (currentFolderName) 1395 { 1396 destFolder = [destFolder lookupName: currentFolderName 1397 inContext: _ctx 1398 acquire: NO]; 1399 if ([destFolder isKindOfClass: [NSException class]]) 1400 return (NSException *) destFolder; 1401 currentFolderName = [folders nextObject]; 1402 } 1403 1404 if (!([destFolder isKindOfClass: [SOGoMailFolder class]] 1405 && [destFolder isNotNull])) 1406 { 1407 reason = [NSString stringWithFormat: @"Did not find folder name '%@'!", 1408 folderName]; 1409 return [NSException exceptionWithHTTPStatus:500 /* Server Error */ 1410 reason: reason]; 1411 } 1412 [destFolder flushMailCaches]; 1413 1414 /* a) copy */ 1415 1416 return [self davCopyToTargetObject: destFolder 1417 newName: @"fakeNewUnusedByIMAP4" /* autoassigned */ 1418 inContext:_ctx]; 1419} 1420 1421- (NSException *) moveToFolderNamed: (NSString *) folderName 1422 inContext: (id)_ctx 1423{ 1424 NSException *error; 1425 1426 if (![self copyToFolderNamed: folderName 1427 inContext: _ctx]) 1428 { 1429 /* b) mark deleted */ 1430 1431 error = [[self imap4Connection] markURLDeleted: [self imap4URL]]; 1432 if (error != nil) return error; 1433 1434 [self flushMailCaches]; 1435 } 1436 1437 return nil; 1438} 1439 1440- (NSException *) delete 1441{ 1442 /* 1443 Note: delete is different to DELETEAction: for mails! The 'delete' runs 1444 either flags a message as deleted or moves it to the Trash while 1445 the DELETEAction: really deletes a message (by flagging it as 1446 deleted _AND_ performing an expunge). 1447 */ 1448 // TODO: copy to Trash folder 1449 NSException *error; 1450 1451 // TODO: check for safe HTTP method 1452 1453 error = [[self imap4Connection] markURLDeleted:[self imap4URL]]; 1454 return error; 1455} 1456 1457- (id) DELETEAction: (id) _ctx 1458{ 1459 NSException *error; 1460 1461 // TODO: ensure safe HTTP method 1462 1463 error = [[self imap4Connection] markURLDeleted:[self imap4URL]]; 1464 if (error != nil) return error; 1465 1466 error = [[self imap4Connection] expungeAtURL:[[self container] imap4URL]]; 1467 if (error != nil) return error; // TODO: unflag as deleted? 1468 1469 return [NSNumber numberWithBool:YES]; /* delete was successful */ 1470} 1471 1472/* some mail classification */ 1473 1474- (BOOL) isMailingListMail 1475{ 1476 NSDictionary *h; 1477 1478 if ((h = [self mailHeaders]) == nil) 1479 return NO; 1480 1481 return [[h objectForKey: @"list-id"] isNotEmpty]; 1482} 1483 1484- (BOOL) isVirusScanned 1485{ 1486 NSDictionary *h; 1487 1488 if ((h = [self mailHeaders]) == nil) 1489 return NO; 1490 1491 if (![[h objectForKey: @"x-virus-status"] isNotEmpty]) return NO; 1492 if (![[h objectForKey: @"x-virus-scanned"] isNotEmpty]) return NO; 1493 return YES; 1494} 1495 1496- (NSString *) scanListHeaderValue: (id) _value 1497 forFieldWithPrefix: (NSString *) _prefix 1498{ 1499 /* Note: not very tolerant on embedded commands and <> */ 1500 // TODO: does not really belong here, should be a header-field-parser 1501 NSRange r; 1502 1503 if (![_value isNotEmpty]) 1504 return nil; 1505 1506 if ([_value isKindOfClass:[NSArray class]]) { 1507 NSEnumerator *e; 1508 id value; 1509 1510 e = [_value objectEnumerator]; 1511 while ((value = [e nextObject]) != nil) { 1512 value = [self scanListHeaderValue:value forFieldWithPrefix:_prefix]; 1513 if (value != nil) return value; 1514 } 1515 return nil; 1516 } 1517 1518 if (![_value isKindOfClass:[NSString class]]) 1519 return nil; 1520 1521 /* check for commas in string values */ 1522 r = [_value rangeOfString: @","]; 1523 if (r.length > 0) { 1524 return [self scanListHeaderValue:[_value componentsSeparatedByString: @","] 1525 forFieldWithPrefix:_prefix]; 1526 } 1527 1528 /* value qualifies */ 1529 if (![(NSString *)_value hasPrefix:_prefix]) 1530 return nil; 1531 1532 /* unquote */ 1533 if ([_value characterAtIndex:0] == '<') { 1534 r = [_value rangeOfString: @">"]; 1535 _value = (r.length == 0) 1536 ? [_value substringFromIndex:1] 1537 : [_value substringWithRange:NSMakeRange(1, r.location - 2)]; 1538 } 1539 1540 return _value; 1541} 1542 1543- (NSString *) mailingListArchiveURL 1544{ 1545 return [self scanListHeaderValue: 1546 [[self mailHeaders] objectForKey: @"list-archive"] 1547 forFieldWithPrefix: @"<http://"]; 1548} 1549 1550- (NSString *) mailingListSubscribeURL 1551{ 1552 return [self scanListHeaderValue: 1553 [[self mailHeaders] objectForKey: @"list-subscribe"] 1554 forFieldWithPrefix: @"<http://"]; 1555} 1556 1557- (NSString *) mailingListUnsubscribeURL 1558{ 1559 return [self scanListHeaderValue: 1560 [[self mailHeaders] objectForKey: @"list-unsubscribe"] 1561 forFieldWithPrefix: @"<http://"]; 1562} 1563 1564/* etag support */ 1565 1566- (id) davEntityTag 1567{ 1568 /* 1569 Note: There is one thing which *can* change for an existing message, 1570 those are the IMAP4 flags (and annotations, which we do not use). 1571 Since we don't render the flags, it should be OK, if this changes 1572 we must embed the flagging into the etag. 1573 */ 1574 return mailETag; 1575} 1576 1577- (NSArray *) aclsForUser: (NSString *) uid 1578{ 1579 return [container aclsForUser: uid]; 1580} 1581 1582/* debugging */ 1583 1584- (BOOL) isDebuggingEnabled 1585{ 1586 return debugOn; 1587} 1588 1589 1590// For DAV PUT 1591- (id) PUTAction: (WOContext *) _ctx 1592{ 1593 WORequest *rq; 1594 NSException *error; 1595 WOResponse *response; 1596 SOGoMailFolder *folder; 1597 int imap4id; 1598 1599 error = [self matchesRequestConditionInContext: _ctx]; 1600 if (error) 1601 response = (WOResponse *) error; 1602 else 1603 { 1604 rq = [_ctx request]; 1605 folder = [self container]; 1606 1607 if ([self doesMailExist]) 1608 response = [NSException exceptionWithHTTPStatus: 403 1609 reason: @"Can't overwrite messages"]; 1610 else 1611 response = [folder appendMessage: [rq content] 1612 usingId: &imap4id]; 1613 } 1614 1615 return response; 1616} 1617 1618// For DAV REPORT 1619- (id) _fetchProperty: (NSString *) property 1620{ 1621 NSArray *parts; 1622 id rc, msgs; 1623 1624 rc = nil; 1625 1626 if (property) 1627 { 1628 parts = [NSArray arrayWithObject: property]; 1629 1630 msgs = [self fetchParts: parts]; 1631 msgs = [msgs valueForKey: @"fetch"]; 1632 if ([msgs count]) { 1633 rc = [msgs objectAtIndex: 0]; 1634 } 1635 } 1636 1637 return rc; 1638} 1639 1640- (BOOL) _hasFlag: (NSString *) flag 1641{ 1642 BOOL rc; 1643 NSArray *flags; 1644 1645 flags = [[self fetchCoreInfos] objectForKey: @"flags"]; 1646 rc = [flags containsObject: flag]; 1647 1648 return rc; 1649} 1650 1651- (NSString *) _emailAddressesFrom: (NSArray *) enveloppeAddresses 1652{ 1653 NSMutableArray *addresses; 1654 NSString *rc; 1655 NGImap4EnvelopeAddress *address; 1656 NSString *email; 1657 int count, max; 1658 1659 rc = nil; 1660 max = [enveloppeAddresses count]; 1661 1662 if (max > 0) 1663 { 1664 addresses = [NSMutableArray array]; 1665 for (count = 0; count < max; count++) 1666 { 1667 address = [enveloppeAddresses objectAtIndex: count]; 1668 email = [NSString stringWithFormat: @"%@", [address email]]; 1669 1670 [addresses addObject: email]; 1671 } 1672 rc = [addresses componentsJoinedByString: @", "]; 1673 } 1674 1675 return rc; 1676} 1677 1678// Properties 1679 1680//{urn:schemas:httpmail:} 1681 1682// date already exists, but this one is the correct format 1683- (NSString *) davDate 1684{ 1685 return [[self date] rfc822DateString]; 1686} 1687 1688- (BOOL) hasAttachment 1689{ 1690 return ([[self fetchFileAttachmentKeys] count] > 0); 1691} 1692 1693- (BOOL) isNewMail 1694{ 1695 return [self _hasFlag: @"recent"]; 1696} 1697 1698- (BOOL) read 1699{ 1700 return [self _hasFlag: @"seen"]; 1701} 1702 1703- (BOOL) flagged 1704{ 1705 return [self _hasFlag: @"flagged"]; 1706} 1707 1708- (BOOL) replied 1709{ 1710 return [self _hasFlag: @"answered"]; 1711} 1712 1713- (BOOL) forwarded 1714{ 1715 return [self _hasFlag: @"$forwarded"]; 1716} 1717 1718- (BOOL) deleted 1719{ 1720 return [self _hasFlag: @"deleted"]; 1721} 1722 1723- (BOOL) isSigned 1724{ 1725 NSString *type, *subtype, *protocol; 1726 NGMimeType *contentType; 1727 1728 contentType = [[self mailHeaders] objectForKey: @"content-type"]; 1729 type = [[contentType type] lowercaseString]; 1730 subtype = [[contentType subType] lowercaseString]; 1731 protocol = [[contentType valueOfParameter: @"protocol"] lowercaseString]; 1732 1733 return ([type isEqualToString: @"multipart"] && 1734 [subtype isEqualToString: @"signed"] && 1735 ([protocol isEqualToString: @"application/x-pkcs7-signature"] || 1736 [protocol isEqualToString: @"application/pkcs7-signature"])); 1737} 1738 1739- (BOOL) isOpaqueSigned 1740{ 1741 NSString *type, *subtype, *smimetype; 1742 NGMimeType *contentType; 1743 1744 contentType = [[self mailHeaders] objectForKey: @"content-type"]; 1745 type = [[contentType type] lowercaseString]; 1746 subtype = [[contentType subType] lowercaseString]; 1747 1748 if ([type isEqualToString: @"application"]) 1749 { 1750 if ([subtype isEqualToString: @"x-pkcs7-mime"] || 1751 [subtype isEqualToString: @"pkcs7-mime"]) 1752 { 1753 smimetype = [[contentType valueOfParameter: @"smime-type"] lowercaseString]; 1754 if ([smimetype isEqualToString: @"signed-data"]) 1755 return YES; 1756 } 1757 } 1758 1759 return NO; 1760} 1761 1762- (BOOL) isEncrypted 1763{ 1764 NSString *type, *subtype, *smimetype; 1765 NGMimeType *contentType; 1766 1767 contentType = [[self mailHeaders] objectForKey: @"content-type"]; 1768 type = [[contentType type] lowercaseString]; 1769 subtype = [[contentType subType] lowercaseString]; 1770 1771 if ([type isEqualToString: @"application"]) 1772 { 1773 if ([subtype isEqualToString: @"x-pkcs7-mime"] || 1774 [subtype isEqualToString: @"pkcs7-mime"]) 1775 { 1776 smimetype = [[contentType valueOfParameter: @"smime-type"] lowercaseString]; 1777 if ([smimetype isEqualToString: @"enveloped-data"]) 1778 return YES; 1779 } 1780 } 1781 1782 return NO; 1783} 1784 1785- (NSString *) textDescription 1786{ 1787#warning We should send the content as an NSData 1788 return [NSString stringWithFormat: @"<![CDATA[%@]]>", [self contentAsString]]; 1789} 1790 1791 1792//{urn:schemas:mailheader:} 1793 1794- (NSString *) to 1795{ 1796 return [self _emailAddressesFrom: [self toEnvelopeAddresses]]; 1797} 1798 1799- (NSString *) cc 1800{ 1801 return [self _emailAddressesFrom: [self ccEnvelopeAddresses]]; 1802} 1803 1804- (NSString *) from 1805{ 1806 return [self _emailAddressesFrom: [self fromEnvelopeAddresses]]; 1807} 1808 1809- (NSString *) inReplyTo 1810{ 1811 return [[self envelope] inReplyTo]; 1812} 1813 1814- (NSString *) messageId 1815{ 1816 return [[self envelope] messageID]; 1817} 1818 1819- (NSString *) received 1820{ 1821 NSDictionary *fetch; 1822 NSData *data; 1823 NSString *value, *rc; 1824 NSRange range; 1825 1826 rc = nil; 1827 fetch = [self _fetchProperty: @"BODY.PEEK[HEADER.FIELDS (RECEIVED)]"]; 1828 1829 if ([fetch count]) 1830 { 1831 data = [fetch objectForKey: @"header"]; 1832 value = [[NSString alloc] initWithData: data 1833 encoding: NSUTF8StringEncoding]; 1834 range = [value rangeOfString: @"received:" 1835 options: NSCaseInsensitiveSearch 1836 range: NSMakeRange (10, [value length] - 11)]; 1837 if (range.length 1838 && range.location < [value length] 1839 && range.length < [value length]) 1840 { 1841 // We want to keep the first part 1842 range.length = range.location; 1843 range.location = 0; 1844 rc = [[value substringWithRange: range] stringByTrimmingSpaces]; 1845 } 1846 else 1847 rc = [value stringByTrimmingSpaces]; 1848 1849 [value release]; 1850 } 1851 1852 return rc; 1853} 1854 1855- (NSString *) references 1856{ 1857 NSDictionary *fetch; 1858 NSData *data; 1859 NSString *value, *rc; 1860 1861 rc = nil; 1862 fetch = [self _fetchProperty: @"BODY.PEEK[HEADER.FIELDS (REFERENCES)]"]; 1863 1864 if ([fetch count]) 1865 { 1866 data = [fetch objectForKey: @"header"]; 1867 value = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding]; 1868 if (value && [value length] > 11) 1869 rc = [[value substringFromIndex: 11] stringByTrimmingSpaces]; 1870 [value release]; 1871 } 1872 1873 return rc; 1874} 1875 1876- (NSString *) davDisplayName 1877{ 1878 return [self subject]; 1879} 1880 1881@end /* SOGoMailObject */ 1882