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 }