File indexing completed on 2024-04-28 05:18:35
0001 /* 0002 SPDX-FileCopyrightText: 1996-1998 Stefan Taferner <taferner@kde.org> 0003 SPDX-FileCopyrightText: 2009 Bertjan Broeksema <broeksema@kde.org> 0004 0005 SPDX-License-Identifier: LGPL-2.0-or-later 0006 0007 NOTE: Most of the code inside here is an slightly adjusted version of 0008 kdepim/kmail/kmfoldermbox.cpp. This is why I added a line for Stefan Taferner. 0009 0010 Bertjan Broeksema, april 2009 0011 */ 0012 0013 #include "mbox.h" 0014 #include "mbox_p.h" 0015 #include "mboxentry_p.h" 0016 0017 #include "kmbox_debug.h" 0018 #include <QStandardPaths> 0019 #include <QUrl> 0020 0021 #include <QBuffer> 0022 #include <QProcess> 0023 0024 using namespace KMBox; 0025 /// public methods. 0026 0027 MBox::MBox() 0028 : d(new MBoxPrivate(this)) 0029 { 0030 // Set some sane defaults 0031 d->mFileLocked = false; 0032 d->mLockType = None; 0033 0034 d->mUnlockTimer.setInterval(0); 0035 d->mUnlockTimer.setSingleShot(true); 0036 } 0037 0038 MBox::~MBox() 0039 { 0040 if (d->mFileLocked) { 0041 unlock(); 0042 } 0043 0044 d->close(); 0045 } 0046 0047 // Appended entries works as follows: When an mbox file is loaded from disk, 0048 // d->mInitialMboxFileSize is set to the file size at that moment. New entries 0049 // are stored in memory (d->mAppendedEntries). The initial file size and the size 0050 // of the buffer determine the offset for the next message to append. 0051 MBoxEntry MBox::appendMessage(const KMime::Message::Ptr &entry) 0052 { 0053 // It doesn't make sense to add entries when we don't have an reference file. 0054 Q_ASSERT(!d->mMboxFile.fileName().isEmpty()); 0055 0056 const QByteArray rawEntry = MBoxPrivate::escapeFrom(entry->encodedContent()); 0057 0058 if (rawEntry.size() <= 0) { 0059 qCDebug(KMBOX_LOG) << "Message added to folder `" << d->mMboxFile.fileName() << "' contains no data. Ignoring it."; 0060 return MBoxEntry(); 0061 } 0062 0063 int nextOffset = d->mAppendedEntries.size(); // Offset of the appended message 0064 0065 // Make sure the byte array is large enough to check for an end character. 0066 // Then check if the required newlines are there. 0067 if (nextOffset < 1 && d->mMboxFile.size() > 0) { // Empty, add one empty line 0068 d->mAppendedEntries.append("\n"); 0069 ++nextOffset; 0070 } else if (nextOffset == 1 && d->mAppendedEntries.at(0) != '\n') { 0071 // This should actually not happen, but catch it anyway. 0072 if (d->mMboxFile.size() < 0) { 0073 d->mAppendedEntries.append("\n"); 0074 ++nextOffset; 0075 } 0076 } else if (nextOffset >= 2) { 0077 if (d->mAppendedEntries.at(nextOffset - 1) != '\n') { 0078 if (d->mAppendedEntries.at(nextOffset) != '\n') { 0079 d->mAppendedEntries.append("\n\n"); 0080 nextOffset += 2; 0081 } else { 0082 d->mAppendedEntries.append("\n"); 0083 ++nextOffset; 0084 } 0085 } 0086 } 0087 0088 const QByteArray separator = MBoxPrivate::mboxMessageSeparator(rawEntry); 0089 d->mAppendedEntries.append(separator); 0090 d->mAppendedEntries.append(rawEntry); 0091 if (rawEntry[rawEntry.size() - 1] != '\n') { 0092 d->mAppendedEntries.append("\n\n"); 0093 } else { 0094 d->mAppendedEntries.append("\n"); 0095 } 0096 0097 MBoxEntry resultEntry; 0098 resultEntry.d->mOffset = d->mInitialMboxFileSize + nextOffset; 0099 resultEntry.d->mMessageSize = rawEntry.size(); 0100 resultEntry.d->mSeparatorSize = separator.size(); 0101 d->mEntries << resultEntry; 0102 0103 return resultEntry; 0104 } 0105 0106 MBoxEntry::List MBox::entries(const MBoxEntry::List &deletedEntries) const 0107 { 0108 if (deletedEntries.isEmpty()) { 0109 // fast path 0110 return d->mEntries; 0111 } 0112 0113 MBoxEntry::List result; 0114 result.reserve(d->mEntries.size()); 0115 0116 for (const MBoxEntry &entry : std::as_const(d->mEntries)) { 0117 if (!deletedEntries.contains(entry)) { 0118 result << entry; 0119 } 0120 } 0121 0122 return result; 0123 } 0124 0125 QString MBox::fileName() const 0126 { 0127 return d->mMboxFile.fileName(); 0128 } 0129 0130 bool MBox::load(const QString &fileName) 0131 { 0132 if (d->mFileLocked) { 0133 return false; 0134 } 0135 0136 d->initLoad(fileName); 0137 0138 if (!lock()) { 0139 qCDebug(KMBOX_LOG) << "Failed to lock"; 0140 return false; 0141 } 0142 0143 d->mInitialMboxFileSize = d->mMboxFile.size(); // AFTER the file has been locked 0144 0145 QByteArray line; 0146 QByteArray prevSeparator; 0147 quint64 offs = 0; // The offset of the next message to read. 0148 0149 while (!d->mMboxFile.atEnd()) { 0150 quint64 pos = d->mMboxFile.pos(); 0151 0152 line = d->mMboxFile.readLine(); 0153 0154 // if atEnd, use mail only if there was a separator line at all, 0155 // otherwise it's not a valid mbox 0156 if (d->isMBoxSeparator(line) || (d->mMboxFile.atEnd() && (prevSeparator.size() != 0))) { 0157 // if we are the at the file end, update pos to not forget the last line 0158 if (d->mMboxFile.atEnd()) { 0159 pos = d->mMboxFile.pos(); 0160 } 0161 0162 // Found the separator or at end of file, the message starts at offs 0163 quint64 msgSize = pos - offs; 0164 0165 if (pos > 0) { 0166 // This is not the separator of the first mail in the file. If pos == 0 0167 // than we matched the separator of the first mail in the file. 0168 MBoxEntry entry; 0169 entry.d->mOffset = offs; 0170 entry.d->mSeparatorSize = prevSeparator.size(); 0171 entry.d->mMessageSize = msgSize - 1; 0172 0173 // Don't add the separator size and the newline up to the message size. 0174 entry.d->mMessageSize -= prevSeparator.size() + 1; 0175 0176 d->mEntries << entry; 0177 } 0178 0179 if (d->isMBoxSeparator(line)) { 0180 prevSeparator = line; 0181 } 0182 0183 offs += msgSize; // Mark the beginning of the next message. 0184 } 0185 } 0186 0187 // FIXME: What if unlock fails? 0188 // if no separator was found, the file is still valid if it is empty 0189 const bool val = unlock() && (!prevSeparator.isEmpty() || (d->mMboxFile.size() == 0)); 0190 return val; 0191 } 0192 0193 bool MBox::lock() 0194 { 0195 if (d->mMboxFile.fileName().isEmpty()) { 0196 return false; // We cannot lock if there is no file loaded. 0197 } 0198 0199 // We can't load another file when the mbox currently is locked so if d->mFileLocked 0200 // is true atm just return true. 0201 if (locked()) { 0202 return true; 0203 } 0204 0205 if (d->mLockType == None) { 0206 d->mFileLocked = true; 0207 if (d->open()) { 0208 d->startTimerIfNeeded(); 0209 return true; 0210 } 0211 0212 d->mFileLocked = false; 0213 return false; 0214 } 0215 0216 QStringList args; 0217 int rc = 0; 0218 0219 switch (d->mLockType) { 0220 case ProcmailLockfile: 0221 args << QStringLiteral("-l20") << QStringLiteral("-r5"); 0222 if (!d->mLockFileName.isEmpty()) { 0223 args << QString::fromLocal8Bit(QFile::encodeName(d->mLockFileName)); 0224 } else { 0225 args << QString::fromLocal8Bit(QFile::encodeName(d->mMboxFile.fileName() + QLatin1StringView(".lock"))); 0226 } 0227 0228 rc = QProcess::execute(QStringLiteral("lockfile"), args); 0229 if (rc != 0) { 0230 qCDebug(KMBOX_LOG) << "lockfile -l20 -r5 " << d->mMboxFile.fileName() << ": Failed (" << rc << ") switching to read only mode"; 0231 d->mReadOnly = true; // In case the MBox object was created read/write we 0232 // set it to read only when locking failed. 0233 } else { 0234 d->mFileLocked = true; 0235 } 0236 break; 0237 0238 case MuttDotlock: 0239 args << QString::fromLocal8Bit(QFile::encodeName(d->mMboxFile.fileName())); 0240 rc = QProcess::execute(QStringLiteral("mutt_dotlock"), args); 0241 0242 if (rc != 0) { 0243 qCDebug(KMBOX_LOG) << "mutt_dotlock " << d->mMboxFile.fileName() << ": Failed (" << rc << ") switching to read only mode"; 0244 d->mReadOnly = true; // In case the MBox object was created read/write we 0245 // set it to read only when locking failed. 0246 } else { 0247 d->mFileLocked = true; 0248 } 0249 break; 0250 0251 case MuttDotlockPrivileged: 0252 args << QStringLiteral("-p") << QString::fromLocal8Bit(QFile::encodeName(d->mMboxFile.fileName())); 0253 rc = QProcess::execute(QStringLiteral("mutt_dotlock"), args); 0254 0255 if (rc != 0) { 0256 qCDebug(KMBOX_LOG) << "mutt_dotlock -p " << d->mMboxFile.fileName() << ":" 0257 << ": Failed (" << rc << ") switching to read only mode"; 0258 d->mReadOnly = true; 0259 } else { 0260 d->mFileLocked = true; 0261 } 0262 break; 0263 0264 case None: 0265 d->mFileLocked = true; 0266 break; 0267 default: 0268 break; 0269 } 0270 0271 if (d->mFileLocked) { 0272 if (!d->open()) { 0273 const bool unlocked = unlock(); 0274 Q_ASSERT(unlocked); // If this fails we're in trouble. 0275 Q_UNUSED(unlocked) 0276 } 0277 } 0278 0279 d->startTimerIfNeeded(); 0280 return d->mFileLocked; 0281 } 0282 0283 bool MBox::locked() const 0284 { 0285 return d->mFileLocked; 0286 } 0287 0288 static bool lessThanByOffset(const MBoxEntry &left, const MBoxEntry &right) 0289 { 0290 return left.messageOffset() < right.messageOffset(); 0291 } 0292 0293 bool MBox::purge(const MBoxEntry::List &deletedEntries, QList<MBoxEntry::Pair> *movedEntries) 0294 { 0295 if (d->mMboxFile.fileName().isEmpty() || d->mReadOnly) { 0296 return false; // No file loaded yet or it's readOnly 0297 } 0298 0299 if (deletedEntries.isEmpty()) { 0300 return true; // Nothing to do. 0301 } 0302 0303 if (!lock()) { 0304 return false; 0305 } 0306 0307 for (const MBoxEntry &entry : std::as_const(deletedEntries)) { 0308 d->mMboxFile.seek(entry.messageOffset()); 0309 const QByteArray line = d->mMboxFile.readLine(); 0310 0311 if (!d->isMBoxSeparator(line)) { 0312 qCDebug(KMBOX_LOG) << "Found invalid separator at:" << entry.messageOffset(); 0313 unlock(); 0314 return false; // The file is messed up or the index is incorrect. 0315 } 0316 } 0317 0318 // All entries are deleted, so just resize the file to a size of 0. 0319 if (deletedEntries.size() == d->mEntries.size()) { 0320 d->mEntries.clear(); 0321 d->mMboxFile.resize(0); 0322 qCDebug(KMBOX_LOG) << "Purge completed successfully, unlocking the file."; 0323 return unlock(); 0324 } 0325 0326 std::sort(d->mEntries.begin(), d->mEntries.end(), lessThanByOffset); 0327 quint64 writeOffset = 0; 0328 bool writeOffSetInitialized = false; 0329 MBoxEntry::List resultingEntryList; 0330 QList<MBoxEntry::Pair> tmpMovedEntries; 0331 0332 quint64 origFileSize = d->mMboxFile.size(); 0333 0334 QListIterator<MBoxEntry> i(d->mEntries); 0335 while (i.hasNext()) { 0336 MBoxEntry entry = i.next(); 0337 0338 if (deletedEntries.contains(entry) && !writeOffSetInitialized) { 0339 writeOffset = entry.messageOffset(); 0340 writeOffSetInitialized = true; 0341 } else if (writeOffSetInitialized && writeOffset < entry.messageOffset() && !deletedEntries.contains(entry)) { 0342 // The current message doesn't have to be deleted, but must be moved. 0343 // First determine the size of the entry that must be moved. 0344 quint64 entrySize = 0; 0345 if (i.hasNext()) { 0346 entrySize = i.next().messageOffset() - entry.messageOffset(); 0347 i.previous(); // Go back to make sure that we also handle the next entry. 0348 } else { 0349 entrySize = origFileSize - entry.messageOffset(); 0350 } 0351 0352 Q_ASSERT(entrySize > 0); // MBox entries really cannot have a size <= 0; 0353 0354 // we map the whole area of the file starting at the writeOffset up to the 0355 // message that have to be moved into memory. This includes eventually the 0356 // messages that are the deleted between the first deleted message 0357 // encountered and the message that has to be moved. 0358 quint64 mapSize = entry.messageOffset() + entrySize - writeOffset; 0359 0360 // Now map writeOffSet + mapSize into mem. 0361 uchar *memArea = d->mMboxFile.map(writeOffset, mapSize); 0362 0363 // Now read the entry that must be moved to writeOffset. 0364 quint64 startOffset = entry.messageOffset() - writeOffset; 0365 memmove(memArea, memArea + startOffset, entrySize); 0366 0367 d->mMboxFile.unmap(memArea); 0368 0369 MBoxEntry resultEntry; 0370 resultEntry.d->mOffset = writeOffset; 0371 resultEntry.d->mSeparatorSize = entry.separatorSize(); 0372 resultEntry.d->mMessageSize = entry.messageSize(); 0373 0374 resultingEntryList << resultEntry; 0375 tmpMovedEntries << MBoxEntry::Pair(MBoxEntry(entry.messageOffset()), MBoxEntry(resultEntry.messageOffset())); 0376 writeOffset += entrySize; 0377 } else if (!deletedEntries.contains(entry)) { 0378 // Unmoved and not deleted entry, can only ocure before the first deleted 0379 // entry. 0380 Q_ASSERT(!writeOffSetInitialized); 0381 resultingEntryList << entry; 0382 } 0383 } 0384 0385 // Chop off remaining entry bits. 0386 d->mMboxFile.resize(writeOffset); 0387 d->mEntries = resultingEntryList; 0388 0389 qCDebug(KMBOX_LOG) << "Purge completed successfully, unlocking the file."; 0390 if (movedEntries) { 0391 *movedEntries = tmpMovedEntries; 0392 } 0393 return unlock(); // FIXME: What if this fails? It will return false but the 0394 // file has changed. 0395 } 0396 0397 QByteArray MBox::readRawMessage(const MBoxEntry &entry) 0398 { 0399 const bool wasLocked = locked(); 0400 if (!wasLocked) { 0401 if (!lock()) { 0402 return QByteArray(); 0403 } 0404 } 0405 0406 // TODO: Add error handling in case locking failed. 0407 0408 quint64 offset = entry.messageOffset(); 0409 0410 Q_ASSERT(d->mFileLocked); 0411 Q_ASSERT(d->mMboxFile.isOpen()); 0412 Q_ASSERT((d->mInitialMboxFileSize + d->mAppendedEntries.size()) > offset); 0413 0414 QByteArray message; 0415 0416 if (offset < d->mInitialMboxFileSize) { 0417 d->mMboxFile.seek(offset); 0418 0419 QByteArray line = d->mMboxFile.readLine(); 0420 0421 if (!d->isMBoxSeparator(line)) { 0422 qCDebug(KMBOX_LOG) << "[MBox::readEntry] Invalid entry at:" << offset; 0423 if (!wasLocked) { 0424 unlock(); 0425 } 0426 return QByteArray(); // The file is messed up or the index is incorrect. 0427 } 0428 0429 line = d->mMboxFile.readLine(); 0430 while (!d->isMBoxSeparator(line)) { 0431 message += line; 0432 if (d->mMboxFile.atEnd()) { 0433 break; 0434 } 0435 line = d->mMboxFile.readLine(); 0436 } 0437 } else { 0438 offset -= d->mInitialMboxFileSize; 0439 if (offset > static_cast<quint64>(d->mAppendedEntries.size())) { 0440 if (!wasLocked) { 0441 unlock(); 0442 } 0443 return QByteArray(); 0444 } 0445 0446 QBuffer buffer(&(d->mAppendedEntries)); 0447 buffer.open(QIODevice::ReadOnly); 0448 buffer.seek(offset); 0449 0450 QByteArray line = buffer.readLine(); 0451 0452 if (!d->isMBoxSeparator(line)) { 0453 qCDebug(KMBOX_LOG) << "[MBox::readEntry] Invalid appended entry at:" << offset; 0454 if (!wasLocked) { 0455 unlock(); 0456 } 0457 return QByteArray(); // The file is messed up or the index is incorrect. 0458 } 0459 0460 line = buffer.readLine(); 0461 while (!d->isMBoxSeparator(line) && !buffer.atEnd()) { 0462 message += line; 0463 line = buffer.readLine(); 0464 } 0465 } 0466 0467 // Remove the last '\n' added by writeEntry. 0468 if (message.endsWith('\n')) { 0469 message.chop(1); 0470 } 0471 0472 MBoxPrivate::unescapeFrom(message.data(), message.size()); 0473 0474 if (!wasLocked) { 0475 if (!d->startTimerIfNeeded()) { 0476 const bool unlocked = unlock(); 0477 Q_ASSERT(unlocked); 0478 Q_UNUSED(unlocked) 0479 } 0480 } 0481 0482 return message; 0483 } 0484 0485 KMime::Message *MBox::readMessage(const MBoxEntry &entry) 0486 { 0487 const QByteArray message = readRawMessage(entry); 0488 if (message.isEmpty()) { 0489 return nullptr; 0490 } 0491 0492 auto mail = new KMime::Message(); 0493 mail->setContent(KMime::CRLFtoLF(message)); 0494 mail->parse(); 0495 0496 return mail; 0497 } 0498 0499 QByteArray MBox::readMessageHeaders(const MBoxEntry &entry) 0500 { 0501 const bool wasLocked = d->mFileLocked; 0502 if (!wasLocked) { 0503 if (!lock()) { 0504 qCDebug(KMBOX_LOG) << "Failed to lock"; 0505 return QByteArray(); 0506 } 0507 } 0508 0509 const quint64 offset = entry.messageOffset(); 0510 0511 Q_ASSERT(d->mFileLocked); 0512 Q_ASSERT(d->mMboxFile.isOpen()); 0513 Q_ASSERT((d->mInitialMboxFileSize + d->mAppendedEntries.size()) > offset); 0514 0515 QByteArray headers; 0516 if (offset < d->mInitialMboxFileSize) { 0517 d->mMboxFile.seek(offset); 0518 QByteArray line = d->mMboxFile.readLine(); 0519 0520 while (line[0] != '\n' && !d->mMboxFile.atEnd()) { 0521 headers += line; 0522 line = d->mMboxFile.readLine(); 0523 } 0524 } else { 0525 QBuffer buffer(&(d->mAppendedEntries)); 0526 buffer.open(QIODevice::ReadOnly); 0527 buffer.seek(offset - d->mInitialMboxFileSize); 0528 QByteArray line = buffer.readLine(); 0529 0530 while (line[0] != '\n' && !buffer.atEnd()) { 0531 headers += line; 0532 line = buffer.readLine(); 0533 } 0534 } 0535 0536 if (!wasLocked) { 0537 unlock(); 0538 } 0539 0540 return headers; 0541 } 0542 0543 bool MBox::save(const QString &fileName) 0544 { 0545 if (!fileName.isEmpty() && QUrl::fromUserInput(fileName).toLocalFile() != d->mMboxFile.fileName()) { 0546 if (!d->mMboxFile.copy(fileName)) { 0547 return false; 0548 } else { 0549 // if the original file was read-only, also the copied file is read-only 0550 // Let's make it writable now 0551 QFile::setPermissions(fileName, d->mMboxFile.permissions() | QFile::WriteOwner); 0552 } 0553 0554 if (d->mAppendedEntries.isEmpty()) { 0555 return true; // Nothing to do 0556 } 0557 0558 QFile otherFile(fileName); 0559 Q_ASSERT(otherFile.exists()); 0560 if (!otherFile.open(QIODevice::ReadWrite)) { 0561 return false; 0562 } 0563 0564 otherFile.seek(d->mMboxFile.size()); 0565 otherFile.write(d->mAppendedEntries); 0566 0567 // Don't clear mAppendedEntries and don't update mInitialFileSize. These 0568 // are still valid for the original file. 0569 return true; 0570 } 0571 0572 if (d->mReadOnly) { 0573 return false; 0574 } 0575 0576 if (d->mAppendedEntries.isEmpty()) { 0577 return true; // Nothing to do. 0578 } 0579 0580 if (!lock()) { 0581 return false; 0582 } 0583 0584 Q_ASSERT(d->mMboxFile.isOpen()); 0585 0586 d->mMboxFile.seek(d->mMboxFile.size()); 0587 d->mMboxFile.write(d->mAppendedEntries); 0588 d->mAppendedEntries.clear(); 0589 d->mInitialMboxFileSize = d->mMboxFile.size(); 0590 return unlock(); 0591 } 0592 0593 bool MBox::setLockType(LockType ltype) 0594 { 0595 if (d->mFileLocked) { 0596 qCDebug(KMBOX_LOG) << "File is currently locked."; 0597 return false; // Don't change the method if the file is currently locked. 0598 } 0599 0600 switch (ltype) { 0601 case ProcmailLockfile: 0602 if (QStandardPaths::findExecutable(QStringLiteral("lockfile")).isEmpty()) { 0603 qCDebug(KMBOX_LOG) << "Could not find the lockfile executable"; 0604 return false; 0605 } 0606 break; 0607 case MuttDotlock: // fall through 0608 case MuttDotlockPrivileged: 0609 if (QStandardPaths::findExecutable(QStringLiteral("mutt_dotlock")).isEmpty()) { 0610 qCDebug(KMBOX_LOG) << "Could not find the mutt_dotlock executable"; 0611 return false; 0612 } 0613 break; 0614 default: 0615 break; // We assume fcntl available and lock_none doesn't need a check. 0616 } 0617 0618 d->mLockType = ltype; 0619 return true; 0620 } 0621 0622 void MBox::setLockFile(const QString &lockFile) 0623 { 0624 d->mLockFileName = lockFile; 0625 } 0626 0627 void MBox::setUnlockTimeout(int msec) 0628 { 0629 d->mUnlockTimer.setInterval(msec); 0630 } 0631 0632 bool MBox::unlock() 0633 { 0634 if (d->mLockType == None && !d->mFileLocked) { 0635 d->mFileLocked = false; 0636 d->mMboxFile.close(); 0637 return true; 0638 } 0639 0640 int rc = 0; 0641 QStringList args; 0642 0643 switch (d->mLockType) { 0644 case ProcmailLockfile: 0645 // QFile::remove returns true on success so negate the result. 0646 if (!d->mLockFileName.isEmpty()) { 0647 rc = !QFile(d->mLockFileName).remove(); 0648 } else { 0649 rc = !QFile(d->mMboxFile.fileName() + QLatin1StringView(".lock")).remove(); 0650 } 0651 break; 0652 0653 case MuttDotlock: 0654 args << QStringLiteral("-u") << QString::fromLocal8Bit(QFile::encodeName(d->mMboxFile.fileName())); 0655 rc = QProcess::execute(QStringLiteral("mutt_dotlock"), args); 0656 break; 0657 0658 case MuttDotlockPrivileged: 0659 args << QStringLiteral("-u") << QStringLiteral("-p") << QString::fromLocal8Bit(QFile::encodeName(d->mMboxFile.fileName())); 0660 rc = QProcess::execute(QStringLiteral("mutt_dotlock"), args); 0661 break; 0662 0663 case None: // Fall through. 0664 default: 0665 break; 0666 } 0667 0668 if (rc == 0) { // Unlocking succeeded 0669 d->mFileLocked = false; 0670 } 0671 0672 d->mMboxFile.close(); 0673 0674 return !d->mFileLocked; 0675 } 0676 0677 void MBox::setReadOnly(bool ro) 0678 { 0679 d->mReadOnly = ro; 0680 } 0681 0682 bool MBox::isReadOnly() const 0683 { 0684 return d->mReadOnly; 0685 }