1 /* 2 * Copyright 2012-2016, Axel Dörfler, axeld@pinc-software.de. 3 * Distributed under the terms of the MIT License. 4 */ 5 6 7 #include "IMAPFolder.h" 8 9 #include <set> 10 11 #include <ByteOrder.h> 12 #include <Debug.h> 13 #include <Directory.h> 14 #include <File.h> 15 #include <fs_attr.h> 16 #include <Messenger.h> 17 #include <Node.h> 18 #include <Path.h> 19 20 #include <NodeMessage.h> 21 22 #include "IMAPProtocol.h" 23 24 25 static const char* kMailboxNameAttribute = "IMAP:mailbox"; 26 static const char* kUIDValidityAttribute = "IMAP:uidvalidity"; 27 static const char* kLastUIDAttribute = "IMAP:lastuid"; 28 static const char* kStateAttribute = "IMAP:state"; 29 static const char* kFlagsAttribute = "IMAP:flags"; 30 static const char* kUIDAttribute = "MAIL:unique_id"; 31 32 33 class TemporaryFile : public BFile { 34 public: 35 TemporaryFile(BFile& file) 36 : 37 fFile(file), 38 fDeleteFile(false) 39 { 40 } 41 42 ~TemporaryFile() 43 { 44 if (fDeleteFile) { 45 fFile.Unset(); 46 BEntry(fPath.Path()).Remove(); 47 } 48 } 49 50 status_t Init(const BPath& path, entry_ref& ref) 51 { 52 int32 tries = 53; 53 while (true) { 54 BString name("temp-mail-"); 55 name << system_time(); 56 57 fPath = path; 58 fPath.Append(name.String()); 59 60 status_t status = fFile.SetTo(fPath.Path(), 61 B_CREATE_FILE | B_FAIL_IF_EXISTS | B_READ_WRITE); 62 if (status == B_FILE_EXISTS && tries-- > 0) 63 continue; 64 if (status != B_OK) 65 return status; 66 67 fDeleteFile = true; 68 return get_ref_for_path(fPath.Path(), &ref); 69 } 70 } 71 72 void KeepFile() 73 { 74 fDeleteFile = false; 75 } 76 77 private: 78 BFile& fFile; 79 BPath fPath; 80 bool fDeleteFile; 81 }; 82 83 84 // #pragma mark - 85 86 87 IMAPFolder::IMAPFolder(IMAPProtocol& protocol, const BString& mailboxName, 88 const entry_ref& ref) 89 : 90 BHandler(mailboxName.String()), 91 fProtocol(protocol), 92 fRef(ref), 93 fMailboxName(mailboxName), 94 fUIDValidity(UINT32_MAX), 95 fLastUID(0), 96 fListener(NULL), 97 fFolderStateInitialized(false), 98 fQuitFolderState(false) 99 { 100 mutex_init(&fLock, "imap folder lock"); 101 mutex_init(&fFolderStateLock, "imap folder state lock"); 102 } 103 104 105 IMAPFolder::~IMAPFolder() 106 { 107 MutexLocker locker(fLock); 108 if (!fFolderStateInitialized && fListener != NULL) { 109 fQuitFolderState = true; 110 locker.Unlock(); 111 wait_for_thread(fReadFolderStateThread, NULL); 112 } 113 } 114 115 116 status_t 117 IMAPFolder::Init() 118 { 119 // Initialize from folder attributes 120 BNode node(&fRef); 121 status_t status = node.InitCheck(); 122 if (status != B_OK) 123 return status; 124 125 node_ref nodeRef; 126 status = node.GetNodeRef(&nodeRef); 127 if (status != B_OK) 128 return status; 129 130 fNodeID = nodeRef.node; 131 132 BString originalMailboxName; 133 if (node.ReadAttrString(kMailboxNameAttribute, &originalMailboxName) == B_OK 134 && originalMailboxName != fMailboxName) { 135 // TODO: mailbox name has changed 136 } 137 138 fUIDValidity = _ReadUInt32(node, kUIDValidityAttribute); 139 fLastUID = _ReadUInt32(node, kLastUIDAttribute); 140 printf("IMAP: %s, last UID %" B_PRIu32 "\n", fMailboxName.String(), 141 fLastUID); 142 143 attr_info info; 144 status = node.GetAttrInfo(kStateAttribute, &info); 145 if (status == B_OK) { 146 struct entry { 147 uint32 uid; 148 uint32 flags; 149 } _PACKED; 150 struct entry* entries = (struct entry*)malloc(info.size); 151 if (entries == NULL) 152 return B_NO_MEMORY; 153 154 ssize_t bytesRead = node.ReadAttr(kStateAttribute, B_RAW_TYPE, 0, 155 entries, info.size); 156 if (bytesRead != info.size) 157 return B_BAD_DATA; 158 159 for (size_t i = 0; i < info.size / sizeof(entry); i++) { 160 uint32 uid = B_BENDIAN_TO_HOST_INT32(entries[i].uid); 161 uint32 flags = B_BENDIAN_TO_HOST_INT32(entries[i].flags); 162 163 fFlagsMap.insert(std::make_pair(uid, flags)); 164 } 165 } 166 167 return B_OK; 168 } 169 170 171 void 172 IMAPFolder::SetListener(FolderListener* listener) 173 { 174 ASSERT(fListener == NULL); 175 176 fListener = listener; 177 178 // Initialize current state from actual folder 179 // TODO: this should be done in another thread 180 _InitializeFolderState(); 181 } 182 183 184 void 185 IMAPFolder::SetUIDValidity(uint32 uidValidity) 186 { 187 if (fUIDValidity == uidValidity) 188 return; 189 190 // TODO: delete all mails that have the same UID validity value we had 191 fUIDValidity = uidValidity; 192 193 BNode node(&fRef); 194 _WriteUInt32(node, kUIDValidityAttribute, uidValidity); 195 } 196 197 198 status_t 199 IMAPFolder::GetMessageEntryRef(uint32 uid, entry_ref& ref) 200 { 201 MutexLocker locker(fLock); 202 return _GetMessageEntryRef(uid, ref); 203 } 204 205 206 status_t 207 IMAPFolder::GetMessageUID(const entry_ref& ref, uint32& uid) const 208 { 209 BNode node(&ref); 210 status_t status = node.InitCheck(); 211 if (status != B_OK) 212 return status; 213 214 uid = _ReadUniqueID(node); 215 if (uid == 0) 216 return B_ENTRY_NOT_FOUND; 217 218 return B_OK; 219 } 220 221 222 uint32 223 IMAPFolder::MessageFlags(uint32 uid) 224 { 225 MutexLocker locker(fLock); 226 UIDToFlagsMap::const_iterator found = fFlagsMap.find(uid); 227 if (found == fFlagsMap.end()) 228 return 0; 229 230 return found->second; 231 } 232 233 234 /*! Synchronizes the message flags/state from the server with the local 235 one. 236 */ 237 void 238 IMAPFolder::SyncMessageFlags(uint32 uid, uint32 mailboxFlags) 239 { 240 if (uid > LastUID()) 241 return; 242 243 entry_ref ref; 244 BNode node; 245 246 while (true) { 247 status_t status = GetMessageEntryRef(uid, ref); 248 if (status == B_ENTRY_NOT_FOUND) { 249 // The message does not exist anymore locally, delete it on the 250 // server 251 // TODO: copy it to the trash directory first! 252 if (fProtocol.Settings()->DeleteRemoteWhenLocal()) 253 fProtocol.UpdateMessageFlags(*this, uid, IMAP::kDeleted); 254 return; 255 } 256 if (status == B_OK) 257 status = node.SetTo(&ref); 258 if (status == B_TIMED_OUT) { 259 // We don't know the message state yet 260 fPendingFlagsMap.insert(std::make_pair(uid, mailboxFlags)); 261 } 262 if (status != B_OK) 263 return; 264 265 break; 266 } 267 fSynchronizedUIDsSet.insert(uid); 268 269 uint32 previousFlags = MessageFlags(uid); 270 uint32 currentFlags = previousFlags; 271 if (_MailToIMAPFlags(node, currentFlags) != B_OK) 272 return; 273 274 // Compare flags to previous/current flags, and update either the 275 // message on the server, or the message locally (or even both) 276 277 uint32 nextFlags = mailboxFlags; 278 _TestMessageFlags(previousFlags, mailboxFlags, currentFlags, 279 IMAP::kSeen, nextFlags); 280 _TestMessageFlags(previousFlags, mailboxFlags, currentFlags, 281 IMAP::kAnswered, nextFlags); 282 283 if (nextFlags != previousFlags) 284 _WriteFlags(node, nextFlags); 285 if (currentFlags != nextFlags) { 286 // Update mail message attributes 287 BMessage attributes; 288 _IMAPToMailFlags(nextFlags, attributes); 289 node << attributes; 290 291 fFlagsMap[uid] = nextFlags; 292 } 293 if (mailboxFlags != nextFlags) { 294 // Update server flags 295 fProtocol.UpdateMessageFlags(*this, uid, nextFlags); 296 } 297 } 298 299 300 void 301 IMAPFolder::MessageEntriesFetched() 302 { 303 _WaitForFolderState(); 304 305 // Synchronize all pending flags first 306 UIDToFlagsMap::const_iterator pendingIterator = fPendingFlagsMap.begin(); 307 for (; pendingIterator != fPendingFlagsMap.end(); pendingIterator++) 308 SyncMessageFlags(pendingIterator->first, pendingIterator->second); 309 310 fPendingFlagsMap.clear(); 311 312 // Delete all local messages that are no longer found on the server 313 314 MutexLocker locker(fLock); 315 UIDSet deleteUIDs; 316 UIDToRefMap::const_iterator iterator = fRefMap.begin(); 317 for (; iterator != fRefMap.end(); iterator++) { 318 uint32 uid = iterator->first; 319 if (fSynchronizedUIDsSet.find(uid) == fSynchronizedUIDsSet.end()) 320 deleteUIDs.insert(uid); 321 } 322 323 fSynchronizedUIDsSet.clear(); 324 locker.Unlock(); 325 326 UIDSet::const_iterator deleteIterator = deleteUIDs.begin(); 327 for (; deleteIterator != deleteUIDs.end(); deleteIterator++) 328 _DeleteLocalMessage(*deleteIterator); 329 } 330 331 332 /*! Stores the given \a stream into a temporary file using the provided 333 BFile object. A new file will be created, and the \a ref object will 334 point to it. The file will remain open when this method exits without 335 an error. 336 337 \a length will reflect how many bytes are left to read in case there 338 was an error. 339 */ 340 status_t 341 IMAPFolder::StoreMessage(uint32 fetchFlags, BDataIO& stream, 342 size_t& length, entry_ref& ref, BFile& file) 343 { 344 BPath path; 345 status_t status = path.SetTo(&fRef); 346 if (status != B_OK) 347 return status; 348 349 TemporaryFile temporaryFile(file); 350 status = temporaryFile.Init(path, ref); 351 if (status != B_OK) 352 return status; 353 354 status = _WriteStream(file, stream, length); 355 if (status == B_OK) 356 temporaryFile.KeepFile(); 357 358 return status; 359 } 360 361 362 /*! Writes UID, and flags to the message, and notifies the protocol that a 363 message has been fetched. This method also closes the \a file passed in. 364 */ 365 void 366 IMAPFolder::MessageStored(entry_ref& ref, BFile& file, uint32 fetchFlags, 367 uint32 uid, uint32 flags) 368 { 369 _WriteUniqueIDValidity(file); 370 _WriteUniqueID(file, uid); 371 if ((fetchFlags & IMAP::kFetchFlags) != 0) 372 _WriteFlags(file, flags); 373 374 BMessage attributes; 375 _IMAPToMailFlags(flags, attributes); 376 377 fProtocol.MessageStored(*this, ref, file, fetchFlags, attributes); 378 file.Unset(); 379 380 fRefMap.insert(std::make_pair(uid, ref)); 381 382 if (uid > fLastUID) { 383 // Update last known UID 384 fLastUID = uid; 385 386 BNode directory(&fRef); 387 status_t status = _WriteUInt32(directory, kLastUIDAttribute, uid); 388 if (status != B_OK) { 389 fprintf(stderr, "IMAP: Could not write last UID for mailbox " 390 "%s: %s\n", fMailboxName.String(), strerror(status)); 391 } 392 } 393 } 394 395 396 /*! Pushes the refs for the pending bodies to the pending bodies list. 397 This allows to prevent retrieving bodies more than once. 398 */ 399 void 400 IMAPFolder::RegisterPendingBodies(IMAP::MessageUIDList& uids, 401 const BMessenger* replyTo) 402 { 403 MutexLocker locker(fLock); 404 405 MessengerList messengers; 406 if (replyTo != NULL) 407 messengers.push_back(*replyTo); 408 409 IMAP::MessageUIDList::const_iterator iterator = uids.begin(); 410 for (; iterator != uids.end(); iterator++) { 411 if (replyTo != NULL) { 412 fPendingBodies[*iterator].push_back(*replyTo); 413 } else { 414 // Note: GCC 13 warns about the unused result of the statement below. This code should 415 // be reviewed as part of #18478 416 #if __GNUC__ == 13 417 # pragma GCC diagnostic push 418 # pragma GCC diagnostic warning "-Wunused-result" 419 #endif 420 fPendingBodies[*iterator].begin(); 421 #if __GNUC__ == 13 422 # pragma GCC diagnostic pop 423 #endif 424 } 425 } 426 } 427 428 429 /*! Appends the given \a stream as body to the message file for the 430 specified unique ID. The file will remain open when this method exits 431 without an error. 432 433 \a length will reflect how many bytes are left to read in case there 434 were an error. 435 */ 436 status_t 437 IMAPFolder::StoreBody(uint32 uid, BDataIO& stream, size_t& length, 438 entry_ref& ref, BFile& file) 439 { 440 status_t status = GetMessageEntryRef(uid, ref); 441 if (status != B_OK) 442 return status; 443 444 status = file.SetTo(&ref, B_OPEN_AT_END | B_WRITE_ONLY); 445 if (status != B_OK) 446 return status; 447 448 BPath path(&ref); 449 printf("IMAP: write body to %s\n", path.Path()); 450 451 return _WriteStream(file, stream, length); 452 } 453 454 455 /*! Notifies the protocol that a body has been fetched. 456 This method also closes the \a file passed in. 457 */ 458 void 459 IMAPFolder::BodyStored(entry_ref& ref, BFile& file, uint32 uid) 460 { 461 BMessage attributes; 462 fProtocol.MessageStored(*this, ref, file, IMAP::kFetchBody, attributes); 463 file.Unset(); 464 465 _NotifyStoredBody(ref, uid, B_OK); 466 } 467 468 469 void 470 IMAPFolder::StoringBodyFailed(const entry_ref& ref, uint32 uid, status_t error) 471 { 472 _NotifyStoredBody(ref, uid, error); 473 } 474 475 476 void 477 IMAPFolder::DeleteMessage(uint32 uid) 478 { 479 // TODO: move message to trash (server side) 480 481 _DeleteLocalMessage(uid); 482 } 483 484 485 void 486 IMAPFolder::MessageReceived(BMessage* message) 487 { 488 switch (message->what) { 489 default: 490 BHandler::MessageReceived(message); 491 break; 492 } 493 } 494 495 496 void 497 IMAPFolder::_WaitForFolderState() 498 { 499 while (true) { 500 MutexLocker locker(fFolderStateLock); 501 if (fFolderStateInitialized) 502 return; 503 } 504 } 505 506 507 void 508 IMAPFolder::_InitializeFolderState() 509 { 510 mutex_lock(&fFolderStateLock); 511 512 fReadFolderStateThread = spawn_thread(&IMAPFolder::_ReadFolderState, 513 "IMAP folder state", B_NORMAL_PRIORITY, this); 514 if (fReadFolderStateThread >= 0) 515 resume_thread(fReadFolderStateThread); 516 else 517 mutex_unlock(&fFolderStateLock); 518 } 519 520 521 void 522 IMAPFolder::_ReadFolderState() 523 { 524 BDirectory directory(&fRef); 525 BEntry entry; 526 while (directory.GetNextEntry(&entry) == B_OK) { 527 entry_ref ref; 528 BNode node; 529 if (!entry.IsFile() || entry.GetRef(&ref) != B_OK 530 || node.SetTo(&entry) != B_OK) 531 continue; 532 533 uint32 uidValidity = _ReadUniqueIDValidity(node); 534 if (uidValidity != fUIDValidity) { 535 // TODO: add file to mailbox 536 continue; 537 } 538 uint32 uid = _ReadUniqueID(node); 539 uint32 flags = _ReadFlags(node); 540 541 MutexLocker locker(fLock); 542 if (fQuitFolderState) 543 return; 544 545 fRefMap.insert(std::make_pair(uid, ref)); 546 fFlagsMap.insert(std::make_pair(uid, flags)); 547 548 // // TODO: make sure a listener exists at this point! 549 // std::set<uint32>::iterator found = lastUIDs.find(uid); 550 // if (found != lastUIDs.end()) { 551 // // The message is still around 552 // lastUIDs.erase(found); 553 // 554 // uint32 flagsFound = MessageFlags(uid); 555 // if (flagsFound != flags) { 556 // // Its flags have changed locally, and need to be updated 557 // fListener->MessageFlagsChanged(_Token(uid), ref, 558 // flagsFound, flags); 559 // } 560 // } else { 561 // // This is a new message 562 // // TODO: the token must be the originating token! 563 // uid = fListener->MessageAdded(_Token(uid), ref); 564 // _WriteUniqueID(node, uid); 565 // } 566 // 567 } 568 569 fFolderStateInitialized = true; 570 mutex_unlock(&fFolderStateLock); 571 } 572 573 574 /*static*/ status_t 575 IMAPFolder::_ReadFolderState(void* self) 576 { 577 ((IMAPFolder*)self)->_ReadFolderState(); 578 return B_OK; 579 } 580 581 582 const MessageToken 583 IMAPFolder::_Token(uint32 uid) const 584 { 585 MessageToken token; 586 token.mailboxName = fMailboxName; 587 token.uidValidity = fUIDValidity; 588 token.uid = uid; 589 590 return token; 591 } 592 593 594 void 595 IMAPFolder::_NotifyStoredBody(const entry_ref& ref, uint32 uid, status_t status) 596 { 597 MutexLocker locker(fLock); 598 MessengerMap::iterator found = fPendingBodies.find(uid); 599 if (found != fPendingBodies.end()) { 600 MessengerList messengers = found->second; 601 fPendingBodies.erase(found); 602 locker.Unlock(); 603 604 MessengerList::iterator iterator = messengers.begin(); 605 for (; iterator != messengers.end(); iterator++) 606 BInboundMailProtocol::ReplyBodyFetched(*iterator, ref, status); 607 } 608 } 609 610 611 status_t 612 IMAPFolder::_GetMessageEntryRef(uint32 uid, entry_ref& ref) const 613 { 614 UIDToRefMap::const_iterator found = fRefMap.find(uid); 615 if (found == fRefMap.end()) 616 return !fFolderStateInitialized ? B_TIMED_OUT : B_ENTRY_NOT_FOUND; 617 618 ref = found->second; 619 return B_OK; 620 } 621 622 623 status_t 624 IMAPFolder::_DeleteLocalMessage(uint32 uid) 625 { 626 entry_ref ref; 627 status_t status = GetMessageEntryRef(uid, ref); 628 if (status != B_OK) 629 return status; 630 631 fRefMap.erase(uid); 632 fFlagsMap.erase(uid); 633 634 BEntry entry(&ref); 635 return entry.Remove(); 636 } 637 638 639 void 640 IMAPFolder::_IMAPToMailFlags(uint32 flags, BMessage& attributes) 641 { 642 // TODO: add some utility function for this in libmail.so 643 if ((flags & IMAP::kAnswered) != 0) 644 attributes.AddString(B_MAIL_ATTR_STATUS, "Answered"); 645 else if ((flags & IMAP::kFlagged) != 0) 646 attributes.AddString(B_MAIL_ATTR_STATUS, "Starred"); 647 else if ((flags & IMAP::kSeen) != 0) 648 attributes.AddString(B_MAIL_ATTR_STATUS, "Read"); 649 } 650 651 652 status_t 653 IMAPFolder::_MailToIMAPFlags(BNode& node, uint32& flags) 654 { 655 BString mailStatus; 656 status_t status = node.ReadAttrString(B_MAIL_ATTR_STATUS, &mailStatus); 657 if (status != B_OK) 658 return status; 659 660 flags &= ~(IMAP::kAnswered | IMAP::kSeen); 661 662 // TODO: add some utility function for this in libmail.so 663 if (mailStatus == "Answered") 664 flags |= IMAP::kAnswered | IMAP::kSeen; 665 else if (mailStatus == "Read") 666 flags |= IMAP::kSeen; 667 else if (mailStatus == "Starred") 668 flags |= IMAP::kFlagged | IMAP::kSeen; 669 670 return B_OK; 671 } 672 673 674 void 675 IMAPFolder::_TestMessageFlags(uint32 previousFlags, uint32 mailboxFlags, 676 uint32 currentFlags, uint32 testFlag, uint32& nextFlags) 677 { 678 if ((previousFlags & testFlag) != (mailboxFlags & testFlag)) { 679 if ((previousFlags & testFlag) == (currentFlags & testFlag)) { 680 // The flags on the mailbox changed, update local flags 681 nextFlags &= ~testFlag; 682 nextFlags |= mailboxFlags & testFlag; 683 } else { 684 // Both flags changed. Since we don't have the means to do 685 // conflict resolution, we use a best effort mechanism 686 nextFlags |= testFlag; 687 } 688 return; 689 } 690 691 // Previous message flags, and mailbox flags are identical, see 692 // if the user changed the flag locally 693 if ((currentFlags & testFlag) != (previousFlags & testFlag)) { 694 // Flag changed, update mailbox 695 nextFlags &= ~testFlag; 696 nextFlags |= currentFlags & testFlag; 697 } 698 } 699 700 701 uint32 702 IMAPFolder::_ReadUniqueID(BNode& node) const 703 { 704 // For compatibility we must assume that the UID is stored as a string 705 BString string; 706 if (node.ReadAttrString(kUIDAttribute, &string) != B_OK) 707 return 0; 708 709 return strtoul(string.String(), NULL, 0); 710 } 711 712 713 status_t 714 IMAPFolder::_WriteUniqueID(BNode& node, uint32 uid) const 715 { 716 // For compatibility we must assume that the UID is stored as a string 717 BString string; 718 string << uid; 719 720 return node.WriteAttrString(kUIDAttribute, &string); 721 } 722 723 724 uint32 725 IMAPFolder::_ReadUniqueIDValidity(BNode& node) const 726 { 727 728 return _ReadUInt32(node, kUIDValidityAttribute); 729 } 730 731 732 status_t 733 IMAPFolder::_WriteUniqueIDValidity(BNode& node) const 734 { 735 return _WriteUInt32(node, kUIDValidityAttribute, fUIDValidity); 736 } 737 738 739 uint32 740 IMAPFolder::_ReadFlags(BNode& node) const 741 { 742 return _ReadUInt32(node, kFlagsAttribute); 743 } 744 745 746 status_t 747 IMAPFolder::_WriteFlags(BNode& node, uint32 flags) const 748 { 749 return _WriteUInt32(node, kFlagsAttribute, flags); 750 } 751 752 753 uint32 754 IMAPFolder::_ReadUInt32(BNode& node, const char* attribute) const 755 { 756 uint32 value; 757 ssize_t bytesRead = node.ReadAttr(attribute, B_UINT32_TYPE, 0, 758 &value, sizeof(uint32)); 759 if (bytesRead == (ssize_t)sizeof(uint32)) 760 return value; 761 762 return 0; 763 } 764 765 766 status_t 767 IMAPFolder::_WriteUInt32(BNode& node, const char* attribute, uint32 value) const 768 { 769 ssize_t bytesWritten = node.WriteAttr(attribute, B_UINT32_TYPE, 0, 770 &value, sizeof(uint32)); 771 if (bytesWritten == (ssize_t)sizeof(uint32)) 772 return B_OK; 773 774 return bytesWritten < 0 ? bytesWritten : B_IO_ERROR; 775 } 776 777 778 status_t 779 IMAPFolder::_WriteStream(BFile& file, BDataIO& stream, size_t& length) const 780 { 781 char buffer[65535]; 782 while (length > 0) { 783 ssize_t bytesRead = stream.Read(buffer, 784 std::min(sizeof(buffer), length)); 785 if (bytesRead < 0) 786 return bytesRead; 787 if (bytesRead <= 0) 788 break; 789 790 length -= bytesRead; 791 792 ssize_t bytesWritten = file.Write(buffer, bytesRead); 793 if (bytesWritten < 0) 794 return bytesWritten; 795 if (bytesWritten != bytesRead) 796 return B_IO_ERROR; 797 } 798 799 return B_OK; 800 } 801