service/ImapService.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "ImapService.h" | ||
| 2 | #include "ImapCommandTable.h" | ||
| 3 | #include "ImapConnectionSetup.h" | ||
| 4 | #include "ImapLineReader.h" | ||
| 5 | #include "ImapPushSession.h" | ||
| 6 | #include "ImapBodyFetch.h" | ||
| 7 | #include "ImapMutationCommands.h" | ||
| 8 | #include "ImapAppendStream.h" | ||
| 9 | #include "ImapResponseParser.h" | ||
| 10 | |||
| 11 | #include <QLoggingCategory> | ||
| 12 | #include <QMetaEnum> | ||
| 13 | #include <QRegularExpression> | ||
| 14 | #include <limits> | ||
| 15 | #include <QSet> | ||
| 16 | #include <QSettings> | ||
| 17 | #include <QSocketNotifier> | ||
| 18 | |||
| 19 | #include <algorithm> | ||
| 20 | #include <utility> | ||
| 21 | #include <chrono> | ||
| 22 | |||
| 23 | // The header fields every header sync requests besides the ENVELOPE: | ||
| 24 | // threading, List-Id (rules, search) and the server spam filter's fields | ||
| 25 | // (Sprint 105 S105.2.2: SpamAssassin and Rspamd score lines, SpamHeaders). | ||
| 26 | static const QString kHeaderFieldsItem = QStringLiteral( | ||
| 27 | "BODY.PEEK[HEADER.FIELDS (In-Reply-To References List-Id X-Spam X-Spam-Status X-Spam-Flag " | ||
| 28 | "X-Spam-Score X-Spam-Level X-Spamd-Result X-Rspamd-Score X-Rspamd-Action)]"); | ||
| 29 | |||
| 30 |
4/7✓ Branch 2 → 3 taken 32 times.
✓ Branch 2 → 8 taken 105300 times.
✓ Branch 4 → 5 taken 32 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 32 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
|
105332 | Q_LOGGING_CATEGORY(lcImap, "mailjd.imap") |
| 31 |
4/7✓ Branch 2 → 3 taken 10 times.
✓ Branch 2 → 8 taken 366 times.
✓ Branch 4 → 5 taken 10 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 10 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
|
376 | Q_LOGGING_CATEGORY(lcImapTiming, "mailjd.imap.timing") |
| 32 | |||
| 33 | 1401 | ImapService::ImapService(QObject *parent) | |
| 34 |
2/5✓ Branch 4 → 5 taken 1401 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 1401 times.
✗ Branch 88 → 89 not taken.
✗ Branch 88 → 90 not taken.
|
1401 | : QObject(parent), m_socket(new QSslSocket(this)), |
| 35 |
1/1✓ Branch 8 → 9 taken 1401 times.
|
1401 | m_commands(createCommandTable()), |
| 36 |
1/1✓ Branch 9 → 10 taken 1401 times.
|
1401 | m_setup(createConnectionSetup()), |
| 37 |
1/1✓ Branch 10 → 11 taken 1401 times.
|
1401 | m_push(createPushSession()), |
| 38 |
1/1✓ Branch 11 → 12 taken 1401 times.
|
1401 | m_body(createBodyFetch()), |
| 39 |
1/1✓ Branch 12 → 13 taken 1401 times.
|
1401 | m_mutations(createMutationCommands()), |
| 40 |
1/1✓ Branch 13 → 14 taken 1401 times.
|
1401 | m_append(createAppendStream()), |
| 41 |
2/2✓ Branch 3 → 4 taken 1401 times.
✓ Branch 23 → 24 taken 1401 times.
|
4203 | m_reader(createLineReader()) { |
| 42 |
1/1✓ Branch 26 → 27 taken 1401 times.
|
1401 | m_setup->setupTimer()->setSingleShot(true); |
| 43 |
1/1✓ Branch 29 → 30 taken 1401 times.
|
1401 | m_commands->commandTimeoutTimer()->setSingleShot(true); |
| 44 |
1/1✓ Branch 32 → 33 taken 1401 times.
|
1401 | m_commands->commandDeadlineTimer()->setSingleShot(true); |
| 45 |
1/1✓ Branch 35 → 36 taken 1401 times.
|
1401 | m_commands->commandDeadlineTimer()->setTimerType(Qt::PreciseTimer); |
| 46 |
1/1✓ Branch 38 → 39 taken 1401 times.
|
1401 | m_push->idleRenewTimer()->setSingleShot(true); |
| 47 |
1/1✓ Branch 41 → 42 taken 1401 times.
|
1401 | m_push->livenessProbeWatchdog()->setSingleShot(true); |
| 48 |
1/1✓ Branch 44 → 45 taken 1401 times.
|
1401 | m_push->idleRenewWatchdog()->setSingleShot(true); |
| 49 | |||
| 50 | // T-720/T-72.1: Enable SO_KEEPALIVE at the Qt level. Native interval | ||
| 51 | // tuning is applied in tuneKeepAlive() once the socket descriptor is | ||
| 52 | // valid (onConnected()/onEncrypted()). assertable on an unconnected | ||
| 53 | // socket per the sprint plan. | ||
| 54 |
1/1✓ Branch 46 → 47 taken 1401 times.
|
1401 | m_socket->setSocketOption(QAbstractSocket::KeepAliveOption, 1); |
| 55 | // Keep encrypted socket buffering bounded as well as our parser buffer. | ||
| 56 | // Literals are consumed incrementally below, so this does not constrain | ||
| 57 | // legitimate messages up to the configured per-message limit. | ||
| 58 |
1/1✓ Branch 48 → 49 taken 1401 times.
|
1401 | m_socket->setReadBufferSize(MAX_SOCKET_READ_BUFFER_SIZE); |
| 59 | |||
| 60 |
1/1✓ Branch 49 → 50 taken 1401 times.
|
1459 | connect(m_socket, &QSslSocket::connected, this, [this] { m_setup->onConnected(); }); |
| 61 |
1/1✓ Branch 51 → 52 taken 1401 times.
|
1452 | connect(m_socket, &QSslSocket::encrypted, this, [this] { m_setup->onEncrypted(); }); |
| 62 |
1/1✓ Branch 53 → 54 taken 1401 times.
|
4335 | connect(m_socket, &QSslSocket::readyRead, this, [this] { m_reader->onReadyRead(); }); |
| 63 | 1401 | connect(m_socket, &QSslSocket::bytesWritten, this, | |
| 64 |
1/1✓ Branch 55 → 56 taken 1401 times.
|
1771 | [this](qint64) { m_append->scheduleAppendLiteralPump(); }); |
| 65 | 1401 | connect(m_socket, &QAbstractSocket::errorOccurred, this, | |
| 66 |
1/1✓ Branch 57 → 58 taken 1401 times.
|
1401 | &ImapService::onSocketError); |
| 67 | 1401 | connect(m_socket, &QAbstractSocket::disconnected, this, | |
| 68 |
1/1✓ Branch 59 → 60 taken 1401 times.
|
1451 | [this] { m_setup->clearCredentials(); }); |
| 69 | // Bug 33: Log and forward SSL certificate errors | ||
| 70 | 1401 | connect(m_socket, &QSslSocket::sslErrors, this, | |
| 71 |
1/1✓ Branch 61 → 62 taken 1401 times.
|
1401 | [this](const QList<QSslError> &errors) { |
| 72 |
2/2✓ Branch 20 → 4 taken 81 times.
✓ Branch 20 → 21 taken 41 times.
|
122 | for (const auto &e : errors) |
| 73 |
7/7✓ Branch 5 → 6 taken 81 times.
✓ Branch 9 → 10 taken 81 times.
✓ Branch 10 → 11 taken 81 times.
✓ Branch 11 → 12 taken 81 times.
✓ Branch 12 → 13 taken 81 times.
✓ Branch 17 → 7 taken 81 times.
✓ Branch 17 → 18 taken 81 times.
|
162 | qCWarning(lcImap) << "SSL error:" << e.errorString(); |
| 74 | // E2E testing: GreenMail uses a self-signed certificate. | ||
| 75 | // Qt rejects self-signed certs even if they're in the system | ||
| 76 | // trust store. Allow bypassing SSL errors via environment variable. | ||
| 77 | // T-607/SEC-03: SSL error bypass is now compile-time only. | ||
| 78 | // Only available when built with -DBUILD_E2E_TESTS=ON | ||
| 79 | // (CMake sets the MAILJD_E2E_TESTING define). | ||
| 80 | #ifdef MAILJD_E2E_TESTING | ||
| 81 |
5/5✓ Branch 21 → 22 taken 41 times.
✓ Branch 25 → 26 taken 41 times.
✓ Branch 26 → 27 taken 41 times.
✓ Branch 30 → 23 taken 41 times.
✓ Branch 30 → 31 taken 41 times.
|
82 | qCWarning(lcImap) << "Ignoring SSL errors (E2E test build)"; |
| 82 | 41 | m_socket->ignoreSslErrors(); | |
| 83 | #endif | ||
| 84 | 41 | }); | |
| 85 |
1/1✓ Branch 65 → 66 taken 1401 times.
|
1401 | connect(m_setup->setupTimer(), &QTimer::timeout, this, &ImapService::onTimeout); |
| 86 |
1/1✓ Branch 69 → 70 taken 1401 times.
|
1401 | connect(m_commands->commandTimeoutTimer(), &QTimer::timeout, this, |
| 87 | 1401 | [this] { m_commands->onCommandTimeout(); }); | |
| 88 |
1/1✓ Branch 73 → 74 taken 1401 times.
|
1401 | connect(m_commands->commandDeadlineTimer(), &QTimer::timeout, this, |
| 89 | 1401 | [this] { m_commands->onCommandDeadline(); }); | |
| 90 |
1/1✓ Branch 77 → 78 taken 1401 times.
|
1401 | connect(m_push->idleRenewTimer(), &QTimer::timeout, this, [this] { m_push->onIdleRenew(); }); |
| 91 | // T-720: Watchdog handlers — both end in failConnection() so a probe | ||
| 92 | // or IDLE renew that the server never ACKs cannot stall the monitor. | ||
| 93 |
1/1✓ Branch 81 → 82 taken 1401 times.
|
1401 | connect(m_push->livenessProbeWatchdog(), &QTimer::timeout, this, |
| 94 | 1401 | [this] { m_push->onLivenessProbeTimeout(); }); | |
| 95 |
1/1✓ Branch 85 → 86 taken 1401 times.
|
1401 | connect(m_push->idleRenewWatchdog(), &QTimer::timeout, this, |
| 96 | 1401 | [this] { m_push->onIdleRenewWatchdogTimeout(); }); | |
| 97 | 1401 | } | |
| 98 | |||
| 99 | 35 | quint64 ImapService::appendMessage(const QString &folder, const QByteArray &rfcMessage, | |
| 100 | const QString &flags, qint64 mutationToken) { | ||
| 101 | 35 | return m_append->appendMessage(folder, rfcMessage, flags, mutationToken); | |
| 102 | } | ||
| 103 | 4 | quint64 ImapService::reserveAppendRequestId() { return m_append->reserveAppendRequestId(); } | |
| 104 | 10 | void ImapService::appendMessage(quint64 requestId, const QString &folder, | |
| 105 | const QByteArray &rfcMessage, const QString &flags, | ||
| 106 | qint64 mutationToken) { | ||
| 107 | 10 | m_append->appendMessage(requestId, folder, rfcMessage, flags, mutationToken); | |
| 108 | 10 | } | |
| 109 | |||
| 110 | 1 | void ImapService::markSeen(qint64 uid) { m_mutations->markSeen(uid); } | |
| 111 | 1 | void ImapService::markUnseen(qint64 uid) { m_mutations->markUnseen(uid); } | |
| 112 | 33 | void ImapService::storeFlag(qint64 uid, const QString &flag, bool add, qint64 mutationToken) { | |
| 113 | 33 | m_mutations->storeFlag(uid, flag, add, mutationToken); | |
| 114 | 33 | } | |
| 115 | 7 | void ImapService::moveMessage(qint64 uid, const QString &targetFolder, qint64 mutationToken) { | |
| 116 | 7 | m_mutations->moveMessage(uid, targetFolder, mutationToken); | |
| 117 | 7 | } | |
| 118 | 16 | void ImapService::moveMessages(const QList<qint64> &uids, const QString &targetFolder, | |
| 119 | qint64 mutationToken) { | ||
| 120 | 16 | m_mutations->moveMessages(uids, targetFolder, mutationToken); | |
| 121 | 16 | } | |
| 122 | 9 | void ImapService::moveMessageSet(const QList<qint64> &uids, const QString &targetFolder, | |
| 123 | const QList<qint64> &mutationTokens) { | ||
| 124 | 9 | m_mutations->moveMessageSet(uids, targetFolder, mutationTokens); | |
| 125 | 9 | } | |
| 126 | 6 | void ImapService::copyMessage(qint64 uid, const QString &targetFolder, qint64 mutationToken) { | |
| 127 | 6 | m_mutations->copyMessage(uid, targetFolder, mutationToken); | |
| 128 | 6 | } | |
| 129 | 12 | void ImapService::expungeUids(const QList<qint64> &uids, const QString &folder, | |
| 130 | quint32 uidValidity) { | ||
| 131 | 12 | m_mutations->expungeUids(uids, folder, uidValidity); | |
| 132 | 12 | } | |
| 133 | 4 | bool ImapService::hasUidExpungeCapability() const { | |
| 134 | 4 | return m_mutations->hasUidExpungeCapability(); | |
| 135 | } | ||
| 136 | |||
| 137 | 3 | void ImapService::fetchBody(qint64 uid) { m_body->fetchBody(uid); } | |
| 138 | 34 | void ImapService::fetchBody(qint64 uid, qint64 maxBytes) { m_body->fetchBody(uid, maxBytes); } | |
| 139 | 3 | void ImapService::requestBody(quint64 requestId, qint64 uid) { | |
| 140 | 3 | m_body->requestBody(requestId, uid); | |
| 141 | 3 | } | |
| 142 | 182 | qint64 ImapService::configuredMaxMessageBytes() { | |
| 143 | 182 | return ImapBodyFetch::configuredMaxMessageBytes(); | |
| 144 | } | ||
| 145 | 3 | void ImapService::selectAndFetchBody(const QString &folderPath, qint64 uid) { | |
| 146 | 3 | m_body->selectAndFetchBody(folderPath, uid); | |
| 147 | 3 | } | |
| 148 | 15 | void ImapService::requestBodyInFolder(quint64 requestId, const QString &folderPath, | |
| 149 | qint64 uid) { | ||
| 150 | 15 | m_body->requestBodyInFolder(requestId, folderPath, uid); | |
| 151 | 15 | } | |
| 152 | #ifdef MAILJD_E2E_TESTING | ||
| 153 | 1 | void ImapService::setNextBodyCompletionForE2e(int delayMs, bool fail, | |
| 154 | const QString &folderPath, qint64 uid) { | ||
| 155 | 1 | m_body->setNextBodyCompletionForE2e(delayMs, fail, folderPath, uid); | |
| 156 | 1 | } | |
| 157 | #endif | ||
| 158 | |||
| 159 | 78 | void ImapService::startIdle() { m_push->startIdle(); } | |
| 160 | 4 | void ImapService::stopIdle() { m_push->stopIdle(); } | |
| 161 | 62 | bool ImapService::isIdling() const { return m_push->isIdling(); } | |
| 162 | 9 | void ImapService::startNotify(const QStringList &subscribedFolders) { | |
| 163 | 9 | m_push->startNotify(subscribedFolders); | |
| 164 | 9 | } | |
| 165 | 6 | void ImapService::stopNotify() { m_push->stopNotify(); } | |
| 166 | 82 | bool ImapService::isNotifying() const { return m_push->isNotifying(); } | |
| 167 | 97 | bool ImapService::usesNotifyPush() const { return m_push->usesNotifyPush(); } | |
| 168 | 3 | void ImapService::setAllowNotifyPush(bool allow) { m_push->allowNotifyPush() = allow; } | |
| 169 | 1 | void ImapService::executeAfterNotify(std::function<void()> command) { | |
| 170 |
1/1✓ Branch 5 → 6 taken 1 time.
|
1 | m_push->executeAfterNotify(std::move(command)); |
| 171 | 1 | } | |
| 172 | 84 | void ImapService::setAutoIdle(bool enabled) { m_push->autoIdle() = enabled; } | |
| 173 | 4 | void ImapService::sendNoop() { m_push->sendNoop(); } | |
| 174 | 24 | void ImapService::requestLivenessProbe(const QString &reason) { m_push->requestLivenessProbe(reason); } | |
| 175 | 4 | void ImapService::verifyAfterPathFailure(const QString &reason) { m_push->verifyAfterPathFailure(reason); } | |
| 176 | 7 | bool ImapService::isLivenessProbeInFlight() const { return !m_push->probeTag().isEmpty(); } | |
| 177 | |||
| 178 | 3 | bool ImapService::keepAliveTuned() const { return m_setup->keepAliveTuned(); } | |
| 179 | |||
| 180 | ✗ | quint64 ImapService::receivedBytes() const { return m_reader->receivedBytes(); } | |
| 181 | |||
| 182 | 2548 | ImapService::~ImapService() { | |
| 183 | // Block signals to prevent stateChanged emission during destruction. | ||
| 184 | // Without this, disconnect() → setState(Disconnected) → emits stateChanged, | ||
| 185 | // which can invoke connected lambdas on already-destroyed parent objects | ||
| 186 | // (e.g. MainWindow members) causing use-after-free crashes. | ||
| 187 | 1401 | blockSignals(true); | |
| 188 | 1401 | disconnect(DisconnectReason::ApplicationShutdown); | |
| 189 | 2548 | } | |
| 190 | |||
| 191 | 76 | void ImapService::connectToServer(const ImapConfig &config) { | |
| 192 |
1/1✓ Branch 6 → 7 taken 76 times.
|
152 | m_mutations->completeOutstandingMutations(QStringLiteral("Connection replaced")); |
| 193 |
4/4✓ Branch 9 → 10 taken 7 times.
✓ Branch 9 → 22 taken 69 times.
✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 22 taken 5 times.
|
76 | if (m_state != State::Disconnected && m_state != State::Error) { |
| 194 |
5/5✓ Branch 11 → 12 taken 2 times.
✓ Branch 15 → 16 taken 2 times.
✓ Branch 16 → 17 taken 2 times.
✓ Branch 20 → 13 taken 2 times.
✓ Branch 20 → 21 taken 2 times.
|
4 | qCWarning(lcImap) << "Already connected or connecting"; |
| 195 | 2 | return; | |
| 196 | } | ||
| 197 | |||
| 198 | 74 | emit connectionAttemptStarted(); | |
| 199 | |||
| 200 | // A reconnect must not overwrite a credential copy left by the previous | ||
| 201 | // attempt without zeroing it first. | ||
| 202 | 74 | m_setup->clearCredentials(); | |
| 203 | 74 | m_setup->config() = config; | |
| 204 | 74 | m_setup->keepAliveTuned() = false; | |
| 205 | 74 | m_commands->tagCounter() = 0; | |
| 206 | 74 | m_reader->resetFraming(); | |
| 207 | // Clear any queued work left over from a previous (failed) session so a | ||
| 208 | // reconnect from Error does not replay stale commands. | ||
| 209 | 74 | m_commands->clearCommands(); | |
| 210 | 74 | m_body->cancelParkedRequests(); // S106-001: the dropped queue held them | |
| 211 | 74 | m_queuedStatusFolders.clear(); | |
| 212 | 74 | m_push->resetRoundTrip(); // S92.5.3: a new path may have another RTT | |
| 213 | 74 | m_push->idleStartPending() = false; | |
| 214 | 74 | m_pendingFolders.clear(); | |
| 215 | 74 | m_pendingHeaders.clear(); | |
| 216 | 74 | m_pendingFlags.clear(); | |
| 217 | 74 | m_pendingFlagSnapshots.clear(); | |
| 218 | 74 | m_pendingSearchUids.clear(); | |
| 219 | 74 | m_capabilities.clear(); | |
| 220 | 74 | m_setup->pendingCapabilities().clear(); | |
| 221 | 74 | m_setup->setupPhase() = SetupPhase::AwaitingGreeting; | |
| 222 | 74 | m_selectedFolder.clear(); | |
| 223 | 74 | m_selectedMessageCount = 0; | |
| 224 | 74 | m_selectedUidValidity = 0; | |
| 225 | // T-79.A1/H1: a stale APPEND payload must never survive into a new session, | ||
| 226 | // or the next "+" continuation writes mail text into the command stream. | ||
| 227 | 74 | m_append->resetForSession(); | |
| 228 | 74 | m_body->resetActiveRequest(); | |
| 229 | 74 | m_push->isIdling() = false; | |
| 230 | 74 | m_push->idleTag().clear(); | |
| 231 | 74 | m_push->isNotifying() = false; | |
| 232 | 74 | m_push->notifyTag().clear(); | |
| 233 | |||
| 234 | 74 | setState(State::Connecting); | |
| 235 | 74 | m_setup->setupTimer()->start(TIMEOUT_MS); | |
| 236 | #ifdef MAILJD_E2E_TESTING | ||
| 237 |
1/1✓ Branch 76 → 77 taken 62 times.
|
62 | reportE2eTransportPhase(QStringLiteral("tcp-connect")); |
| 238 | #endif | ||
| 239 | |||
| 240 |
3/4✓ Branch 74 → 75 taken 12 times.
✗ Branch 74 → 91 not taken.
✓ Branch 80 → 81 taken 58 times.
✓ Branch 80 → 97 taken 4 times.
|
74 | if (config.security == "ssl") { |
| 241 |
12/12✓ Branch 75 → 76 taken 12 times.
✓ Branch 79 → 80 taken 12 times.
✓ Branch 80 → 81 taken 12 times.
✓ Branch 81 → 82 taken 70 times.
✓ Branch 82 → 83 taken 12 times.
✓ Branch 85 → 86 taken 58 times.
✓ Branch 86 → 87 taken 58 times.
✓ Branch 87 → 77 taken 12 times.
✓ Branch 87 → 88 taken 70 times.
✓ Branch 88 → 89 taken 58 times.
✓ Branch 93 → 83 taken 58 times.
✓ Branch 93 → 94 taken 58 times.
|
140 | qCInfo(lcImap) << "Connecting via SSL to" << config.host << ":" |
| 242 |
2/2✓ Branch 83 → 84 taken 12 times.
✓ Branch 89 → 90 taken 58 times.
|
70 | << config.port; |
| 243 |
2/2✓ Branch 89 → 90 taken 12 times.
✓ Branch 95 → 96 taken 58 times.
|
70 | m_socket->connectToHostEncrypted(config.host, config.port); |
| 244 | } else { | ||
| 245 |
7/12✗ Branch 91 → 92 not taken.
✗ Branch 95 → 96 not taken.
✗ Branch 96 → 97 not taken.
✓ Branch 97 → 98 taken 4 times.
✗ Branch 98 → 99 not taken.
✓ Branch 101 → 102 taken 4 times.
✓ Branch 102 → 103 taken 4 times.
✗ Branch 103 → 93 not taken.
✓ Branch 103 → 104 taken 4 times.
✓ Branch 104 → 105 taken 4 times.
✓ Branch 109 → 99 taken 4 times.
✓ Branch 109 → 110 taken 4 times.
|
8 | qCInfo(lcImap) << "Connecting via STARTTLS to" << config.host << ":" |
| 246 |
1/2✗ Branch 99 → 100 not taken.
✓ Branch 105 → 106 taken 4 times.
|
4 | << config.port; |
| 247 |
1/2✗ Branch 105 → 106 not taken.
✓ Branch 111 → 112 taken 4 times.
|
4 | m_socket->connectToHost(config.host, config.port); |
| 248 | } | ||
| 249 | } | ||
| 250 | |||
| 251 | 2342 | void ImapService::disconnect(DisconnectReason reason) { | |
| 252 | 2342 | emit disconnectReasonChanged(reason); | |
| 253 |
2/2✓ Branch 4 → 5 taken 1424 times.
✓ Branch 4 → 6 taken 880 times.
|
2304 | const bool deliberate = reason == DisconnectReason::UserRequested || |
| 254 |
4/4✓ Branch 3 → 4 taken 2304 times.
✓ Branch 3 → 6 taken 38 times.
✓ Branch 5 → 6 taken 1422 times.
✓ Branch 5 → 7 taken 2 times.
|
4646 | reason == DisconnectReason::AccountReload || |
| 255 | reason == DisconnectReason::ApplicationShutdown; | ||
| 256 |
2/2✓ Branch 8 → 9 taken 2340 times.
✓ Branch 8 → 10 taken 2 times.
|
2342 | if (deliberate) |
| 257 | 2340 | emit intentionalDisconnect(); | |
| 258 |
1/1✓ Branch 15 → 16 taken 2342 times.
|
4684 | m_body->completeBodyRequest(BodyOutcome::Cancelled, {}, |
| 259 | 4684 | QStringLiteral("Connection closed")); | |
| 260 |
1/1✓ Branch 23 → 24 taken 2342 times.
|
4684 | m_mutations->completeOutstandingMutations(QStringLiteral("Connection closed")); |
| 261 | 2342 | m_setup->setupTimer()->stop(); | |
| 262 | 2342 | m_commands->commandTimeoutTimer()->stop(); | |
| 263 | 2342 | m_commands->commandDeadlineTimer()->stop(); | |
| 264 | // T-720: Stop the probe/IDLE-renew watchdogs on explicit teardown so | ||
| 265 | // they cannot fire against a torn-down socket. | ||
| 266 | 2342 | m_push->livenessProbeWatchdog()->stop(); | |
| 267 | 2342 | m_push->idleRenewWatchdog()->stop(); | |
| 268 | 2342 | m_push->probeTag().clear(); | |
| 269 | 2342 | const bool appendInFlight = m_append->literalPending(); | |
| 270 |
2/2✓ Branch 47 → 48 taken 78 times.
✓ Branch 47 → 70 taken 2264 times.
|
2342 | if (m_socket->state() != QAbstractSocket::UnconnectedState) { |
| 271 | // A LOGOUT inside a pending literal would become message data. Close the | ||
| 272 | // socket immediately and let journal reconciliation own the outcome. | ||
| 273 |
1/2✗ Branch 48 → 49 not taken.
✓ Branch 48 → 50 taken 78 times.
|
78 | if (appendInFlight) { |
| 274 | ✗ | m_socket->abort(); | |
| 275 | } else { | ||
| 276 | // Try to send LOGOUT gracefully. | ||
| 277 |
1/2✓ Branch 50 → 51 taken 78 times.
✗ Branch 50 → 63 not taken.
|
78 | if (reason != DisconnectReason::RemoteBye && |
| 278 |
4/4✓ Branch 51 → 52 taken 61 times.
✓ Branch 51 → 53 taken 17 times.
✓ Branch 52 → 53 taken 40 times.
✓ Branch 52 → 63 taken 21 times.
|
78 | (m_state == State::Authenticated || m_state == State::Selected)) { |
| 279 |
1/1✓ Branch 54 → 55 taken 57 times.
|
57 | auto tag = m_commands->nextTag(); |
| 280 |
3/3✓ Branch 55 → 56 taken 57 times.
✓ Branch 56 → 57 taken 57 times.
✓ Branch 57 → 58 taken 57 times.
|
57 | m_socket->write((tag + " LOGOUT\r\n").toUtf8()); |
| 281 |
1/1✓ Branch 60 → 61 taken 57 times.
|
57 | m_socket->flush(); |
| 282 | 57 | } | |
| 283 |
3/4✓ Branch 63 → 64 taken 78 times.
✗ Branch 63 → 68 not taken.
✓ Branch 64 → 65 taken 61 times.
✓ Branch 64 → 68 taken 17 times.
|
78 | if (deliberate && m_state != State::Authenticated && |
| 284 |
3/4✓ Branch 65 → 66 taken 21 times.
✓ Branch 65 → 68 taken 40 times.
✓ Branch 66 → 67 taken 21 times.
✗ Branch 66 → 68 not taken.
|
61 | m_state != State::Selected && m_state != State::Idling) |
| 285 | 21 | m_socket->abort(); // Cancel lookup/connect/TLS instead of finishing setup. | |
| 286 | else | ||
| 287 | 57 | m_socket->disconnectFromHost(); | |
| 288 | } | ||
| 289 | } | ||
| 290 | // T-405/Bug 16: Reset all state variables to prevent corruption on reconnect | ||
| 291 | 2342 | m_push->isIdling() = false; | |
| 292 | // T-79.A1/H1: drop any in-flight APPEND payload with the session | ||
| 293 | 2342 | m_append->resetForSession(); | |
| 294 | 2342 | m_reader->resetFraming(); | |
| 295 | 2342 | m_commands->clearCommands(); | |
| 296 | 2342 | m_body->cancelParkedRequests(); // S106-001: the dropped queue held them | |
| 297 | 2342 | m_queuedStatusFolders.clear(); | |
| 298 | 2342 | m_push->idleStartPending() = false; | |
| 299 | 2342 | m_pendingFolders.clear(); | |
| 300 | 2342 | m_pendingHeaders.clear(); | |
| 301 | 2342 | m_pendingFlags.clear(); | |
| 302 | 2342 | m_pendingFlagSnapshots.clear(); | |
| 303 | 2342 | m_pendingSearchUids.clear(); | |
| 304 | 2342 | m_capabilities.clear(); | |
| 305 | 2342 | m_setup->pendingCapabilities().clear(); | |
| 306 | 2342 | m_setup->setupPhase() = SetupPhase::AwaitingGreeting; | |
| 307 | 2342 | m_body->resetActiveRequest(); | |
| 308 | 2342 | m_setup->clearCredentials(); | |
| 309 | 2342 | setState(State::Disconnected); | |
| 310 | 2342 | } | |
| 311 | |||
| 312 | 23 | void ImapService::listFolders() { | |
| 313 |
4/4✓ Branch 2 → 3 taken 5 times.
✓ Branch 2 → 15 taken 18 times.
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 15 taken 2 times.
|
23 | if (m_state != State::Authenticated && m_state != State::Selected) { |
| 314 |
5/5✓ Branch 4 → 5 taken 3 times.
✓ Branch 8 → 9 taken 3 times.
✓ Branch 9 → 10 taken 3 times.
✓ Branch 13 → 6 taken 3 times.
✓ Branch 13 → 14 taken 3 times.
|
6 | qCWarning(lcImap) << "Cannot list folders: not authenticated"; |
| 315 | 3 | return; | |
| 316 | } | ||
| 317 |
2/2✓ Branch 17 → 18 taken 1 time.
✓ Branch 17 → 23 taken 19 times.
|
20 | if (m_commands->hasStatefulCommandInFlight()) { |
| 318 |
1/1✓ Branch 20 → 21 taken 1 time.
|
2 | m_commands->enqueueSerializedCommand([this]() { listFolders(); }); |
| 319 | 1 | return; | |
| 320 | } | ||
| 321 | |||
| 322 | 19 | m_pendingFolders.clear(); | |
| 323 |
3/3✓ Branch 25 → 26 taken 19 times.
✓ Branch 26 → 27 taken 19 times.
✓ Branch 27 → 28 taken 19 times.
|
19 | m_commands->sendCommand("LIST", R"(LIST "" "*")"); |
| 324 | } | ||
| 325 | |||
| 326 | 78 | void ImapService::selectFolder(const QString &folderPath) { | |
| 327 |
4/4✓ Branch 2 → 3 taken 27 times.
✓ Branch 2 → 15 taken 51 times.
✓ Branch 3 → 4 taken 9 times.
✓ Branch 3 → 15 taken 18 times.
|
78 | if (m_state != State::Authenticated && m_state != State::Selected) { |
| 328 |
5/5✓ Branch 4 → 5 taken 9 times.
✓ Branch 8 → 9 taken 9 times.
✓ Branch 9 → 10 taken 9 times.
✓ Branch 13 → 6 taken 9 times.
✓ Branch 13 → 14 taken 9 times.
|
18 | qCWarning(lcImap) << "Cannot select folder: not authenticated"; |
| 329 | 27 | return; | |
| 330 | } | ||
| 331 |
2/3✓ Branch 16 → 17 taken 69 times.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 20 taken 69 times.
|
69 | if (!m_push->probeBeforeUse("SELECT")) |
| 332 | ✗ | return selectFolder(folderPath); | |
| 333 |
3/3✓ Branch 21 → 22 taken 69 times.
✓ Branch 22 → 23 taken 18 times.
✓ Branch 22 → 30 taken 51 times.
|
69 | if (m_commands->hasStatefulCommandInFlight()) { |
| 334 |
2/2✓ Branch 25 → 26 taken 18 times.
✓ Branch 26 → 27 taken 18 times.
|
20 | m_commands->enqueueSerializedCommand([this, folderPath]() { selectFolder(folderPath); }); |
| 335 | 18 | return; | |
| 336 | } | ||
| 337 | |||
| 338 | 51 | ImapPendingCommand select; // Bug 34: selected only on SELECT OK | |
| 339 | 51 | select.selectFolder = folderPath; | |
| 340 | 51 | m_selectedMessageCount = 0; | |
| 341 | 51 | m_selectedUidValidity = 0; | |
| 342 | // T-79.A2/M11: reset like selectAndFetchFlags/-Body — otherwise a folder | ||
| 343 | // whose OK carries no HIGHESTMODSEQ reports the previous folder's value. | ||
| 344 | 51 | m_selectedHighestModseq = 0; | |
| 345 |
5/5✓ Branch 34 → 35 taken 51 times.
✓ Branch 36 → 37 taken 51 times.
✓ Branch 37 → 38 taken 51 times.
✓ Branch 38 → 39 taken 51 times.
✓ Branch 39 → 40 taken 51 times.
|
153 | m_commands->sendCommand("SELECT", QString("SELECT %1").arg(quoteMailboxName(folderPath)), |
| 346 | 51 | std::move(select)); | |
| 347 | 51 | } | |
| 348 | |||
| 349 | 7 | void ImapService::fetchHeaders(qint64 uidFrom) { | |
| 350 |
2/2✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 14 taken 3 times.
|
7 | if (m_state != State::Selected) { |
| 351 |
5/5✓ Branch 3 → 4 taken 4 times.
✓ Branch 7 → 8 taken 4 times.
✓ Branch 8 → 9 taken 4 times.
✓ Branch 12 → 5 taken 4 times.
✓ Branch 12 → 13 taken 4 times.
|
8 | qCWarning(lcImap) << "Cannot fetch: no folder selected"; |
| 352 | 4 | return; | |
| 353 | } | ||
| 354 |
2/2✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 22 taken 2 times.
|
3 | if (m_commands->hasStatefulCommandInFlight()) { |
| 355 |
1/1✓ Branch 19 → 20 taken 1 time.
|
2 | m_commands->enqueueSerializedCommand([this, uidFrom]() { fetchHeaders(uidFrom); }); |
| 356 | 1 | return; | |
| 357 | } | ||
| 358 | |||
| 359 | 2 | m_pendingHeaders.clear(); | |
| 360 |
2/2✓ Branch 29 → 30 taken 2 times.
✓ Branch 30 → 31 taken 2 times.
|
6 | m_commands->sendCommand( |
| 361 | "FETCH_HEADERS", | ||
| 362 |
2/2✓ Branch 27 → 28 taken 2 times.
✓ Branch 28 → 29 taken 2 times.
|
4 | QStringLiteral("UID FETCH %1:* (UID FLAGS RFC822.SIZE INTERNALDATE ENVELOPE %2)").arg(QString::number(uidFrom), kHeaderFieldsItem)); |
| 363 | } | ||
| 364 | |||
| 365 | // T-281: Create a new folder on the server (RFC 3501 §6.3.3) | ||
| 366 | 13 | void ImapService::createFolder(const QString &folderPath) { | |
| 367 |
4/4✓ Branch 2 → 3 taken 10 times.
✓ Branch 2 → 16 taken 3 times.
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 16 taken 6 times.
|
13 | if (m_state != State::Authenticated && m_state != State::Selected) { |
| 368 |
1/1✓ Branch 10 → 11 taken 4 times.
|
4 | emit folderOperationError(QStringLiteral("CREATE"), |
| 369 | 8 | QStringLiteral("Not authenticated")); | |
| 370 | 8 | return; | |
| 371 | } | ||
| 372 |
3/3✓ Branch 17 → 18 taken 9 times.
✓ Branch 18 → 19 taken 4 times.
✓ Branch 18 → 26 taken 5 times.
|
9 | if (m_commands->hasStatefulCommandInFlight()) { |
| 373 |
2/2✓ Branch 21 → 22 taken 4 times.
✓ Branch 22 → 23 taken 4 times.
|
7 | m_commands->enqueueSerializedCommand([this, folderPath]() { createFolder(folderPath); }); |
| 374 | 4 | return; | |
| 375 | } | ||
| 376 | 5 | ImapPendingCommand create; | |
| 377 | 5 | create.folderPath = folderPath; | |
| 378 |
5/5✓ Branch 30 → 31 taken 5 times.
✓ Branch 32 → 33 taken 5 times.
✓ Branch 33 → 34 taken 5 times.
✓ Branch 34 → 35 taken 5 times.
✓ Branch 35 → 36 taken 5 times.
|
15 | m_commands->sendCommand("CREATE", QString("CREATE %1").arg(quoteMailboxName(folderPath)), |
| 379 | 5 | std::move(create)); | |
| 380 | 5 | } | |
| 381 | |||
| 382 | // T-281: Delete a folder on the server (RFC 3501 §6.3.4) | ||
| 383 | 9 | void ImapService::deleteFolder(const QString &folderPath) { | |
| 384 |
4/4✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 16 taken 5 times.
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 16 taken 2 times.
|
9 | if (m_state != State::Authenticated && m_state != State::Selected) { |
| 385 |
1/1✓ Branch 10 → 11 taken 2 times.
|
2 | emit folderOperationError(QStringLiteral("DELETE"), |
| 386 | 4 | QStringLiteral("Not authenticated")); | |
| 387 | 6 | return; | |
| 388 | } | ||
| 389 |
3/3✓ Branch 17 → 18 taken 7 times.
✓ Branch 18 → 19 taken 4 times.
✓ Branch 18 → 26 taken 3 times.
|
7 | if (m_commands->hasStatefulCommandInFlight()) { |
| 390 |
2/2✓ Branch 21 → 22 taken 4 times.
✓ Branch 22 → 23 taken 4 times.
|
7 | m_commands->enqueueSerializedCommand([this, folderPath]() { deleteFolder(folderPath); }); |
| 391 | 4 | return; | |
| 392 | } | ||
| 393 | 3 | ImapPendingCommand remove; | |
| 394 | 3 | remove.folderPath = folderPath; | |
| 395 |
5/5✓ Branch 30 → 31 taken 3 times.
✓ Branch 32 → 33 taken 3 times.
✓ Branch 33 → 34 taken 3 times.
✓ Branch 34 → 35 taken 3 times.
✓ Branch 35 → 36 taken 3 times.
|
9 | m_commands->sendCommand("DELETE", QString("DELETE %1").arg(quoteMailboxName(folderPath)), |
| 396 | 3 | std::move(remove)); | |
| 397 | 3 | } | |
| 398 | |||
| 399 | // T-281: Rename (or move) a folder on the server (RFC 3501 §6.3.5) | ||
| 400 | 9 | void ImapService::renameFolder(const QString &oldPath, const QString &newPath) { | |
| 401 |
4/4✓ Branch 2 → 3 taken 7 times.
✓ Branch 2 → 16 taken 2 times.
✓ Branch 3 → 4 taken 5 times.
✓ Branch 3 → 16 taken 2 times.
|
9 | if (m_state != State::Authenticated && m_state != State::Selected) { |
| 402 |
1/1✓ Branch 10 → 11 taken 5 times.
|
5 | emit folderOperationError(QStringLiteral("RENAME"), |
| 403 | 10 | QStringLiteral("Not authenticated")); | |
| 404 | 7 | return; | |
| 405 | } | ||
| 406 |
3/3✓ Branch 17 → 18 taken 4 times.
✓ Branch 18 → 19 taken 2 times.
✓ Branch 18 → 27 taken 2 times.
|
4 | if (m_commands->hasStatefulCommandInFlight()) { |
| 407 |
2/4✓ Branch 22 → 23 taken 2 times.
✓ Branch 23 → 24 taken 2 times.
✗ Branch 64 → 65 not taken.
✗ Branch 64 → 66 not taken.
|
2 | m_commands->enqueueSerializedCommand([this, oldPath, newPath]() { |
| 408 | 2 | renameFolder(oldPath, newPath); | |
| 409 | 2 | }); | |
| 410 | 2 | return; | |
| 411 | } | ||
| 412 | 2 | ImapPendingCommand rename; | |
| 413 | 2 | rename.folderPath = oldPath; | |
| 414 | 2 | rename.folderNewPath = newPath; | |
| 415 |
3/3✓ Branch 32 → 33 taken 2 times.
✓ Branch 36 → 37 taken 2 times.
✓ Branch 37 → 38 taken 2 times.
|
6 | m_commands->sendCommand("RENAME", QString("RENAME %1 %2") |
| 416 |
3/3✓ Branch 33 → 34 taken 2 times.
✓ Branch 34 → 35 taken 2 times.
✓ Branch 35 → 36 taken 2 times.
|
6 | .arg(quoteMailboxName(oldPath), quoteMailboxName(newPath)), std::move(rename)); |
| 417 | 2 | } | |
| 418 | |||
| 419 | 548 | void ImapService::fetchHeadersByUids(const QList<qint64> &uids) { | |
| 420 |
2/2✓ Branch 2 → 3 taken 538 times.
✓ Branch 2 → 14 taken 10 times.
|
548 | if (m_state != State::Selected) { |
| 421 |
5/5✓ Branch 3 → 4 taken 538 times.
✓ Branch 7 → 8 taken 538 times.
✓ Branch 8 → 9 taken 538 times.
✓ Branch 12 → 5 taken 538 times.
✓ Branch 12 → 13 taken 538 times.
|
1076 | qCWarning(lcImap) << "Cannot fetch: no folder selected"; |
| 422 | 545 | return; | |
| 423 | } | ||
| 424 |
2/2✓ Branch 15 → 16 taken 1 time.
✓ Branch 15 → 17 taken 9 times.
|
10 | if (uids.isEmpty()) |
| 425 | 1 | return; | |
| 426 |
3/3✓ Branch 18 → 19 taken 9 times.
✓ Branch 19 → 20 taken 6 times.
✓ Branch 19 → 27 taken 3 times.
|
9 | if (m_commands->hasStatefulCommandInFlight()) { |
| 427 |
2/2✓ Branch 22 → 23 taken 6 times.
✓ Branch 23 → 24 taken 6 times.
|
7 | m_commands->enqueueSerializedCommand([this, uids]() { fetchHeadersByUids(uids); }); |
| 428 | 6 | return; | |
| 429 | } | ||
| 430 | |||
| 431 |
1/1✓ Branch 27 → 28 taken 3 times.
|
3 | m_pendingHeaders.clear(); |
| 432 | |||
| 433 | // Build comma-separated UID set, e.g. "100,99,98,97" | ||
| 434 | 3 | QStringList uidStrings; | |
| 435 |
1/1✓ Branch 29 → 30 taken 3 times.
|
3 | uidStrings.reserve(uids.size()); |
| 436 |
2/2✓ Branch 38 → 32 taken 5 times.
✓ Branch 38 → 39 taken 3 times.
|
8 | for (qint64 uid : uids) { |
| 437 |
2/2✓ Branch 33 → 34 taken 5 times.
✓ Branch 34 → 35 taken 5 times.
|
5 | uidStrings.append(QString::number(uid)); |
| 438 | } | ||
| 439 |
1/1✓ Branch 40 → 41 taken 3 times.
|
3 | QString uidSet = uidStrings.join(','); |
| 440 | |||
| 441 |
2/2✓ Branch 46 → 47 taken 3 times.
✓ Branch 47 → 48 taken 3 times.
|
6 | m_commands->sendCommand("FETCH_HEADERS", |
| 442 | 6 | QStringLiteral("UID FETCH %1 (UID FLAGS RFC822.SIZE INTERNALDATE ENVELOPE %2)") | |
| 443 |
1/1✓ Branch 45 → 46 taken 3 times.
|
9 | .arg(uidSet, kHeaderFieldsItem)); |
| 444 | 3 | } | |
| 445 | |||
| 446 | 125 | bool ImapService::hasIdleCapability() const { | |
| 447 |
1/1✓ Branch 2 → 3 taken 125 times.
|
125 | return m_capabilities.contains("IDLE", Qt::CaseInsensitive); |
| 448 | } | ||
| 449 | |||
| 450 | // T-208: Check for CONDSTORE capability (RFC 4551) | ||
| 451 | 540 | bool ImapService::hasCondstoreCapability() const { | |
| 452 |
1/1✓ Branch 2 → 3 taken 540 times.
|
540 | return m_capabilities.contains("CONDSTORE", Qt::CaseInsensitive); |
| 453 | } | ||
| 454 | |||
| 455 | 5 | bool ImapService::hasEsearchCapability() const { | |
| 456 |
1/1✓ Branch 2 → 3 taken 5 times.
|
5 | return m_capabilities.contains("ESEARCH", Qt::CaseInsensitive); |
| 457 | } | ||
| 458 | |||
| 459 | // ═══════════════════════════════════════════════════════ | ||
| 460 | // T-320: IMAP NOTIFY (RFC 5465) | ||
| 461 | // ═══════════════════════════════════════════════════════ | ||
| 462 | |||
| 463 | 9 | bool ImapService::hasNotifyCapability() const { | |
| 464 |
1/1✓ Branch 2 → 3 taken 9 times.
|
9 | return m_capabilities.contains("NOTIFY", Qt::CaseInsensitive); |
| 465 | } | ||
| 466 | |||
| 467 | 1231 | void ImapService::executeAfterIdle(std::function<void()> command) { | |
| 468 |
1/1✓ Branch 5 → 6 taken 1231 times.
|
1231 | m_commands->executeAfterIdle(std::move(command)); |
| 469 | 1231 | } | |
| 470 | |||
| 471 | 28 | void ImapService::clearDeferredCommands() { m_commands->clearDeferredCommands(); } | |
| 472 | |||
| 473 | 14 | void ImapService::fetchFlags() { | |
| 474 |
2/2✓ Branch 2 → 3 taken 8 times.
✓ Branch 2 → 14 taken 6 times.
|
14 | if (m_state != State::Selected) { |
| 475 |
5/5✓ Branch 3 → 4 taken 8 times.
✓ Branch 7 → 8 taken 8 times.
✓ Branch 8 → 9 taken 8 times.
✓ Branch 12 → 5 taken 8 times.
✓ Branch 12 → 13 taken 8 times.
|
16 | qCWarning(lcImap) << "Cannot fetch flags: no folder selected"; |
| 476 | 8 | return; | |
| 477 | } | ||
| 478 |
2/2✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 22 taken 5 times.
|
6 | if (m_commands->hasStatefulCommandInFlight()) { |
| 479 |
1/1✓ Branch 19 → 20 taken 1 time.
|
2 | m_commands->enqueueSerializedCommand([this]() { fetchFlags(); }); |
| 480 | 1 | return; | |
| 481 | } | ||
| 482 | |||
| 483 | 5 | m_pendingFlags.clear(); | |
| 484 | 5 | m_pendingFlagSnapshots.clear(); | |
| 485 |
3/3✓ Branch 25 → 26 taken 5 times.
✓ Branch 26 → 27 taken 5 times.
✓ Branch 27 → 28 taken 5 times.
|
5 | m_commands->sendCommand("FETCH_FLAGS", "UID FETCH 1:* (UID FLAGS)"); |
| 486 | } | ||
| 487 | |||
| 488 | // F-38 (SPRINT-104): verification readback scoped to the changed message. | ||
| 489 | // A whole-folder UID FETCH 1:* costs one full flag snapshot (measured | ||
| 490 | // 838 kB / 84 ms on a 20,000-message Dovecot folder) per flag change; the | ||
| 491 | // mutation pipeline only needs the STOREd UID's flags back. The partial | ||
| 492 | // answer arrives on its own signal so FolderFlagSync never mistakes it | ||
| 493 | // for a complete snapshot. | ||
| 494 | 6 | void ImapService::fetchVerificationFlags(qint64 uid) { | |
| 495 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 14 taken 6 times.
|
6 | if (m_state != State::Selected) { |
| 496 | ✗ | qCWarning(lcImap) << "Cannot fetch verification flags: no folder selected"; | |
| 497 | ✗ | return; | |
| 498 | } | ||
| 499 |
1/2✗ Branch 16 → 17 not taken.
✓ Branch 16 → 22 taken 6 times.
|
6 | if (m_commands->hasStatefulCommandInFlight()) { |
| 500 | ✗ | m_commands->enqueueSerializedCommand( | |
| 501 | ✗ | [this, uid]() { fetchVerificationFlags(uid); }); | |
| 502 | ✗ | return; | |
| 503 | } | ||
| 504 | |||
| 505 | 6 | m_pendingFlags.clear(); | |
| 506 | 6 | m_pendingFlagSnapshots.clear(); | |
| 507 | 6 | m_verificationUid = uid; | |
| 508 |
2/2✓ Branch 28 → 29 taken 6 times.
✓ Branch 29 → 30 taken 6 times.
|
18 | m_commands->sendCommand("FETCH_VERIFY_FLAGS", |
| 509 |
2/2✓ Branch 25 → 26 taken 6 times.
✓ Branch 27 → 28 taken 6 times.
|
18 | QString("UID FETCH %1 (UID FLAGS)").arg(uid)); |
| 510 | } | ||
| 511 | |||
| 512 | 5 | void ImapService::handleTaggedVerifyFlags(const TaggedResponse &response) { | |
| 513 |
1/2✓ Branch 3 → 4 taken 5 times.
✗ Branch 3 → 18 not taken.
|
5 | if (response.status == "OK") { |
| 514 |
6/6✓ Branch 4 → 5 taken 5 times.
✓ Branch 8 → 9 taken 5 times.
✓ Branch 9 → 10 taken 5 times.
✓ Branch 11 → 12 taken 5 times.
✓ Branch 16 → 6 taken 5 times.
✓ Branch 16 → 17 taken 5 times.
|
10 | qCInfo(lcImap) << "Verification FETCH complete:" << m_pendingFlagSnapshots.size() |
| 515 |
1/1✓ Branch 12 → 13 taken 5 times.
|
5 | << "snapshot(s)"; |
| 516 | 5 | emit verifyFlagsReceived(m_pendingFlagSnapshots, m_selectedFolder, m_verificationUid); | |
| 517 | } else { | ||
| 518 | ✗ | qCWarning(lcImap) << "Verification FETCH failed:" << response.message; | |
| 519 | ✗ | emit verifyFlagsReceived({}, m_selectedFolder, m_verificationUid); | |
| 520 | } | ||
| 521 | 5 | m_pendingFlags.clear(); | |
| 522 | 5 | m_pendingFlagSnapshots.clear(); | |
| 523 | 5 | } | |
| 524 | |||
| 525 | // T-207: Pipeline SELECT + FETCH FLAGS — saves one round-trip. | ||
| 526 | // Both commands are sent immediately; the server processes them in-order. | ||
| 527 | // The FETCH FLAGS will execute in the context of the newly selected folder. | ||
| 528 | 536 | void ImapService::selectAndFetchFlags(const QString &folderPath) { | |
| 529 |
4/4✓ Branch 2 → 3 taken 535 times.
✓ Branch 2 → 15 taken 1 time.
✓ Branch 3 → 4 taken 532 times.
✓ Branch 3 → 15 taken 3 times.
|
536 | if (m_state != State::Authenticated && m_state != State::Selected) { |
| 530 |
5/5✓ Branch 4 → 5 taken 532 times.
✓ Branch 8 → 9 taken 532 times.
✓ Branch 9 → 10 taken 532 times.
✓ Branch 13 → 6 taken 532 times.
✓ Branch 13 → 14 taken 532 times.
|
1064 | qCWarning(lcImap) << "Cannot selectAndFetchFlags: not authenticated"; |
| 531 | 534 | return; | |
| 532 | } | ||
| 533 |
2/3✓ Branch 16 → 17 taken 4 times.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 20 taken 4 times.
|
4 | if (!m_push->probeBeforeUse("SELECT")) |
| 534 | ✗ | return selectAndFetchFlags(folderPath); | |
| 535 |
3/3✓ Branch 21 → 22 taken 4 times.
✓ Branch 22 → 23 taken 2 times.
✓ Branch 22 → 30 taken 2 times.
|
4 | if (m_commands->hasStatefulCommandInFlight()) { |
| 536 |
2/2✓ Branch 25 → 26 taken 2 times.
✓ Branch 26 → 27 taken 2 times.
|
4 | m_commands->enqueueSerializedCommand( |
| 537 | 5 | [this, folderPath]() { selectAndFetchFlags(folderPath); }); | |
| 538 | 2 | return; | |
| 539 | } | ||
| 540 | |||
| 541 | 2 | ImapPendingCommand select; // Bug 34: selected only on SELECT OK | |
| 542 | 2 | select.selectFolder = folderPath; | |
| 543 | 2 | m_selectedMessageCount = 0; | |
| 544 | 2 | m_selectedUidValidity = 0; | |
| 545 | 2 | m_selectedHighestModseq = 0; // T-208: reset, will be set by untagged OK | |
| 546 |
1/1✓ Branch 31 → 32 taken 2 times.
|
2 | m_pendingFlags.clear(); |
| 547 |
1/1✓ Branch 32 → 33 taken 2 times.
|
2 | m_pendingFlagSnapshots.clear(); |
| 548 | |||
| 549 | // Send both commands back-to-back (pipelining). SELECT (CONDSTORE) makes | ||
| 550 | // the server report HIGHESTMODSEQ, the base of the next reopen. | ||
| 551 |
5/5✓ Branch 36 → 37 taken 2 times.
✓ Branch 43 → 44 taken 2 times.
✓ Branch 44 → 45 taken 2 times.
✓ Branch 45 → 46 taken 2 times.
✓ Branch 46 → 47 taken 2 times.
|
8 | m_commands->sendCommand("SELECT", QString("SELECT %1%2").arg(quoteMailboxName(folderPath), |
| 552 |
5/7✓ Branch 37 → 38 taken 2 times.
✓ Branch 38 → 39 taken 1 time.
✓ Branch 38 → 42 taken 1 time.
✓ Branch 51 → 52 taken 1 time.
✓ Branch 51 → 53 taken 1 time.
✗ Branch 98 → 99 not taken.
✗ Branch 98 → 100 not taken.
|
5 | hasCondstoreCapability() ? QStringLiteral(" (CONDSTORE)") : QString()), |
| 553 | 2 | std::move(select)); | |
| 554 |
3/3✓ Branch 56 → 57 taken 2 times.
✓ Branch 57 → 58 taken 2 times.
✓ Branch 58 → 59 taken 2 times.
|
2 | m_commands->sendCommand("FETCH_FLAGS", "UID FETCH 1:* (UID FLAGS)"); |
| 555 | |||
| 556 |
6/6✓ Branch 62 → 63 taken 2 times.
✓ Branch 66 → 67 taken 2 times.
✓ Branch 67 → 68 taken 2 times.
✓ Branch 68 → 69 taken 2 times.
✓ Branch 72 → 64 taken 2 times.
✓ Branch 72 → 73 taken 2 times.
|
4 | qCInfo(lcImap) << "T-207: Pipelined SELECT + FETCH_FLAGS for" << folderPath; |
| 557 | 2 | } | |
| 558 | |||
| 559 | 60 | void ImapService::selectAndSearchAll(const QString &folderPath) { | |
| 560 |
4/4✓ Branch 2 → 3 taken 58 times.
✓ Branch 2 → 15 taken 2 times.
✓ Branch 3 → 4 taken 56 times.
✓ Branch 3 → 15 taken 2 times.
|
60 | if (m_state != State::Authenticated && m_state != State::Selected) { |
| 561 |
5/5✓ Branch 4 → 5 taken 56 times.
✓ Branch 8 → 9 taken 56 times.
✓ Branch 9 → 10 taken 56 times.
✓ Branch 13 → 6 taken 56 times.
✓ Branch 13 → 14 taken 56 times.
|
112 | qCWarning(lcImap) << "Cannot selectAndSearchAll: not authenticated"; |
| 562 | 57 | return; | |
| 563 | } | ||
| 564 |
2/3✓ Branch 16 → 17 taken 4 times.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 20 taken 4 times.
|
4 | if (!m_push->probeBeforeUse("SELECT")) |
| 565 | ✗ | return selectAndSearchAll(folderPath); | |
| 566 |
3/3✓ Branch 21 → 22 taken 4 times.
✓ Branch 22 → 23 taken 1 time.
✓ Branch 22 → 30 taken 3 times.
|
4 | if (m_commands->hasStatefulCommandInFlight()) { |
| 567 |
2/2✓ Branch 25 → 26 taken 1 time.
✓ Branch 26 → 27 taken 1 time.
|
2 | m_commands->enqueueSerializedCommand( |
| 568 | 2 | [this, folderPath]() { selectAndSearchAll(folderPath); }); | |
| 569 | 1 | return; | |
| 570 | } | ||
| 571 | |||
| 572 | 3 | ImapPendingCommand select; // Bug 34: selected only on SELECT OK | |
| 573 | 3 | select.selectFolder = folderPath; | |
| 574 | 3 | m_selectedMessageCount = 0; | |
| 575 | 3 | m_selectedUidValidity = 0; | |
| 576 | 3 | m_selectedHighestModseq = 0; | |
| 577 |
1/1✓ Branch 31 → 32 taken 3 times.
|
3 | m_pendingSearchUids.clear(); |
| 578 | |||
| 579 |
5/5✓ Branch 35 → 36 taken 3 times.
✓ Branch 42 → 43 taken 3 times.
✓ Branch 43 → 44 taken 3 times.
✓ Branch 44 → 45 taken 3 times.
✓ Branch 45 → 46 taken 3 times.
|
12 | m_commands->sendCommand("SELECT", QString("SELECT %1%2").arg(quoteMailboxName(folderPath), |
| 580 |
5/7✓ Branch 36 → 37 taken 3 times.
✓ Branch 37 → 38 taken 1 time.
✓ Branch 37 → 41 taken 2 times.
✓ Branch 50 → 51 taken 1 time.
✓ Branch 50 → 52 taken 2 times.
✗ Branch 99 → 100 not taken.
✗ Branch 99 → 101 not taken.
|
7 | hasCondstoreCapability() ? QStringLiteral(" (CONDSTORE)") : QString()), |
| 581 | 3 | std::move(select)); | |
| 582 | 3 | m_activeMessageIdSearch = {}; | |
| 583 |
3/3✓ Branch 57 → 58 taken 3 times.
✓ Branch 58 → 59 taken 3 times.
✓ Branch 59 → 60 taken 3 times.
|
3 | m_commands->sendCommand("SEARCH", "UID SEARCH ALL"); |
| 584 | |||
| 585 |
6/6✓ Branch 63 → 64 taken 3 times.
✓ Branch 67 → 68 taken 3 times.
✓ Branch 68 → 69 taken 3 times.
✓ Branch 69 → 70 taken 3 times.
✓ Branch 73 → 65 taken 3 times.
✓ Branch 73 → 74 taken 3 times.
|
6 | qCInfo(lcImap) << "Pipelined SELECT + UID SEARCH ALL for" << folderPath; |
| 586 | 3 | } | |
| 587 | |||
| 588 | 4 | void ImapService::selectAndFetchChangedFlags(const QString &folderPath, quint64 modseq) { | |
| 589 |
3/4✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 15 taken 3 times.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 15 taken 1 time.
|
4 | if (m_state != State::Authenticated && m_state != State::Selected) { |
| 590 | ✗ | qCWarning(lcImap) << "Cannot selectAndFetchChangedFlags: not authenticated"; | |
| 591 | ✗ | return; | |
| 592 | } | ||
| 593 |
2/3✓ Branch 16 → 17 taken 4 times.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 20 taken 4 times.
|
4 | if (!m_push->probeBeforeUse("SELECT")) |
| 594 | ✗ | return selectAndFetchChangedFlags(folderPath, modseq); | |
| 595 |
2/3✓ Branch 21 → 22 taken 4 times.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 30 taken 4 times.
|
4 | if (m_commands->hasStatefulCommandInFlight()) { |
| 596 | ✗ | m_commands->enqueueSerializedCommand([this, folderPath, modseq]() { | |
| 597 | ✗ | selectAndFetchChangedFlags(folderPath, modseq); | |
| 598 | ✗ | }); | |
| 599 | ✗ | return; | |
| 600 | } | ||
| 601 | 4 | ImapPendingCommand select; // Bug 34: selected only on SELECT OK | |
| 602 | 4 | select.selectFolder = folderPath; | |
| 603 | 4 | m_selectedMessageCount = 0; | |
| 604 | 4 | m_selectedUidValidity = 0; | |
| 605 | 4 | m_selectedHighestModseq = 0; | |
| 606 |
1/1✓ Branch 31 → 32 taken 4 times.
|
4 | m_pendingFlags.clear(); |
| 607 |
1/1✓ Branch 32 → 33 taken 4 times.
|
4 | m_pendingFlagSnapshots.clear(); |
| 608 |
1/1✓ Branch 33 → 34 taken 4 times.
|
4 | m_pendingSearchUids.clear(); |
| 609 |
5/5✓ Branch 37 → 38 taken 4 times.
✓ Branch 39 → 40 taken 4 times.
✓ Branch 40 → 41 taken 4 times.
✓ Branch 41 → 42 taken 4 times.
✓ Branch 42 → 43 taken 4 times.
|
12 | m_commands->sendCommand("SELECT", QString("SELECT %1 (CONDSTORE)").arg(quoteMailboxName(folderPath)), |
| 610 | 4 | std::move(select)); | |
| 611 |
2/2✓ Branch 52 → 53 taken 4 times.
✓ Branch 53 → 54 taken 4 times.
|
8 | m_commands->sendCommand("FETCH_FLAGS_CHANGED", |
| 612 |
2/2✓ Branch 49 → 50 taken 4 times.
✓ Branch 51 → 52 taken 4 times.
|
12 | QString("UID FETCH 1:* (UID FLAGS) (CHANGEDSINCE %1)").arg(modseq)); |
| 613 |
3/3✓ Branch 59 → 60 taken 4 times.
✓ Branch 67 → 68 taken 4 times.
✓ Branch 68 → 69 taken 4 times.
|
8 | m_commands->sendCommand("SEARCH_MEMBERSHIP", hasEsearchCapability() |
| 614 |
6/10✓ Branch 60 → 61 taken 3 times.
✓ Branch 60 → 64 taken 1 time.
✓ Branch 71 → 72 taken 1 time.
✓ Branch 71 → 73 taken 3 times.
✓ Branch 73 → 74 taken 3 times.
✓ Branch 73 → 75 taken 1 time.
✗ Branch 141 → 142 not taken.
✗ Branch 141 → 143 not taken.
✗ Branch 145 → 146 not taken.
✗ Branch 145 → 147 not taken.
|
12 | ? QStringLiteral("UID SEARCH RETURN (ALL) ALL") : QStringLiteral("UID SEARCH ALL")); |
| 615 |
6/6✓ Branch 76 → 77 taken 4 times.
✓ Branch 80 → 81 taken 4 times.
✓ Branch 81 → 82 taken 4 times.
✓ Branch 82 → 83 taken 4 times.
✓ Branch 88 → 78 taken 4 times.
✓ Branch 88 → 89 taken 4 times.
|
8 | qCInfo(lcImap) << "Pipelined SELECT (CONDSTORE) + CHANGEDSINCE" << modseq |
| 616 |
2/2✓ Branch 83 → 84 taken 4 times.
✓ Branch 84 → 85 taken 4 times.
|
4 | << "+ UID membership for" << folderPath; |
| 617 | 4 | } | |
| 618 | |||
| 619 | // T-208: Incremental flag sync using CONDSTORE (RFC 4551) | ||
| 620 | 6 | void ImapService::fetchFlagsChanged(quint64 modseq) { | |
| 621 |
2/2✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 14 taken 5 times.
|
6 | if (m_state != State::Selected) { |
| 622 |
5/5✓ Branch 3 → 4 taken 1 time.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 12 → 5 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
|
2 | qCWarning(lcImap) << "Cannot fetchFlagsChanged: no folder selected"; |
| 623 | 1 | return; | |
| 624 | } | ||
| 625 |
2/2✓ Branch 16 → 17 taken 2 times.
✓ Branch 16 → 22 taken 3 times.
|
5 | if (m_commands->hasStatefulCommandInFlight()) { |
| 626 |
1/1✓ Branch 19 → 20 taken 2 times.
|
3 | m_commands->enqueueSerializedCommand([this, modseq]() { fetchFlagsChanged(modseq); }); |
| 627 | 2 | return; | |
| 628 | } | ||
| 629 | |||
| 630 | 3 | m_pendingFlags.clear(); | |
| 631 | 3 | m_pendingFlagSnapshots.clear(); | |
| 632 |
2/2✓ Branch 28 → 29 taken 3 times.
✓ Branch 29 → 30 taken 3 times.
|
6 | m_commands->sendCommand("FETCH_FLAGS", |
| 633 |
2/2✓ Branch 25 → 26 taken 3 times.
✓ Branch 27 → 28 taken 3 times.
|
9 | QString("UID FETCH 1:* (UID FLAGS) (CHANGEDSINCE %1)").arg(modseq)); |
| 634 |
6/6✓ Branch 34 → 35 taken 3 times.
✓ Branch 38 → 39 taken 3 times.
✓ Branch 39 → 40 taken 3 times.
✓ Branch 40 → 41 taken 3 times.
✓ Branch 44 → 36 taken 3 times.
✓ Branch 44 → 45 taken 3 times.
|
6 | qCInfo(lcImap) << "T-208: CONDSTORE flag sync, CHANGEDSINCE" << modseq; |
| 635 | } | ||
| 636 | |||
| 637 | 5 | void ImapService::fetchUidForSeqNo(int seqNo) { | |
| 638 |
2/2✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 14 taken 3 times.
|
5 | if (m_state != State::Selected) { |
| 639 |
5/5✓ Branch 3 → 4 taken 2 times.
✓ Branch 7 → 8 taken 2 times.
✓ Branch 8 → 9 taken 2 times.
✓ Branch 12 → 5 taken 2 times.
✓ Branch 12 → 13 taken 2 times.
|
4 | qCWarning(lcImap) << "Cannot fetch UID for seqNo: no folder selected"; |
| 640 | 2 | return; | |
| 641 | } | ||
| 642 |
2/2✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 22 taken 2 times.
|
3 | if (m_commands->hasStatefulCommandInFlight()) { |
| 643 |
1/1✓ Branch 19 → 20 taken 1 time.
|
2 | m_commands->enqueueSerializedCommand([this, seqNo]() { fetchUidForSeqNo(seqNo); }); |
| 644 | 1 | return; | |
| 645 | } | ||
| 646 | // T-065 fix: Use DISTINCT command type so the single-UID result | ||
| 647 | // is NOT treated as a full flag sync (which would purge all other mails). | ||
| 648 | 2 | m_pendingFlags.clear(); | |
| 649 | 2 | m_pendingFlagSnapshots.clear(); | |
| 650 |
2/2✓ Branch 28 → 29 taken 2 times.
✓ Branch 29 → 30 taken 2 times.
|
6 | m_commands->sendCommand("FETCH_SEQNO", |
| 651 |
2/2✓ Branch 25 → 26 taken 2 times.
✓ Branch 27 → 28 taken 2 times.
|
6 | QString("FETCH %1 (UID FLAGS)").arg(seqNo)); |
| 652 | } | ||
| 653 | |||
| 654 | 130 | void ImapService::statusFolder(const QString &folderPath) { | |
| 655 |
4/4✓ Branch 2 → 3 taken 119 times.
✓ Branch 2 → 15 taken 11 times.
✓ Branch 3 → 4 taken 5 times.
✓ Branch 3 → 15 taken 114 times.
|
130 | if (m_state != State::Authenticated && m_state != State::Selected) { |
| 656 |
5/5✓ Branch 4 → 5 taken 5 times.
✓ Branch 8 → 9 taken 5 times.
✓ Branch 9 → 10 taken 5 times.
✓ Branch 13 → 6 taken 5 times.
✓ Branch 13 → 14 taken 5 times.
|
10 | qCWarning(lcImap) << "Cannot status: not authenticated or selected"; |
| 657 | 111 | return; | |
| 658 | } | ||
| 659 |
3/3✓ Branch 16 → 17 taken 125 times.
✓ Branch 17 → 18 taken 106 times.
✓ Branch 17 → 29 taken 19 times.
|
125 | if (m_commands->hasStatefulCommandInFlight()) { |
| 660 |
2/2✓ Branch 19 → 20 taken 100 times.
✓ Branch 19 → 21 taken 6 times.
|
106 | if (m_queuedStatusFolders.contains(folderPath)) return; |
| 661 |
1/1✓ Branch 21 → 22 taken 6 times.
|
6 | m_queuedStatusFolders.insert(folderPath); |
| 662 |
2/2✓ Branch 24 → 25 taken 6 times.
✓ Branch 25 → 26 taken 6 times.
|
6 | m_commands->enqueueSerializedCommand([this, folderPath]() { |
| 663 | 2 | m_queuedStatusFolders.remove(folderPath); | |
| 664 | 2 | statusFolder(folderPath); | |
| 665 | 2 | }); | |
| 666 | 6 | return; | |
| 667 | } | ||
| 668 | |||
| 669 | 19 | ImapPendingCommand status; | |
| 670 | 19 | status.statusFolder = folderPath; | |
| 671 | 19 | status.statusStartedNs = std::chrono::duration_cast<std::chrono::nanoseconds>( | |
| 672 | 38 | std::chrono::steady_clock::now().time_since_epoch()).count(); | |
| 673 |
3/3✓ Branch 37 → 38 taken 19 times.
✓ Branch 41 → 42 taken 19 times.
✓ Branch 42 → 43 taken 19 times.
|
57 | m_commands->sendCommand("STATUS", QString("STATUS %1 (MESSAGES UNSEEN RECENT)") |
| 674 |
2/2✓ Branch 39 → 40 taken 19 times.
✓ Branch 40 → 41 taken 19 times.
|
57 | .arg(quoteMailboxName(folderPath)), std::move(status)); |
| 675 | 19 | } | |
| 676 | |||
| 677 | // ═══════════════════════════════════════════════════════ | ||
| 678 | // T-720: Liveness probe + reconnect API (Sprint 72) | ||
| 679 | // ═══════════════════════════════════════════════════════ | ||
| 680 | |||
| 681 | 20 | void ImapService::abortForReconnect(const QString &reason) { | |
| 682 |
6/6✓ Branch 2 → 3 taken 20 times.
✓ Branch 6 → 7 taken 20 times.
✓ Branch 7 → 8 taken 20 times.
✓ Branch 8 → 9 taken 20 times.
✓ Branch 12 → 4 taken 20 times.
✓ Branch 12 → 13 taken 20 times.
|
40 | qCWarning(lcImap) << "Forced reconnect requested:" << reason; |
| 683 | // failConnection() clears pending state, aborts the socket, emits | ||
| 684 | // errorOccurred and transitions to State::Error — the monitor's | ||
| 685 | // scheduleReconnect() arm fires from there. | ||
| 686 |
2/2✓ Branch 17 → 18 taken 20 times.
✓ Branch 18 → 19 taken 20 times.
|
40 | failConnection(QStringLiteral("Forced reconnect: %1").arg(reason), |
| 687 | DisconnectReason::ForcedHealthProbe); | ||
| 688 | 20 | } | |
| 689 | |||
| 690 | 66 | void ImapService::failConnection(const QString &error, | |
| 691 | DisconnectReason reason) { | ||
| 692 |
5/5✓ Branch 2 → 3 taken 66 times.
✓ Branch 6 → 7 taken 66 times.
✓ Branch 7 → 8 taken 66 times.
✓ Branch 11 → 4 taken 66 times.
✓ Branch 11 → 12 taken 66 times.
|
132 | qCWarning(lcImap) << error; |
| 693 | 66 | emit disconnectReasonChanged(reason); | |
| 694 |
1/1✓ Branch 15 → 16 taken 66 times.
|
66 | m_body->completeBodyRequest(BodyOutcome::Failed, {}, error); |
| 695 | 66 | m_mutations->completeOutstandingMutations(error); | |
| 696 | 66 | m_setup->clearCredentials(); | |
| 697 | 66 | m_setup->setupTimer()->stop(); | |
| 698 | 66 | m_commands->commandTimeoutTimer()->stop(); | |
| 699 | 66 | m_commands->commandDeadlineTimer()->stop(); | |
| 700 | 66 | m_push->idleRenewTimer()->stop(); | |
| 701 | // T-720: Stop the liveness probe + IDLE renew watchdogs on failure. | ||
| 702 | 66 | m_push->livenessProbeWatchdog()->stop(); | |
| 703 | 66 | m_push->idleRenewWatchdog()->stop(); | |
| 704 | 66 | m_push->probeTag().clear(); | |
| 705 | 66 | m_commands->clearCommands(); | |
| 706 | 66 | m_body->cancelParkedRequests(); // S106-001: the dropped queue held them | |
| 707 | 66 | m_queuedStatusFolders.clear(); | |
| 708 | 66 | m_push->idleStartPending() = false; | |
| 709 | 66 | m_reader->resetFraming(); | |
| 710 | 66 | m_pendingFolders.clear(); | |
| 711 | 66 | m_pendingHeaders.clear(); | |
| 712 | 66 | m_pendingFlags.clear(); | |
| 713 | 66 | m_pendingFlagSnapshots.clear(); | |
| 714 | 66 | m_pendingSearchUids.clear(); | |
| 715 | 66 | m_push->isIdling() = false; | |
| 716 | 66 | m_push->idleTag().clear(); | |
| 717 | 66 | m_push->isNotifying() = false; | |
| 718 | 66 | m_push->notifyTag().clear(); | |
| 719 | // T-79.A1/H1: drop any in-flight APPEND payload with the session | ||
| 720 | 66 | m_append->resetForSession(); | |
| 721 | 66 | m_body->resetActiveRequest(); | |
| 722 | 66 | m_socket->abort(); | |
| 723 | 66 | emit errorOccurred(error); | |
| 724 | 66 | setState(State::Error); | |
| 725 | 66 | } | |
| 726 | |||
| 727 | 2849 | void ImapService::setState(State newState) { | |
| 728 |
2/2✓ Branch 2 → 3 taken 789 times.
✓ Branch 2 → 17 taken 2060 times.
|
2849 | if (m_state != newState) { |
| 729 |
5/5✓ Branch 3 → 4 taken 789 times.
✓ Branch 7 → 8 taken 789 times.
✓ Branch 8 → 9 taken 789 times.
✓ Branch 15 → 5 taken 789 times.
✓ Branch 15 → 16 taken 789 times.
|
1578 | qCInfo(lcImap) << "State:" |
| 730 |
1/1✓ Branch 9 → 10 taken 789 times.
|
1578 | << QMetaEnum::fromType<State>().valueToKey( |
| 731 |
2/2✓ Branch 10 → 11 taken 789 times.
✓ Branch 11 → 12 taken 789 times.
|
789 | static_cast<int>(newState)); |
| 732 | 789 | m_state = newState; | |
| 733 | 789 | emit stateChanged(newState); | |
| 734 | } | ||
| 735 | 2849 | } | |
| 736 | |||
| 737 | #ifdef MAILJD_E2E_TESTING | ||
| 738 | 798 | quint64 ImapService::e2eRequestTokenFor(const ImapPendingCommand &command) const { | |
| 739 | 798 | const QString &type = command.type; | |
| 740 |
2/2✓ Branch 8 → 9 taken 66 times.
✓ Branch 8 → 12 taken 732 times.
|
798 | if (type == QStringLiteral("FETCH_BODY")) |
| 741 | 66 | return m_body->activeBodyRequestId(); | |
| 742 |
2/2✓ Branch 18 → 19 taken 51 times.
✓ Branch 18 → 20 taken 681 times.
|
732 | if (type == QStringLiteral("APPEND")) |
| 743 | 51 | return command.appendRequestId; | |
| 744 |
10/12✓ Branch 29 → 30 taken 619 times.
✓ Branch 29 → 40 taken 28 times.
✓ Branch 34 → 35 taken 602 times.
✓ Branch 34 → 40 taken 17 times.
✓ Branch 52 → 53 taken 647 times.
✓ Branch 52 → 54 taken 34 times.
✓ Branch 54 → 55 taken 681 times.
✗ Branch 54 → 56 not taken.
✓ Branch 56 → 57 taken 681 times.
✗ Branch 56 → 58 not taken.
✓ Branch 58 → 59 taken 89 times.
✓ Branch 58 → 63 taken 592 times.
|
2628 | if (type == QStringLiteral("STORE") || type == QStringLiteral("MOVE") || |
| 745 |
14/14✓ Branch 24 → 25 taken 647 times.
✓ Branch 24 → 40 taken 34 times.
✓ Branch 39 → 40 taken 10 times.
✓ Branch 39 → 41 taken 592 times.
✓ Branch 42 → 43 taken 602 times.
✓ Branch 42 → 44 taken 79 times.
✓ Branch 44 → 45 taken 602 times.
✓ Branch 44 → 46 taken 79 times.
✓ Branch 46 → 47 taken 619 times.
✓ Branch 46 → 48 taken 62 times.
✓ Branch 48 → 49 taken 619 times.
✓ Branch 48 → 50 taken 62 times.
✓ Branch 50 → 51 taken 647 times.
✓ Branch 50 → 52 taken 34 times.
|
2628 | type == QStringLiteral("COPY") || type == QStringLiteral("COPY_ONLY")) { |
| 746 |
1/1✓ Branch 59 → 60 taken 89 times.
|
89 | return static_cast<quint64>(qMax(command.storeToken, command.mutationTokens.value(0))); |
| 747 | } | ||
| 748 | 592 | return 0; | |
| 749 | } | ||
| 750 | |||
| 751 | 1065 | void ImapService::reportE2eTransportPhase(const QString &phase, | |
| 752 | quint64 requestToken) { | ||
| 753 | // Setup and IDLE phases do not own a body/mutation request ID. Use the | ||
| 754 | // current tagged-command sequence (or 1 before the first tag) so external | ||
| 755 | // fault controls still receive a non-zero opaque correlation token. | ||
| 756 | const quint64 correlationToken = | ||
| 757 | 1065 | requestToken > 0 | |
| 758 |
2/2✓ Branch 2 → 3 taken 155 times.
✓ Branch 2 → 4 taken 910 times.
|
1975 | ? requestToken |
| 759 | 910 | : qMax<quint64>(1, static_cast<quint64>(m_commands->tagCounter())); | |
| 760 | 1065 | emit e2eTransportPhase(phase, correlationToken); | |
| 761 | 1065 | } | |
| 762 | #endif | ||
| 763 | |||
| 764 | 158 | QString ImapService::quoteImapString(const QString &str) { | |
| 765 | // RFC 3501: quoted strings must not contain CR, LF, or NUL | ||
| 766 | 158 | QString escaped = str; | |
| 767 |
1/1✓ Branch 4 → 5 taken 158 times.
|
158 | escaped.remove('\r'); |
| 768 |
1/1✓ Branch 6 → 7 taken 158 times.
|
158 | escaped.remove('\n'); |
| 769 |
1/1✓ Branch 8 → 9 taken 158 times.
|
158 | escaped.remove(QChar(0)); |
| 770 |
2/2✓ Branch 9 → 10 taken 158 times.
✓ Branch 11 → 12 taken 158 times.
|
158 | escaped.replace('\\', "\\\\"); |
| 771 |
2/2✓ Branch 13 → 14 taken 158 times.
✓ Branch 15 → 16 taken 158 times.
|
158 | escaped.replace('"', "\\\""); |
| 772 |
2/2✓ Branch 19 → 20 taken 158 times.
✓ Branch 20 → 21 taken 158 times.
|
474 | return '"' + escaped + '"'; |
| 773 | 158 | } | |
| 774 | |||
| 775 | 165 | QString ImapService::quoteMailboxName(const QString &path) { | |
| 776 | // Mailbox paths are kept in their wire form (modified UTF-7, RFC 3501 | ||
| 777 | // §5.1.3); this only quotes them (S106-968). The service keeps the | ||
| 778 | // forwarding name so every send site reads the same way. | ||
| 779 | 165 | return ImapResponseParser::quoteMailboxName(path); | |
| 780 | } | ||
| 781 | |||
| 782 | 82 | bool ImapService::isValidImapFlag(const QString &flag) { | |
| 783 |
2/2✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 80 times.
|
82 | if (flag.isEmpty()) |
| 784 | 2 | return false; | |
| 785 |
3/3✓ Branch 7 → 8 taken 80 times.
✓ Branch 8 → 9 taken 63 times.
✓ Branch 8 → 10 taken 17 times.
|
80 | const qsizetype start = flag.startsWith(QLatin1Char('\\')) ? 1 : 0; |
| 786 |
2/2✓ Branch 12 → 13 taken 2 times.
✓ Branch 12 → 14 taken 78 times.
|
80 | if (start == flag.size()) |
| 787 | 2 | return false; | |
| 788 |
3/4✓ Branch 14 → 15 taken 8 times.
✓ Branch 14 → 24 taken 70 times.
✓ Branch 16 → 17 taken 8 times.
✗ Branch 16 → 24 not taken.
|
86 | static const QString atomSpecials = QStringLiteral("(){ %*\"\\]}"); |
| 789 |
2/2✓ Branch 41 → 25 taken 382 times.
✓ Branch 41 → 42 taken 58 times.
|
440 | for (qsizetype i = start; i < flag.size(); ++i) { |
| 790 | 382 | const QChar c = flag.at(i); | |
| 791 | 382 | const ushort u = c.unicode(); | |
| 792 |
8/8✓ Branch 27 → 28 taken 376 times.
✓ Branch 27 → 35 taken 6 times.
✓ Branch 28 → 29 taken 375 times.
✓ Branch 28 → 35 taken 1 time.
✓ Branch 32 → 33 taken 373 times.
✓ Branch 32 → 35 taken 2 times.
✓ Branch 37 → 38 taken 20 times.
✓ Branch 37 → 39 taken 362 times.
|
755 | if (u <= 0x20 || u == 0x7F || c == QLatin1Char('\\') || |
| 793 |
3/3✓ Branch 33 → 34 taken 373 times.
✓ Branch 34 → 35 taken 11 times.
✓ Branch 34 → 36 taken 362 times.
|
373 | atomSpecials.contains(c)) { |
| 794 | 20 | return false; | |
| 795 | } | ||
| 796 | } | ||
| 797 | 58 | return true; | |
| 798 | } | ||
| 799 | |||
| 800 | 100444 | void ImapService::handleUntagged(const QString &line) { | |
| 801 |
1/1✓ Branch 2 → 3 taken 100444 times.
|
100444 | auto response = ImapResponseParser::parseUntaggedResponse(line); |
| 802 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 100444 times.
|
100444 | if (!response) |
| 803 | ✗ | return; | |
| 804 | |||
| 805 | 100444 | const auto &type = response->type; | |
| 806 | 100444 | const auto &data = response->data; | |
| 807 | |||
| 808 |
7/8✓ Branch 8 → 9 taken 54 times.
✓ Branch 8 → 14 taken 100390 times.
✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 13 taken 52 times.
✓ Branch 12 → 13 taken 2 times.
✗ Branch 12 → 14 not taken.
✓ Branch 15 → 16 taken 54 times.
✓ Branch 15 → 20 taken 100390 times.
|
100444 | if (m_state == State::Connected && (type == "OK" || type == "PREAUTH")) { |
| 809 |
1/1✓ Branch 18 → 19 taken 54 times.
|
54 | m_setup->handleGreeting(data, type == "PREAUTH"); |
| 810 | 54 | return; | |
| 811 | } | ||
| 812 | |||
| 813 |
2/2✓ Branch 21 → 22 taken 58 times.
✓ Branch 21 → 25 taken 100332 times.
|
100390 | if (type == "CAPABILITY") { |
| 814 |
1/1✓ Branch 23 → 24 taken 58 times.
|
58 | m_setup->collectCapabilities(data); |
| 815 | 58 | return; | |
| 816 | } | ||
| 817 | |||
| 818 |
2/2✓ Branch 26 → 27 taken 22 times.
✓ Branch 26 → 29 taken 100310 times.
|
100332 | if (type == "LIST") { |
| 819 |
1/1✓ Branch 27 → 28 taken 22 times.
|
22 | handleUntaggedList(data); |
| 820 | 22 | return; | |
| 821 | } | ||
| 822 | |||
| 823 | // FETCH responses: "* N FETCH (...)" | ||
| 824 |
4/4✓ Branch 29 → 30 taken 100310 times.
✓ Branch 30 → 31 taken 100310 times.
✓ Branch 32 → 33 taken 89 times.
✓ Branch 32 → 35 taken 100221 times.
|
100310 | if (data.startsWith("FETCH")) { |
| 825 |
1/1✓ Branch 33 → 34 taken 89 times.
|
89 | handleUntaggedFetch(type, data); |
| 826 | 89 | return; | |
| 827 | } | ||
| 828 | |||
| 829 |
2/2✓ Branch 36 → 37 taken 4 times.
✓ Branch 36 → 39 taken 100217 times.
|
100221 | if (type == "ESEARCH") { |
| 830 |
1/1✓ Branch 37 → 38 taken 4 times.
|
4 | handleUntaggedEsearch(data); |
| 831 | 4 | return; | |
| 832 | } | ||
| 833 | |||
| 834 |
2/2✓ Branch 40 → 41 taken 42 times.
✓ Branch 40 → 43 taken 100175 times.
|
100217 | if (type == "SEARCH") { |
| 835 |
1/1✓ Branch 41 → 42 taken 42 times.
|
42 | handleUntaggedSearch(data); |
| 836 | 42 | return; | |
| 837 | } | ||
| 838 | |||
| 839 | // EXISTS response: "* N EXISTS" → type="N", data="EXISTS" | ||
| 840 |
2/2✓ Branch 44 → 45 taken 48 times.
✓ Branch 44 → 47 taken 100127 times.
|
100175 | if (data == "EXISTS") { |
| 841 |
1/1✓ Branch 45 → 46 taken 48 times.
|
48 | handleUntaggedExists(type); |
| 842 | 48 | return; | |
| 843 | } | ||
| 844 | |||
| 845 | // EXPUNGE response: "* N EXPUNGE" → type="N", data="EXPUNGE" | ||
| 846 |
2/2✓ Branch 48 → 49 taken 6 times.
✓ Branch 48 → 51 taken 100121 times.
|
100127 | if (data == "EXPUNGE") { |
| 847 |
1/1✓ Branch 49 → 50 taken 6 times.
|
6 | handleUntaggedExpunge(type); |
| 848 | 6 | return; | |
| 849 | } | ||
| 850 | |||
| 851 | // STATUS response: "* STATUS "INBOX" (MESSAGES 42 UNSEEN 3 RECENT 0)" | ||
| 852 |
2/2✓ Branch 52 → 53 taken 23 times.
✓ Branch 52 → 55 taken 100098 times.
|
100121 | if (type == "STATUS") { |
| 853 |
1/1✓ Branch 53 → 54 taken 23 times.
|
23 | handleUntaggedStatus(data); |
| 854 | 23 | return; | |
| 855 | } | ||
| 856 | |||
| 857 | // UIDVALIDITY in OK response: "* OK [UIDVALIDITY 12345]" | ||
| 858 |
2/2✓ Branch 56 → 57 taken 100070 times.
✓ Branch 56 → 59 taken 28 times.
|
100098 | if (type == "OK") { |
| 859 |
1/1✓ Branch 57 → 58 taken 100070 times.
|
100070 | handleUntaggedOk(data); |
| 860 | 100070 | return; | |
| 861 | } | ||
| 862 | |||
| 863 |
2/2✓ Branch 60 → 61 taken 2 times.
✓ Branch 60 → 74 taken 26 times.
|
28 | if (type == "BYE") { |
| 864 |
6/6✓ Branch 61 → 62 taken 2 times.
✓ Branch 65 → 66 taken 2 times.
✓ Branch 66 → 67 taken 2 times.
✓ Branch 67 → 68 taken 2 times.
✓ Branch 71 → 63 taken 2 times.
✓ Branch 71 → 72 taken 2 times.
|
4 | qCInfo(lcImap) << "Server BYE:" << data; |
| 865 |
1/1✓ Branch 72 → 73 taken 2 times.
|
2 | disconnect(DisconnectReason::RemoteBye); |
| 866 | 2 | return; | |
| 867 | } | ||
| 868 | 100444 | } | |
| 869 | |||
| 870 | 22 | void ImapService::handleUntaggedList(const QString &data) { | |
| 871 |
1/1✓ Branch 2 → 3 taken 22 times.
|
22 | auto folder = ImapResponseParser::parseListResponse(data); |
| 872 |
2/2✓ Branch 4 → 5 taken 21 times.
✓ Branch 4 → 11 taken 1 time.
|
22 | if (folder) { |
| 873 |
2/3✓ Branch 6 → 7 taken 21 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 21 times.
|
21 | if (!m_commands->accountCommandResultItems()) |
| 874 | ✗ | return; | |
| 875 |
2/2✓ Branch 9 → 10 taken 21 times.
✓ Branch 10 → 11 taken 21 times.
|
21 | m_pendingFolders.append(folder.value()); |
| 876 | } | ||
| 877 | 22 | } | |
| 878 | |||
| 879 | // FETCH responses: "* N FETCH (...)" | ||
| 880 | 89 | void ImapService::handleUntaggedFetch(const QString &type, const QString &data) { | |
| 881 | // type is the sequence number, data starts with "FETCH" | ||
| 882 |
2/2✓ Branch 2 → 3 taken 89 times.
✓ Branch 3 → 4 taken 89 times.
|
89 | auto fetchData = data.mid(5).trimmed(); // skip "FETCH" |
| 883 | |||
| 884 | // Check if this is a flags-only fetch (from fetchFlags or IDLE/NOTIFY push) | ||
| 885 |
11/16✓ Branch 5 → 6 taken 89 times.
✓ Branch 6 → 7 taken 89 times.
✓ Branch 7 → 8 taken 88 times.
✓ Branch 7 → 15 taken 1 time.
✓ Branch 8 → 9 taken 88 times.
✓ Branch 9 → 10 taken 88 times.
✓ Branch 10 → 11 taken 23 times.
✓ Branch 10 → 15 taken 65 times.
✓ Branch 20 → 21 taken 89 times.
✗ Branch 20 → 22 not taken.
✓ Branch 22 → 23 taken 22 times.
✓ Branch 22 → 92 taken 67 times.
✗ Branch 156 → 157 not taken.
✗ Branch 156 → 158 not taken.
✗ Branch 160 → 161 not taken.
✗ Branch 160 → 162 not taken.
|
201 | if (fetchData.contains("FLAGS") && !fetchData.contains("ENVELOPE") && |
| 886 |
8/10✓ Branch 11 → 12 taken 23 times.
✓ Branch 12 → 13 taken 23 times.
✓ Branch 13 → 14 taken 22 times.
✓ Branch 13 → 15 taken 1 time.
✓ Branch 16 → 17 taken 23 times.
✓ Branch 16 → 18 taken 66 times.
✓ Branch 18 → 19 taken 88 times.
✓ Branch 18 → 20 taken 1 time.
✗ Branch 152 → 153 not taken.
✗ Branch 152 → 154 not taken.
|
112 | !fetchData.contains("BODY[]")) { |
| 887 | // Parse UID and FLAGS from "(UID 123 FLAGS (\Seen \Flagged))" | ||
| 888 |
1/1✓ Branch 23 → 24 taken 22 times.
|
22 | auto parsed = ImapResponseParser::parseFetchFlagSnapshot(fetchData); |
| 889 | |||
| 890 | // T-320: Determine if this is a command response or unsolicited push. | ||
| 891 | // With IDLE: no commands in-flight → always unsolicited. | ||
| 892 | // With NOTIFY: commands run concurrently → check pending commands. | ||
| 893 | bool isCommandResponse = | ||
| 894 |
1/3✓ Branch 25 → 26 taken 22 times.
✗ Branch 180 → 181 not taken.
✗ Branch 180 → 182 not taken.
|
44 | m_commands->hasCommandType("FETCH_FLAGS") || |
| 895 |
5/8✓ Branch 29 → 30 taken 14 times.
✓ Branch 30 → 31 taken 14 times.
✓ Branch 31 → 32 taken 9 times.
✓ Branch 31 → 44 taken 5 times.
✓ Branch 54 → 55 taken 22 times.
✗ Branch 54 → 56 not taken.
✗ Branch 176 → 177 not taken.
✗ Branch 176 → 178 not taken.
|
36 | m_commands->hasCommandType("FETCH_FLAGS_CHANGED") || |
| 896 |
6/8✓ Branch 33 → 34 taken 9 times.
✓ Branch 34 → 35 taken 9 times.
✓ Branch 35 → 36 taken 8 times.
✓ Branch 35 → 44 taken 1 time.
✓ Branch 52 → 53 taken 14 times.
✓ Branch 52 → 54 taken 8 times.
✗ Branch 172 → 173 not taken.
✗ Branch 172 → 174 not taken.
|
31 | m_commands->hasCommandType("FETCH_VERIFY_FLAGS") || |
| 897 |
9/11✓ Branch 26 → 27 taken 22 times.
✓ Branch 27 → 28 taken 14 times.
✓ Branch 27 → 44 taken 8 times.
✓ Branch 37 → 38 taken 8 times.
✓ Branch 38 → 39 taken 8 times.
✓ Branch 39 → 40 taken 6 times.
✓ Branch 39 → 44 taken 2 times.
✓ Branch 50 → 51 taken 9 times.
✓ Branch 50 → 52 taken 13 times.
✗ Branch 168 → 169 not taken.
✗ Branch 168 → 170 not taken.
|
86 | m_commands->hasCommandType("FETCH_SEQNO") || |
| 898 |
8/10✓ Branch 41 → 42 taken 6 times.
✓ Branch 42 → 43 taken 6 times.
✓ Branch 43 → 44 taken 1 time.
✓ Branch 43 → 45 taken 5 times.
✓ Branch 46 → 47 taken 6 times.
✓ Branch 46 → 48 taken 16 times.
✓ Branch 48 → 49 taken 8 times.
✓ Branch 48 → 50 taken 14 times.
✗ Branch 164 → 165 not taken.
✗ Branch 164 → 166 not taken.
|
28 | m_commands->hasCommandType("FETCH_HEADERS"); |
| 899 | |||
| 900 |
2/2✓ Branch 57 → 58 taken 17 times.
✓ Branch 57 → 72 taken 5 times.
|
22 | if (parsed) { |
| 901 |
2/2✓ Branch 58 → 59 taken 16 times.
✓ Branch 58 → 69 taken 1 time.
|
17 | if (isCommandResponse) { |
| 902 | // Response to explicit FETCH → accumulate for batch emit | ||
| 903 |
2/3✓ Branch 60 → 61 taken 16 times.
✗ Branch 61 → 62 not taken.
✓ Branch 61 → 63 taken 16 times.
|
16 | if (!m_commands->accountCommandResultItems()) |
| 904 | ✗ | return; | |
| 905 |
1/1✓ Branch 66 → 67 taken 16 times.
|
16 | m_pendingFlags.append({parsed->uid, parsed->flags}); |
| 906 |
2/2✓ Branch 67 → 68 taken 16 times.
✓ Branch 68 → 89 taken 16 times.
|
16 | m_pendingFlagSnapshots.append(parsed.value()); |
| 907 | } else { | ||
| 908 | // Unsolicited push (IDLE or NOTIFY event) → emit immediately | ||
| 909 |
1/1✓ Branch 71 → 89 taken 1 time.
|
1 | emit idleFlagsChanged(parsed->uid, parsed->flags); |
| 910 | } | ||
| 911 |
2/2✓ Branch 72 → 73 taken 4 times.
✓ Branch 72 → 89 taken 1 time.
|
5 | } else if (!isCommandResponse) { |
| 912 | // T-065: Unsolicited flag push WITHOUT UID (common for IMAP servers). | ||
| 913 | // The sequence number is in the 'type' field from parseUntaggedResponse. | ||
| 914 | 4 | bool ok = false; | |
| 915 |
1/1✓ Branch 73 → 74 taken 4 times.
|
4 | int seqNo = type.toInt(&ok); |
| 916 |
4/4✓ Branch 74 → 75 taken 3 times.
✓ Branch 74 → 88 taken 1 time.
✓ Branch 75 → 76 taken 2 times.
✓ Branch 75 → 88 taken 1 time.
|
4 | if (ok && seqNo > 0) { |
| 917 |
6/6✓ Branch 76 → 77 taken 2 times.
✓ Branch 80 → 81 taken 2 times.
✓ Branch 81 → 82 taken 2 times.
✓ Branch 82 → 83 taken 2 times.
✓ Branch 86 → 78 taken 2 times.
✓ Branch 86 → 87 taken 2 times.
|
4 | qCInfo(lcImap) << "Push flag change without UID, seqNo:" << seqNo; |
| 918 |
1/1✓ Branch 87 → 88 taken 2 times.
|
2 | emit idleFlagsNeedRefetch(seqNo); |
| 919 | } | ||
| 920 | } | ||
| 921 | 22 | return; | |
| 922 | 22 | } | |
| 923 | |||
| 924 | // Check if this is a header or body fetch | ||
| 925 |
4/4✓ Branch 92 → 93 taken 67 times.
✓ Branch 93 → 94 taken 67 times.
✓ Branch 95 → 96 taken 65 times.
✓ Branch 95 → 142 taken 2 times.
|
67 | if (fetchData.contains("ENVELOPE")) { |
| 926 |
1/1✓ Branch 96 → 97 taken 65 times.
|
65 | auto header = ImapResponseParser::parseFetchHeaderResponse(fetchData); |
| 927 |
2/2✓ Branch 98 → 99 taken 64 times.
✓ Branch 98 → 118 taken 1 time.
|
65 | if (header) { |
| 928 |
2/3✓ Branch 100 → 101 taken 64 times.
✗ Branch 101 → 102 not taken.
✓ Branch 101 → 103 taken 64 times.
|
64 | if (!m_commands->accountCommandResultItems()) |
| 929 | ✗ | return; | |
| 930 |
2/2✓ Branch 103 → 104 taken 64 times.
✓ Branch 104 → 105 taken 64 times.
|
64 | m_pendingHeaders.append(header.value()); |
| 931 |
6/6✓ Branch 105 → 106 taken 64 times.
✓ Branch 109 → 110 taken 64 times.
✓ Branch 110 → 111 taken 64 times.
✓ Branch 112 → 113 taken 64 times.
✓ Branch 116 → 107 taken 64 times.
✓ Branch 116 → 117 taken 64 times.
|
128 | qCDebug(lcImap) << "Parsed header for UID" << header->uid; |
| 932 | } else { | ||
| 933 | // parseFetchHeaderResponse() returns nullopt ONLY when the FETCH | ||
| 934 | // data carries no UID (everything else is parsed tolerantly with | ||
| 935 | // empty fields). The former T-067 "fallback header" branch here | ||
| 936 | // re-extracted the UID from the same data and could therefore | ||
| 937 | // never succeed — verified dead code, removed in Sprint 65. | ||
| 938 |
5/5✓ Branch 118 → 119 taken 1 time.
✓ Branch 122 → 123 taken 1 time.
✓ Branch 123 → 124 taken 1 time.
✓ Branch 130 → 120 taken 1 time.
✓ Branch 130 → 131 taken 1 time.
|
2 | qCWarning(lcImap) << "FETCH without UID – mail dropped! data:" |
| 939 |
2/2✓ Branch 124 → 125 taken 1 time.
✓ Branch 125 → 126 taken 1 time.
|
1 | << fetchData.left(300); |
| 940 | } | ||
| 941 | // Streaming: emit batch when threshold reached | ||
| 942 |
2/2✓ Branch 133 → 134 taken 1 time.
✓ Branch 133 → 136 taken 64 times.
|
65 | if (m_pendingHeaders.size() >= HEADER_BATCH_SIZE) { |
| 943 |
1/1✓ Branch 134 → 135 taken 1 time.
|
1 | emit headersReceived(m_pendingHeaders); |
| 944 |
1/1✓ Branch 135 → 136 taken 1 time.
|
1 | m_pendingHeaders.clear(); |
| 945 | } | ||
| 946 | 65 | } | |
| 947 | 89 | } | |
| 948 | |||
| 949 | // S106-022: SEARCH/ESEARCH results count only while a search command is on | ||
| 950 | // the wire. Unsolicited ones (during IDLE no command budget is active) used | ||
| 951 | // to grow m_pendingSearchUids without bound until the next search. | ||
| 952 | 46 | bool ImapService::searchResultExpected() const { | |
| 953 |
4/9✓ Branch 6 → 7 taken 46 times.
✓ Branch 7 → 8 taken 6 times.
✓ Branch 7 → 14 taken 40 times.
✓ Branch 22 → 23 taken 46 times.
✗ Branch 22 → 24 not taken.
✗ Branch 34 → 35 not taken.
✗ Branch 34 → 36 not taken.
✗ Branch 38 → 39 not taken.
✗ Branch 38 → 40 not taken.
|
144 | return m_commands->hasCommandType(QStringLiteral("SEARCH")) || |
| 954 |
8/13✓ Branch 12 → 13 taken 6 times.
✓ Branch 13 → 14 taken 3 times.
✓ Branch 13 → 15 taken 3 times.
✓ Branch 16 → 17 taken 6 times.
✓ Branch 16 → 18 taken 40 times.
✓ Branch 18 → 19 taken 6 times.
✓ Branch 18 → 20 taken 40 times.
✓ Branch 20 → 21 taken 46 times.
✗ Branch 20 → 22 not taken.
✗ Branch 26 → 27 not taken.
✗ Branch 26 → 28 not taken.
✗ Branch 30 → 31 not taken.
✗ Branch 30 → 32 not taken.
|
104 | m_commands->hasCommandType(QStringLiteral("SEARCH_MEMBERSHIP")); |
| 955 | } | ||
| 956 | |||
| 957 | 4 | void ImapService::handleUntaggedEsearch(const QString &data) { | |
| 958 | // RFC 4731: '(TAG "A5") UID ALL 1:4,6:30' – only ALL is requested. | ||
| 959 |
2/2✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 15 taken 3 times.
|
4 | if (!searchResultExpected()) { |
| 960 |
5/5✓ Branch 4 → 5 taken 1 time.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 13 → 6 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
|
2 | qCDebug(lcImap) << "Unsolicited ESEARCH ignored"; |
| 961 | 1 | return; | |
| 962 | } | ||
| 963 |
1/1✓ Branch 18 → 19 taken 3 times.
|
3 | const int all = data.indexOf(QStringLiteral(" ALL ")); |
| 964 |
1/2✓ Branch 21 → 22 taken 3 times.
✗ Branch 21 → 57 not taken.
|
3 | if (all >= 0) { |
| 965 |
3/3✓ Branch 22 → 23 taken 3 times.
✓ Branch 23 → 24 taken 3 times.
✓ Branch 26 → 27 taken 3 times.
|
6 | const auto set = data.mid(all + 5).trimmed().section(' ', 0, 0); |
| 966 |
1/1✓ Branch 29 → 30 taken 3 times.
|
3 | const auto uids = ImapResponseParser::expandUidSet(set, MAX_RESULT_ITEMS_PER_COMMAND); |
| 967 |
2/2✓ Branch 31 → 32 taken 1 time.
✓ Branch 31 → 39 taken 2 times.
|
3 | if (!uids) { |
| 968 |
1/1✓ Branch 35 → 36 taken 1 time.
|
1 | failConnection(QStringLiteral("Malformed or oversized ESEARCH result")); |
| 969 | 1 | return; | |
| 970 | } | ||
| 971 |
2/3✓ Branch 42 → 43 taken 2 times.
✗ Branch 43 → 44 not taken.
✓ Branch 43 → 45 taken 2 times.
|
2 | if (!m_commands->accountCommandResultItems(uids->size())) |
| 972 | ✗ | return; | |
| 973 |
1/1✓ Branch 46 → 47 taken 2 times.
|
2 | m_pendingSearchUids.append(*uids); |
| 974 | 4 | } | |
| 975 | } | ||
| 976 | |||
| 977 | 42 | void ImapService::handleUntaggedSearch(const QString &data) { | |
| 978 | // SEARCH response: data contains space-separated UIDs | ||
| 979 | // e.g. type="SEARCH", data="1 2 3 45 67" | ||
| 980 |
2/2✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 15 taken 40 times.
|
42 | if (!searchResultExpected()) { |
| 981 |
5/5✓ Branch 4 → 5 taken 2 times.
✓ Branch 8 → 9 taken 2 times.
✓ Branch 9 → 10 taken 2 times.
✓ Branch 13 → 6 taken 2 times.
✓ Branch 13 → 14 taken 2 times.
|
4 | qCDebug(lcImap) << "Unsolicited SEARCH ignored"; |
| 982 | 2 | return; | |
| 983 | } | ||
| 984 |
2/2✓ Branch 16 → 17 taken 39 times.
✓ Branch 16 → 40 taken 1 time.
|
40 | if (!data.isEmpty()) { |
| 985 |
5/5✓ Branch 19 → 20 taken 39 times.
✓ Branch 20 → 21 taken 39 times.
✓ Branch 21 → 22 taken 39 times.
✓ Branch 33 → 23 taken 56 times.
✓ Branch 33 → 34 taken 39 times.
|
95 | for (const auto &token : data.split(' ', Qt::SkipEmptyParts)) { |
| 986 | 56 | bool ok = false; | |
| 987 |
1/1✓ Branch 24 → 25 taken 56 times.
|
56 | qint64 uid = token.toLongLong(&ok); |
| 988 |
1/2✓ Branch 25 → 26 taken 56 times.
✗ Branch 25 → 31 not taken.
|
56 | if (ok) { |
| 989 |
2/3✓ Branch 27 → 28 taken 56 times.
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 30 taken 56 times.
|
56 | if (!m_commands->accountCommandResultItems()) |
| 990 | ✗ | return; | |
| 991 |
1/1✓ Branch 30 → 31 taken 56 times.
|
56 | m_pendingSearchUids.append(uid); |
| 992 | } | ||
| 993 | 39 | } | |
| 994 | } | ||
| 995 | } | ||
| 996 | |||
| 997 | // EXISTS response: "* N EXISTS" → type="N", data="EXISTS" | ||
| 998 | 48 | void ImapService::handleUntaggedExists(const QString &type) { | |
| 999 | 48 | bool ok = false; | |
| 1000 |
1/1✓ Branch 2 → 3 taken 48 times.
|
48 | int count = type.toInt(&ok); |
| 1001 |
2/2✓ Branch 3 → 4 taken 47 times.
✓ Branch 3 → 40 taken 1 time.
|
48 | if (ok) { |
| 1002 | // T-320: only SELECT reports the mailbox size as part of its answer. | ||
| 1003 | // Growth while a FETCH runs is a real arrival: servers announce new | ||
| 1004 | // mail between any responses, and a FETCH 1:* may already be past the | ||
| 1005 | // new message. Dropping it lost the arrival until the next sync. | ||
| 1006 |
2/2✓ Branch 5 → 6 taken 47 times.
✓ Branch 6 → 7 taken 47 times.
|
47 | bool isCommandResponse = m_commands->hasCommandType("SELECT"); |
| 1007 |
4/4✓ Branch 8 → 9 taken 8 times.
✓ Branch 8 → 26 taken 39 times.
✓ Branch 9 → 10 taken 7 times.
✓ Branch 9 → 26 taken 1 time.
|
47 | if (!isCommandResponse && count > m_selectedMessageCount) { |
| 1008 | // New messages arrived (IDLE or NOTIFY push) | ||
| 1009 |
6/6✓ Branch 10 → 11 taken 7 times.
✓ Branch 14 → 15 taken 7 times.
✓ Branch 15 → 16 taken 7 times.
✓ Branch 16 → 17 taken 7 times.
✓ Branch 23 → 12 taken 7 times.
✓ Branch 23 → 24 taken 7 times.
|
14 | qCInfo(lcImap) << "Push: new messages detected," << count |
| 1010 |
3/3✓ Branch 17 → 18 taken 7 times.
✓ Branch 18 → 19 taken 7 times.
✓ Branch 19 → 20 taken 7 times.
|
7 | << "total (was" << m_selectedMessageCount << ")"; |
| 1011 | 7 | int newCount = count - m_selectedMessageCount; | |
| 1012 | 7 | m_selectedMessageCount = count; | |
| 1013 |
1/1✓ Branch 24 → 25 taken 7 times.
|
7 | emit idleNewMessages(newCount); |
| 1014 | 7 | } else { | |
| 1015 | 40 | m_selectedMessageCount = count; | |
| 1016 |
7/7✓ Branch 26 → 27 taken 40 times.
✓ Branch 30 → 31 taken 40 times.
✓ Branch 31 → 32 taken 40 times.
✓ Branch 32 → 33 taken 40 times.
✓ Branch 33 → 34 taken 40 times.
✓ Branch 37 → 28 taken 40 times.
✓ Branch 37 → 38 taken 40 times.
|
80 | qCInfo(lcImap) << "EXISTS:" << count << "messages"; |
| 1017 | } | ||
| 1018 | } | ||
| 1019 | 48 | } | |
| 1020 | |||
| 1021 | // EXPUNGE response: "* N EXPUNGE" → type="N", data="EXPUNGE" | ||
| 1022 | 6 | void ImapService::handleUntaggedExpunge(const QString &type) { | |
| 1023 | 6 | bool ok = false; | |
| 1024 |
1/1✓ Branch 2 → 3 taken 6 times.
|
6 | int seqNo = type.toInt(&ok); |
| 1025 |
2/2✓ Branch 3 → 4 taken 5 times.
✓ Branch 3 → 39 taken 1 time.
|
6 | if (ok) { |
| 1026 | 5 | m_selectedMessageCount = qMax(0, m_selectedMessageCount - 1); | |
| 1027 |
7/7✓ Branch 5 → 6 taken 5 times.
✓ Branch 9 → 10 taken 5 times.
✓ Branch 10 → 11 taken 5 times.
✓ Branch 11 → 12 taken 5 times.
✓ Branch 12 → 13 taken 5 times.
✓ Branch 16 → 7 taken 5 times.
✓ Branch 16 → 17 taken 5 times.
|
10 | qCInfo(lcImap) << "EXPUNGE: message" << seqNo << "removed"; |
| 1028 | // T-320: Only emit push signal if no command caused this EXPUNGE | ||
| 1029 | bool isCommandResponse = | ||
| 1030 |
1/3✓ Branch 18 → 19 taken 5 times.
✗ Branch 53 → 54 not taken.
✗ Branch 53 → 55 not taken.
|
10 | m_commands->hasCommandType("EXPUNGE") || |
| 1031 |
8/11✓ Branch 19 → 20 taken 5 times.
✓ Branch 20 → 21 taken 4 times.
✓ Branch 20 → 29 taken 1 time.
✓ Branch 22 → 23 taken 4 times.
✓ Branch 23 → 24 taken 4 times.
✓ Branch 24 → 25 taken 3 times.
✓ Branch 24 → 29 taken 1 time.
✓ Branch 35 → 36 taken 5 times.
✗ Branch 35 → 37 not taken.
✗ Branch 49 → 50 not taken.
✗ Branch 49 → 51 not taken.
|
18 | m_commands->hasCommandType("MOVE") || |
| 1032 |
8/10✓ Branch 26 → 27 taken 3 times.
✓ Branch 27 → 28 taken 3 times.
✓ Branch 28 → 29 taken 1 time.
✓ Branch 28 → 30 taken 2 times.
✓ Branch 31 → 32 taken 3 times.
✓ Branch 31 → 33 taken 2 times.
✓ Branch 33 → 34 taken 4 times.
✓ Branch 33 → 35 taken 1 time.
✗ Branch 45 → 46 not taken.
✗ Branch 45 → 47 not taken.
|
8 | m_commands->hasCommandType("SELECT"); |
| 1033 |
2/2✓ Branch 37 → 38 taken 2 times.
✓ Branch 37 → 39 taken 3 times.
|
5 | if (!isCommandResponse) { |
| 1034 |
1/1✓ Branch 38 → 39 taken 2 times.
|
2 | emit idleMessageExpunged(seqNo); |
| 1035 | } | ||
| 1036 | } | ||
| 1037 | 6 | } | |
| 1038 | |||
| 1039 | // STATUS response: "* STATUS "INBOX" (MESSAGES 42 UNSEEN 3 RECENT 0)" | ||
| 1040 | 23 | void ImapService::handleUntaggedStatus(const QString &data) { | |
| 1041 | // data = '"INBOX" (MESSAGES 42 UNSEEN 3 RECENT 0)' | ||
| 1042 |
1/1✓ Branch 2 → 3 taken 23 times.
|
23 | auto parsed = ImapResponseParser::parseStatusResponse(data); |
| 1043 |
2/2✓ Branch 4 → 5 taken 21 times.
✓ Branch 4 → 30 taken 2 times.
|
23 | if (parsed) { |
| 1044 |
1/1✓ Branch 9 → 10 taken 21 times.
|
42 | ImapPendingCommand *status = m_commands->recordOfType(QStringLiteral("STATUS")); |
| 1045 |
6/6✓ Branch 12 → 13 taken 14 times.
✓ Branch 12 → 17 taken 7 times.
✓ Branch 15 → 16 taken 13 times.
✓ Branch 15 → 17 taken 1 time.
✓ Branch 18 → 19 taken 13 times.
✓ Branch 18 → 28 taken 8 times.
|
21 | if (status && parsed->folderPath == status->statusFolder) { |
| 1046 | // A queued NOTIFY push may precede the actual reply. Publish the last | ||
| 1047 | // matching snapshot only after the command's tagged success boundary. | ||
| 1048 |
6/6✓ Branch 20 → 21 taken 3 times.
✓ Branch 20 → 23 taken 10 times.
✓ Branch 22 → 23 taken 2 times.
✓ Branch 22 → 24 taken 1 time.
✓ Branch 25 → 26 taken 12 times.
✓ Branch 25 → 30 taken 1 time.
|
13 | if (parsed->hasUnseen || !status->status) |
| 1049 | 12 | status->status = *parsed; | |
| 1050 | } else { | ||
| 1051 |
1/1✓ Branch 29 → 30 taken 8 times.
|
8 | emit folderStatusReceived(*parsed); |
| 1052 | } | ||
| 1053 | } | ||
| 1054 | 23 | } | |
| 1055 | |||
| 1056 | // UIDVALIDITY in OK response: "* OK [UIDVALIDITY 12345]" | ||
| 1057 | 100070 | void ImapService::handleUntaggedOk(const QString &data) { | |
| 1058 | // S106-023: UIDVALIDITY and HIGHESTMODSEQ describe the mailbox a SELECT | ||
| 1059 | // opens. Outside one they are ignored — adopting them decoupled the epoch | ||
| 1060 | // and the CONDSTORE watermark from the mailbox actually selected (a | ||
| 1061 | // raised watermark makes the next CHANGEDSINCE skip unseen changes). | ||
| 1062 |
3/3✓ Branch 6 → 7 taken 100070 times.
✓ Branch 9 → 10 taken 100004 times.
✓ Branch 9 → 34 taken 66 times.
|
200140 | if (!m_commands->hasCommandType(QStringLiteral("SELECT"))) { |
| 1063 |
3/3✓ Branch 11 → 12 taken 100004 times.
✓ Branch 12 → 13 taken 100003 times.
✓ Branch 12 → 16 taken 1 time.
|
200007 | if (data.contains(QLatin1String("[UIDVALIDITY"), Qt::CaseInsensitive) || |
| 1064 |
5/5✓ Branch 14 → 15 taken 100003 times.
✓ Branch 15 → 16 taken 1 time.
✓ Branch 15 → 17 taken 100002 times.
✓ Branch 18 → 19 taken 2 times.
✓ Branch 18 → 33 taken 100002 times.
|
200007 | data.contains(QLatin1String("[HIGHESTMODSEQ"), Qt::CaseInsensitive)) |
| 1065 |
7/7✓ Branch 19 → 20 taken 2 times.
✓ Branch 23 → 24 taken 2 times.
✓ Branch 24 → 25 taken 2 times.
✓ Branch 25 → 26 taken 2 times.
✓ Branch 26 → 27 taken 2 times.
✓ Branch 31 → 21 taken 2 times.
✓ Branch 31 → 32 taken 2 times.
|
4 | qCInfo(lcImap) << "Mailbox epoch outside a SELECT ignored:" << data.left(80); |
| 1066 | 100004 | return; | |
| 1067 | } | ||
| 1068 |
1/1✓ Branch 34 → 35 taken 66 times.
|
66 | auto uv = ImapResponseParser::parseUidValidity(data); |
| 1069 |
2/2✓ Branch 36 → 37 taken 39 times.
✓ Branch 36 → 50 taken 27 times.
|
66 | if (uv) { |
| 1070 |
1/1✓ Branch 37 → 38 taken 39 times.
|
39 | m_selectedUidValidity = uv.value(); |
| 1071 |
6/6✓ Branch 38 → 39 taken 39 times.
✓ Branch 42 → 43 taken 39 times.
✓ Branch 43 → 44 taken 39 times.
✓ Branch 44 → 45 taken 39 times.
✓ Branch 48 → 40 taken 39 times.
✓ Branch 48 → 49 taken 39 times.
|
78 | qCInfo(lcImap) << "UIDVALIDITY:" << m_selectedUidValidity; |
| 1072 | } | ||
| 1073 | |||
| 1074 | // T-208: Parse HIGHESTMODSEQ from "* OK [HIGHESTMODSEQ 12345]" | ||
| 1075 | static QRegularExpression modseqRx( | ||
| 1076 |
5/8✓ Branch 50 → 51 taken 4 times.
✓ Branch 50 → 60 taken 62 times.
✓ Branch 52 → 53 taken 4 times.
✗ Branch 52 → 60 not taken.
✓ Branch 54 → 55 taken 4 times.
✓ Branch 55 → 56 taken 4 times.
✗ Branch 107 → 108 not taken.
✗ Branch 107 → 109 not taken.
|
66 | R"(\[HIGHESTMODSEQ\s+(\d+)\])", QRegularExpression::CaseInsensitiveOption); |
| 1077 |
1/1✓ Branch 61 → 62 taken 66 times.
|
66 | auto modseqMatch = modseqRx.match(data); |
| 1078 |
3/3✓ Branch 62 → 63 taken 66 times.
✓ Branch 63 → 64 taken 25 times.
✓ Branch 63 → 79 taken 41 times.
|
66 | if (modseqMatch.hasMatch()) { |
| 1079 |
2/2✓ Branch 64 → 65 taken 25 times.
✓ Branch 65 → 66 taken 25 times.
|
25 | m_selectedHighestModseq = modseqMatch.captured(1).toULongLong(); |
| 1080 |
6/6✓ Branch 67 → 68 taken 25 times.
✓ Branch 71 → 72 taken 25 times.
✓ Branch 72 → 73 taken 25 times.
✓ Branch 73 → 74 taken 25 times.
✓ Branch 77 → 69 taken 25 times.
✓ Branch 77 → 78 taken 25 times.
|
50 | qCInfo(lcImap) << "T-208: HIGHESTMODSEQ:" << m_selectedHighestModseq; |
| 1081 | } | ||
| 1082 | 66 | } | |
| 1083 | |||
| 1084 | 475 | void ImapService::handleTagged(const QString &line) { | |
| 1085 |
1/1✓ Branch 2 → 3 taken 475 times.
|
475 | auto response = ImapResponseParser::parseTaggedResponse(line); |
| 1086 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 475 times.
|
475 | if (!response) |
| 1087 | ✗ | return; | |
| 1088 | |||
| 1089 |
1/1✓ Branch 8 → 9 taken 475 times.
|
475 | ImapPendingCommand command = m_commands->takeRecord(response->tag); |
| 1090 | 475 | const QString commandType = command.type; | |
| 1091 | #ifdef MAILJD_E2E_TESTING | ||
| 1092 |
2/2✓ Branch 10 → 11 taken 405 times.
✓ Branch 16 → 17 taken 405 times.
|
810 | reportE2eTransportPhase( |
| 1093 | 810 | QStringLiteral("command-tagged:%1:%2") | |
| 1094 |
1/1✓ Branch 15 → 16 taken 405 times.
|
810 | .arg(commandType, response->status), |
| 1095 | e2eRequestTokenFor(command)); | ||
| 1096 | #endif | ||
| 1097 | struct SerializedDrainGuard { | ||
| 1098 | ImapCommandTable *commands = nullptr; | ||
| 1099 | 475 | ~SerializedDrainGuard() { | |
| 1100 |
1/2✓ Branch 2 → 3 taken 475 times.
✗ Branch 2 → 4 not taken.
|
475 | if (commands) |
| 1101 | 475 | commands->runNextSerializedCommand(); | |
| 1102 | 475 | } | |
| 1103 | 475 | } drainGuard{m_commands.get()}; | |
| 1104 | |||
| 1105 | // T-210: Log command duration | ||
| 1106 |
4/4✓ Branch 12 → 13 taken 69 times.
✓ Branch 12 → 31 taken 1 time.
✓ Branch 22 → 23 taken 307 times.
✓ Branch 22 → 41 taken 98 times.
|
475 | if (command.timer) { |
| 1107 |
11/11✓ Branch 13 → 14 taken 69 times.
✓ Branch 17 → 18 taken 69 times.
✓ Branch 18 → 19 taken 69 times.
✓ Branch 19 → 20 taken 69 times.
✓ Branch 23 → 24 taken 307 times.
✓ Branch 27 → 28 taken 307 times.
✓ Branch 28 → 29 taken 307 times.
✓ Branch 29 → 15 taken 69 times.
✓ Branch 29 → 30 taken 376 times.
✓ Branch 39 → 25 taken 307 times.
✓ Branch 39 → 40 taken 307 times.
|
752 | qCInfo(lcImapTiming) << "IMAP timing:" << commandType |
| 1108 |
4/4✓ Branch 20 → 21 taken 69 times.
✓ Branch 21 → 22 taken 69 times.
✓ Branch 30 → 31 taken 307 times.
✓ Branch 31 → 32 taken 307 times.
|
376 | << m_selectedFolder << "→" |
| 1109 |
4/4✓ Branch 24 → 25 taken 69 times.
✓ Branch 25 → 26 taken 69 times.
✓ Branch 34 → 35 taken 307 times.
✓ Branch 35 → 36 taken 307 times.
|
376 | << command.timer->elapsed() << "ms"; |
| 1110 | } | ||
| 1111 |
2/2✓ Branch 32 → 33 taken 70 times.
✓ Branch 42 → 43 taken 405 times.
|
475 | m_commands->refreshCommandTimeout(); |
| 1112 |
2/2✓ Branch 34 → 35 taken 70 times.
✓ Branch 44 → 45 taken 405 times.
|
475 | m_commands->refreshCommandDeadline(); |
| 1113 |
6/6✓ Branch 36 → 37 taken 70 times.
✓ Branch 37 → 38 taken 69 times.
✓ Branch 37 → 40 taken 1 time.
✓ Branch 46 → 47 taken 405 times.
✓ Branch 47 → 48 taken 385 times.
✓ Branch 47 → 50 taken 20 times.
|
475 | if (!m_commands->hasTimeoutTrackedCommandInFlight()) |
| 1114 |
2/2✓ Branch 39 → 40 taken 69 times.
✓ Branch 49 → 50 taken 385 times.
|
454 | m_commands->resetCommandAccumulators(); |
| 1115 | |||
| 1116 | // T-720: Clear the liveness probe state when its tagged response arrives | ||
| 1117 | // (any OK/NO from the server proves the socket is alive). BAD responses | ||
| 1118 | // are surfaced below by the regular errorOccurred handlers, which call | ||
| 1119 | // failConnection() and stop the watchdog there. | ||
| 1120 |
7/12✗ Branch 43 → 44 not taken.
✓ Branch 43 → 50 taken 70 times.
✗ Branch 48 → 49 not taken.
✗ Branch 48 → 50 not taken.
✗ Branch 51 → 52 not taken.
✓ Branch 51 → 78 taken 70 times.
✓ Branch 53 → 54 taken 5 times.
✓ Branch 53 → 60 taken 400 times.
✓ Branch 58 → 59 taken 5 times.
✗ Branch 58 → 60 not taken.
✓ Branch 61 → 62 taken 5 times.
✓ Branch 61 → 88 taken 400 times.
|
475 | if (!m_push->probeTag().isEmpty() && m_push->probeTag() == response->tag) { |
| 1121 |
6/12✗ Branch 52 → 53 not taken.
✗ Branch 56 → 57 not taken.
✗ Branch 57 → 58 not taken.
✗ Branch 60 → 61 not taken.
✓ Branch 62 → 63 taken 5 times.
✗ Branch 65 → 54 not taken.
✗ Branch 65 → 66 not taken.
✓ Branch 66 → 67 taken 5 times.
✓ Branch 67 → 68 taken 5 times.
✓ Branch 70 → 71 taken 5 times.
✓ Branch 75 → 64 taken 5 times.
✓ Branch 75 → 76 taken 5 times.
|
10 | qCInfo(lcImap) << "Liveness probe OK (" << m_push->probeTag() |
| 1122 |
1/2✗ Branch 61 → 62 not taken.
✓ Branch 71 → 72 taken 5 times.
|
5 | << ") — disarming watchdog"; |
| 1123 | 5 | m_push->probeTag().clear(); | |
| 1124 |
1/2✗ Branch 71 → 72 not taken.
✓ Branch 81 → 82 taken 5 times.
|
5 | m_push->livenessProbeWatchdog()->stop(); |
| 1125 |
1/4✗ Branch 73 → 74 not taken.
✗ Branch 73 → 78 not taken.
✓ Branch 83 → 84 taken 5 times.
✗ Branch 83 → 88 not taken.
|
5 | if (command.timer) |
| 1126 |
1/2✗ Branch 77 → 78 not taken.
✓ Branch 87 → 88 taken 5 times.
|
5 | m_push->noteRoundTrip(command.timer->elapsed()); |
| 1127 | } | ||
| 1128 | |||
| 1129 |
13/13✓ Branch 78 → 79 taken 70 times.
✓ Branch 82 → 83 taken 70 times.
✓ Branch 83 → 84 taken 70 times.
✓ Branch 84 → 85 taken 70 times.
✓ Branch 85 → 86 taken 70 times.
✓ Branch 88 → 89 taken 405 times.
✓ Branch 92 → 93 taken 405 times.
✓ Branch 93 → 80 taken 70 times.
✓ Branch 93 → 94 taken 475 times.
✓ Branch 94 → 95 taken 405 times.
✓ Branch 95 → 96 taken 405 times.
✓ Branch 103 → 90 taken 405 times.
✓ Branch 103 → 104 taken 405 times.
|
950 | qCDebug(lcImap) << "Tagged response for" << commandType << ":" |
| 1130 |
4/4✓ Branch 87 → 88 taken 70 times.
✓ Branch 89 → 90 taken 70 times.
✓ Branch 97 → 98 taken 405 times.
✓ Branch 99 → 100 taken 405 times.
|
475 | << response->status << response->message; |
| 1131 | |||
| 1132 |
2/2✓ Branch 95 → 96 taken 70 times.
✓ Branch 105 → 106 taken 405 times.
|
475 | dispatchTagged(command, *response); |
| 1133 | 475 | } | |
| 1134 | |||
| 1135 | // S92.1.8: tagged completion → handler of the unit that owns the command, in | ||
| 1136 | // the order of the former if-chain. Types without a handler (NOOP, LOGOUT) | ||
| 1137 | // only run the common completion steps of handleTagged. | ||
| 1138 | 475 | void ImapService::dispatchTagged(ImapPendingCommand &command, | |
| 1139 | const TaggedResponse &response) { | ||
| 1140 | using Command = ImapPendingCommand; | ||
| 1141 | using Handler = void (*)(ImapService &, const TaggedResponse &, Command &); | ||
| 1142 | static const std::pair<QLatin1StringView, Handler> handlers[] = { | ||
| 1143 | {QLatin1StringView("CAPABILITY"), | ||
| 1144 | 63 | [](ImapService &s, const TaggedResponse &r, Command &) { s.m_setup->handleTaggedCapability(r); }}, | |
| 1145 | {QLatin1StringView("STARTTLS"), | ||
| 1146 | 3 | [](ImapService &s, const TaggedResponse &r, Command &) { s.m_setup->handleTaggedStartTls(r); }}, | |
| 1147 | {QLatin1StringView("LOGIN"), | ||
| 1148 | 55 | [](ImapService &s, const TaggedResponse &r, Command &) { s.m_setup->handleTaggedLogin(r); }}, | |
| 1149 | {QLatin1StringView("LIST"), | ||
| 1150 | 20 | [](ImapService &s, const TaggedResponse &r, Command &) { s.handleTaggedList(r); }}, | |
| 1151 | {QLatin1StringView("SELECT"), | ||
| 1152 | 48 | [](ImapService &s, const TaggedResponse &r, Command &c) { s.handleTaggedSelect(r, c); }}, | |
| 1153 | {QLatin1StringView("FETCH_HEADERS"), | ||
| 1154 | 5 | [](ImapService &s, const TaggedResponse &r, Command &) { s.handleTaggedFetchHeaders(r); }}, | |
| 1155 | {QLatin1StringView("FETCH_BODY"), | ||
| 1156 | 42 | [](ImapService &s, const TaggedResponse &r, Command &) { s.m_body->handleTaggedFetchBody(r); }}, | |
| 1157 | {QLatin1StringView("FETCH_FLAGS"), | ||
| 1158 | 9 | [](ImapService &s, const TaggedResponse &r, Command &) { s.handleTaggedFetchFlags(r); }}, | |
| 1159 | {QLatin1StringView("FETCH_VERIFY_FLAGS"), | ||
| 1160 | 5 | [](ImapService &s, const TaggedResponse &r, Command &) { s.handleTaggedVerifyFlags(r); }}, | |
| 1161 | {QLatin1StringView("FETCH_FLAGS_CHANGED"), | ||
| 1162 | 3 | [](ImapService &s, const TaggedResponse &r, Command &) { s.handleTaggedFetchFlagsChanged(r); }}, | |
| 1163 | {QLatin1StringView("SEARCH_MEMBERSHIP"), | ||
| 1164 | 2 | [](ImapService &s, const TaggedResponse &r, Command &) { s.handleTaggedSearchMembership(r); }}, | |
| 1165 | {QLatin1StringView("SEARCH"), | ||
| 1166 | 48 | [](ImapService &s, const TaggedResponse &r, Command &) { s.handleTaggedSearch(r); }}, | |
| 1167 | {QLatin1StringView("FETCH_SEQNO"), | ||
| 1168 | 4 | [](ImapService &s, const TaggedResponse &r, Command &) { s.handleTaggedFetchSeqno(r); }}, | |
| 1169 | {QLatin1StringView("IDLE"), | ||
| 1170 | 10 | [](ImapService &s, const TaggedResponse &r, Command &) { s.m_push->handleTaggedIdle(r); }}, | |
| 1171 | {QLatin1StringView("NOTIFY"), // T-320: NOTIFY SET | ||
| 1172 | 6 | [](ImapService &s, const TaggedResponse &r, Command &) { s.m_push->handleTaggedNotify(r); }}, | |
| 1173 | {QLatin1StringView("NOTIFY_NONE"), | ||
| 1174 | 7 | [](ImapService &s, const TaggedResponse &r, Command &) { s.m_push->handleTaggedNotifyNone(r); }}, | |
| 1175 | {QLatin1StringView("STATUS"), | ||
| 1176 | 13 | [](ImapService &s, const TaggedResponse &r, Command &c) { s.handleTaggedStatus(r, c); }}, | |
| 1177 | {QLatin1StringView("STORE"), | ||
| 1178 | 15 | [](ImapService &s, const TaggedResponse &r, Command &c) { s.m_mutations->handleTaggedStore(r, c); }}, | |
| 1179 | {QLatin1StringView("MOVE"), // T-100: RFC 6851 | ||
| 1180 | 16 | [](ImapService &s, const TaggedResponse &r, Command &c) { s.m_mutations->handleTaggedMove(r, c); }}, | |
| 1181 | {QLatin1StringView("COPY"), // T-100: MOVE fallback COPY + DELETE + EXPUNGE | ||
| 1182 | 8 | [](ImapService &s, const TaggedResponse &r, Command &c) { s.m_mutations->handleTaggedCopy(r, c); }}, | |
| 1183 | {QLatin1StringView("STORE_DELETE"), | ||
| 1184 | 8 | [](ImapService &s, const TaggedResponse &r, Command &c) { s.m_mutations->handleTaggedStoreDelete(r, c); }}, | |
| 1185 | {QLatin1StringView("EXPUNGE_MOVE"), | ||
| 1186 | 9 | [](ImapService &s, const TaggedResponse &r, Command &c) { s.m_mutations->handleTaggedExpungeMove(r, c); }}, | |
| 1187 | {QLatin1StringView("COPY_ONLY"), // T-100: standalone COPY | ||
| 1188 | 6 | [](ImapService &s, const TaggedResponse &r, Command &c) { s.m_mutations->handleTaggedCopyOnly(r, c); }}, | |
| 1189 | {QLatin1StringView("EXPUNGE"), // T-176: standalone EXPUNGE | ||
| 1190 | 8 | [](ImapService &s, const TaggedResponse &r, Command &) { s.m_mutations->handleTaggedExpunge(r); }}, | |
| 1191 | {QLatin1StringView("APPEND"), | ||
| 1192 | 34 | [](ImapService &s, const TaggedResponse &r, Command &c) { s.m_append->handleTaggedAppend(r, c); }}, | |
| 1193 | {QLatin1StringView("CREATE"), | ||
| 1194 | 6 | [](ImapService &s, const TaggedResponse &r, Command &c) { s.handleTaggedCreate(r, c); }}, | |
| 1195 | {QLatin1StringView("DELETE"), | ||
| 1196 | 5 | [](ImapService &s, const TaggedResponse &r, Command &c) { s.handleTaggedDelete(r, c); }}, | |
| 1197 | {QLatin1StringView("RENAME"), | ||
| 1198 | 4 | [](ImapService &s, const TaggedResponse &r, Command &c) { s.handleTaggedRename(r, c); }}, | |
| 1199 | }; | ||
| 1200 |
2/2✓ Branch 10 → 3 taken 5285 times.
✓ Branch 10 → 11 taken 13 times.
|
5298 | for (const auto &[type, handler] : handlers) { |
| 1201 |
2/2✓ Branch 6 → 7 taken 462 times.
✓ Branch 6 → 9 taken 4823 times.
|
5285 | if (command.type == type) { |
| 1202 | 462 | handler(*this, response, command); | |
| 1203 | 462 | return; | |
| 1204 | } | ||
| 1205 | } | ||
| 1206 | } | ||
| 1207 | |||
| 1208 | 20 | void ImapService::handleTaggedList(const TaggedResponse &response) { | |
| 1209 |
2/2✓ Branch 3 → 4 taken 19 times.
✓ Branch 3 → 19 taken 1 time.
|
20 | if (response.status == "OK") { |
| 1210 |
6/6✓ Branch 4 → 5 taken 19 times.
✓ Branch 8 → 9 taken 19 times.
✓ Branch 9 → 10 taken 19 times.
✓ Branch 11 → 12 taken 19 times.
✓ Branch 16 → 6 taken 19 times.
✓ Branch 16 → 17 taken 19 times.
|
38 | qCInfo(lcImap) << "LIST complete:" << m_pendingFolders.size() |
| 1211 |
1/1✓ Branch 12 → 13 taken 19 times.
|
19 | << "folders"; |
| 1212 | 19 | emit folderListReceived(m_pendingFolders); | |
| 1213 | 19 | m_pendingFolders.clear(); | |
| 1214 | } else { | ||
| 1215 |
2/2✓ Branch 19 → 20 taken 1 time.
✓ Branch 20 → 21 taken 1 time.
|
1 | emit errorOccurred("LIST failed: " + response.message); |
| 1216 | } | ||
| 1217 | 20 | } | |
| 1218 | |||
| 1219 | 48 | void ImapService::handleTaggedSelect(const TaggedResponse &response, | |
| 1220 | ImapPendingCommand &command) { | ||
| 1221 | const quint64 bodyRequestId = | ||
| 1222 |
2/2✓ Branch 4 → 5 taken 5 times.
✓ Branch 4 → 8 taken 43 times.
|
48 | m_body->bodyFetchRequiresSelect() ? m_body->activeBodyRequestId() : 0; |
| 1223 |
2/2✓ Branch 10 → 11 taken 44 times.
✓ Branch 10 → 33 taken 4 times.
|
48 | if (response.status == "OK") { |
| 1224 | // A pipelined body response is valid only after this SELECT succeeds. | ||
| 1225 | 44 | m_body->bodyFetchRequiresSelect() = false; | |
| 1226 | // Bug 34: Only assign m_selectedFolder on SELECT OK | ||
| 1227 | 44 | m_selectedFolder = command.selectFolder; | |
| 1228 |
6/6✓ Branch 14 → 15 taken 44 times.
✓ Branch 18 → 19 taken 44 times.
✓ Branch 19 → 20 taken 44 times.
✓ Branch 20 → 21 taken 44 times.
✓ Branch 28 → 16 taken 44 times.
✓ Branch 28 → 29 taken 44 times.
|
88 | qCInfo(lcImap) << "SELECT complete:" << m_selectedFolder |
| 1229 |
2/2✓ Branch 21 → 22 taken 44 times.
✓ Branch 22 → 23 taken 44 times.
|
44 | << "msgs=" << m_selectedMessageCount |
| 1230 |
2/2✓ Branch 23 → 24 taken 44 times.
✓ Branch 24 → 25 taken 44 times.
|
44 | << "uidvalidity=" << m_selectedUidValidity; |
| 1231 | 44 | setState(State::Selected); | |
| 1232 | 44 | emit folderSelected(m_selectedFolder, m_selectedMessageCount, | |
| 1233 | m_selectedUidValidity, m_selectedHighestModseq); | ||
| 1234 |
2/2✓ Branch 31 → 32 taken 2 times.
✓ Branch 31 → 53 taken 42 times.
|
44 | if (bodyRequestId > 0) |
| 1235 | 2 | emit bodyMailboxSelected(bodyRequestId, true); | |
| 1236 | } else { | ||
| 1237 |
2/2✓ Branch 33 → 34 taken 4 times.
✓ Branch 34 → 35 taken 4 times.
|
4 | emit errorOccurred("SELECT failed: " + response.message); |
| 1238 |
2/2✓ Branch 38 → 39 taken 2 times.
✓ Branch 38 → 52 taken 2 times.
|
4 | if (m_body->bodyFetchRequiresSelect()) { |
| 1239 |
2/2✓ Branch 39 → 40 taken 1 time.
✓ Branch 39 → 41 taken 1 time.
|
2 | if (bodyRequestId > 0) |
| 1240 | 1 | emit bodyMailboxSelected(bodyRequestId, false); | |
| 1241 |
1/1✓ Branch 46 → 47 taken 2 times.
|
4 | m_body->completeBodyRequest(BodyOutcome::Failed, {}, |
| 1242 | 4 | QStringLiteral("Mailbox selection failed")); | |
| 1243 | 2 | m_body->invalidateBodyFetch(); | |
| 1244 | } | ||
| 1245 | // Notify multi-folder flows so they can skip this folder instead of | ||
| 1246 | // stalling (real servers reject \Noselect container folders here). | ||
| 1247 | 4 | emit folderSelectFailed(command.selectFolder); | |
| 1248 | } | ||
| 1249 | // T-401/Bug 10: Only drain deferred commands on SELECT success | ||
| 1250 |
6/6✓ Branch 54 → 55 taken 44 times.
✓ Branch 54 → 60 taken 4 times.
✓ Branch 58 → 59 taken 1 time.
✓ Branch 58 → 60 taken 43 times.
✓ Branch 61 → 62 taken 1 time.
✓ Branch 61 → 64 taken 47 times.
|
48 | if (response.status == "OK" && !m_commands->deferredCommands().isEmpty()) { |
| 1251 | 1 | m_commands->runNextDeferredCommand(); | |
| 1252 |
2/2✓ Branch 65 → 66 taken 4 times.
✓ Branch 65 → 88 taken 43 times.
|
47 | } else if (response.status != "OK") { |
| 1253 | // Discard deferred commands — they'd operate on wrong folder | ||
| 1254 |
2/2✓ Branch 69 → 70 taken 1 time.
✓ Branch 69 → 88 taken 3 times.
|
4 | if (!m_commands->deferredCommands().isEmpty()) { |
| 1255 |
5/5✓ Branch 70 → 71 taken 1 time.
✓ Branch 74 → 75 taken 1 time.
✓ Branch 75 → 76 taken 1 time.
✓ Branch 84 → 72 taken 1 time.
✓ Branch 84 → 85 taken 1 time.
|
2 | qCWarning(lcImap) << "SELECT failed — discarding" |
| 1256 |
1/1✓ Branch 79 → 80 taken 1 time.
|
1 | << m_commands->deferredCommands().size() |
| 1257 |
1/1✓ Branch 80 → 81 taken 1 time.
|
1 | << "deferred commands"; |
| 1258 | 1 | m_commands->deferredCommands().clear(); | |
| 1259 | } | ||
| 1260 | } | ||
| 1261 | 48 | } | |
| 1262 | |||
| 1263 | 5 | void ImapService::handleTaggedFetchHeaders(const TaggedResponse &response) { | |
| 1264 |
1/2✓ Branch 3 → 4 taken 5 times.
✗ Branch 3 → 22 not taken.
|
5 | if (response.status == "OK") { |
| 1265 |
6/6✓ Branch 4 → 5 taken 5 times.
✓ Branch 8 → 9 taken 5 times.
✓ Branch 9 → 10 taken 5 times.
✓ Branch 11 → 12 taken 5 times.
✓ Branch 16 → 6 taken 5 times.
✓ Branch 16 → 17 taken 5 times.
|
10 | qCInfo(lcImap) << "FETCH headers complete:" << m_pendingHeaders.size() |
| 1266 |
1/1✓ Branch 12 → 13 taken 5 times.
|
5 | << "remaining"; |
| 1267 | // Emit any remaining buffered headers | ||
| 1268 |
2/2✓ Branch 18 → 19 taken 2 times.
✓ Branch 18 → 21 taken 3 times.
|
5 | if (!m_pendingHeaders.isEmpty()) { |
| 1269 | 2 | emit headersReceived(m_pendingHeaders); | |
| 1270 | 2 | m_pendingHeaders.clear(); | |
| 1271 | } | ||
| 1272 | // Signal that all header batches have been delivered | ||
| 1273 | 5 | emit headerFetchComplete(); | |
| 1274 | } else { | ||
| 1275 | ✗ | emit errorOccurred("FETCH headers failed: " + response.message); | |
| 1276 | } | ||
| 1277 | 5 | } | |
| 1278 | |||
| 1279 | 9 | void ImapService::handleTaggedFetchFlags(const TaggedResponse &response) { | |
| 1280 |
1/2✓ Branch 3 → 4 taken 9 times.
✗ Branch 3 → 21 not taken.
|
9 | if (response.status == "OK") { |
| 1281 |
6/6✓ Branch 4 → 5 taken 9 times.
✓ Branch 8 → 9 taken 9 times.
✓ Branch 9 → 10 taken 9 times.
✓ Branch 11 → 12 taken 9 times.
✓ Branch 16 → 6 taken 9 times.
✓ Branch 16 → 17 taken 9 times.
|
18 | qCInfo(lcImap) << "FETCH flags complete:" << m_pendingFlags.size() |
| 1282 |
1/1✓ Branch 12 → 13 taken 9 times.
|
9 | << "entries"; |
| 1283 | 9 | emit flagSnapshotsReceived(m_pendingFlagSnapshots, m_selectedFolder); | |
| 1284 | 9 | emit flagsReceived(m_pendingFlags); | |
| 1285 | 9 | m_pendingFlags.clear(); | |
| 1286 | 9 | m_pendingFlagSnapshots.clear(); | |
| 1287 | } else { | ||
| 1288 | ✗ | emit errorOccurred("FETCH flags failed: " + response.message); | |
| 1289 | ✗ | m_pendingFlags.clear(); | |
| 1290 | ✗ | m_pendingFlagSnapshots.clear(); | |
| 1291 | } | ||
| 1292 | 9 | } | |
| 1293 | |||
| 1294 | 3 | void ImapService::handleTaggedFetchFlagsChanged(const TaggedResponse &response) { | |
| 1295 | // A partial list: never emitted as a flag snapshot (that would claim | ||
| 1296 | // every unlisted UID is absent). | ||
| 1297 |
1/2✓ Branch 3 → 4 taken 3 times.
✗ Branch 3 → 18 not taken.
|
3 | if (response.status == "OK") { |
| 1298 |
7/7✓ Branch 4 → 5 taken 3 times.
✓ Branch 8 → 9 taken 3 times.
✓ Branch 9 → 10 taken 3 times.
✓ Branch 11 → 12 taken 3 times.
✓ Branch 12 → 13 taken 3 times.
✓ Branch 16 → 6 taken 3 times.
✓ Branch 16 → 17 taken 3 times.
|
6 | qCInfo(lcImap) << "CHANGEDSINCE complete:" << m_pendingFlags.size() << "changed"; |
| 1299 | 3 | emit changedFlagsReceived(m_pendingFlags); | |
| 1300 | } else { | ||
| 1301 | // F-72: a declined CHANGEDSINCE (e.g. a stale watermark pipelined | ||
| 1302 | // before SELECT revealed a UIDVALIDITY change) is a folder-sync | ||
| 1303 | // event, not a connection error — it must not arm the health | ||
| 1304 | // monitor's reconnect ladder for a healthy Selected connection. | ||
| 1305 | // Emit the empty result so the pipeline advances; the membership | ||
| 1306 | // query and backfill complete the flag pass. | ||
| 1307 | ✗ | qCWarning(lcImap) << "CHANGEDSINCE declined, falling back to full sync:" | |
| 1308 | ✗ | << response.message; | |
| 1309 | ✗ | emit changedFlagsReceived({}); | |
| 1310 | } | ||
| 1311 | 3 | m_pendingFlags.clear(); | |
| 1312 | 3 | m_pendingFlagSnapshots.clear(); | |
| 1313 | 3 | } | |
| 1314 | |||
| 1315 | 2 | void ImapService::handleTaggedSearchMembership(const TaggedResponse &response) { | |
| 1316 |
1/2✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 18 not taken.
|
2 | if (response.status == "OK") { |
| 1317 |
7/7✓ Branch 4 → 5 taken 2 times.
✓ Branch 8 → 9 taken 2 times.
✓ Branch 9 → 10 taken 2 times.
✓ Branch 11 → 12 taken 2 times.
✓ Branch 12 → 13 taken 2 times.
✓ Branch 16 → 6 taken 2 times.
✓ Branch 16 → 17 taken 2 times.
|
4 | qCInfo(lcImap) << "UID membership complete:" << m_pendingSearchUids.size() << "UIDs"; |
| 1318 | 2 | emit uidMembershipReceived(m_pendingSearchUids); | |
| 1319 | } else { | ||
| 1320 | // F-72: same family as CHANGEDSINCE above — a declined membership | ||
| 1321 | // SEARCH reports an empty membership, which reconciles every cached | ||
| 1322 | // UID as missing and triggers the full backfill (with flags) instead | ||
| 1323 | // of a spurious reconnect of the healthy connection. | ||
| 1324 | ✗ | qCWarning(lcImap) << "UID SEARCH declined, reconciling via backfill:" | |
| 1325 | ✗ | << response.message; | |
| 1326 | ✗ | emit uidMembershipReceived({}); | |
| 1327 | } | ||
| 1328 | 2 | m_pendingSearchUids.clear(); | |
| 1329 | 2 | } | |
| 1330 | |||
| 1331 | // T-065 fix: Single-UID flag fetch (from IDLE seqNo resolution). | ||
| 1332 | // Emit each result directly as idleFlagsChanged → targeted update, no purge. | ||
| 1333 | 4 | void ImapService::handleTaggedFetchSeqno(const TaggedResponse &response) { | |
| 1334 |
2/2✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 29 taken 1 time.
|
4 | if (response.status == "OK") { |
| 1335 |
6/6✓ Branch 4 → 5 taken 3 times.
✓ Branch 8 → 9 taken 3 times.
✓ Branch 9 → 10 taken 3 times.
✓ Branch 11 → 12 taken 3 times.
✓ Branch 16 → 6 taken 3 times.
✓ Branch 16 → 17 taken 3 times.
|
6 | qCInfo(lcImap) << "FETCH_SEQNO complete:" << m_pendingFlags.size() |
| 1336 |
1/1✓ Branch 12 → 13 taken 3 times.
|
3 | << "entries"; |
| 1337 |
4/4✓ Branch 17 → 18 taken 3 times.
✓ Branch 18 → 19 taken 3 times.
✓ Branch 26 → 20 taken 3 times.
✓ Branch 26 → 27 taken 3 times.
|
6 | for (const auto &[uid, flags] : m_pendingFlags) { |
| 1338 |
1/1✓ Branch 23 → 24 taken 3 times.
|
3 | emit idleFlagsChanged(uid, flags); |
| 1339 | } | ||
| 1340 | 3 | m_pendingFlags.clear(); | |
| 1341 | 3 | m_pendingFlagSnapshots.clear(); | |
| 1342 | } else { | ||
| 1343 |
2/2✓ Branch 29 → 30 taken 1 time.
✓ Branch 30 → 31 taken 1 time.
|
1 | emit errorOccurred("FETCH seqNo failed: " + response.message); |
| 1344 | 1 | m_pendingFlags.clear(); | |
| 1345 | 1 | m_pendingFlagSnapshots.clear(); | |
| 1346 | } | ||
| 1347 | // Drain remaining deferred commands (e.g. more flag refetches from | ||
| 1348 | // multiple IDLE pushes) before restarting IDLE. | ||
| 1349 |
2/2✓ Branch 37 → 38 taken 1 time.
✓ Branch 37 → 40 taken 3 times.
|
4 | if (!m_commands->deferredCommands().isEmpty()) { |
| 1350 | 1 | m_commands->runNextDeferredCommand(); | |
| 1351 | } else { | ||
| 1352 | 3 | m_push->restartPush(); | |
| 1353 | } | ||
| 1354 | 4 | } | |
| 1355 | |||
| 1356 | 13 | void ImapService::handleTaggedStatus(const TaggedResponse &response, | |
| 1357 | ImapPendingCommand &command) { | ||
| 1358 | 13 | auto snapshot = std::exchange(command.status, std::nullopt); | |
| 1359 | 13 | const auto startedNs = std::exchange(command.statusStartedNs, 0); | |
| 1360 | 13 | const auto folder = std::exchange(command.statusFolder, {}); | |
| 1361 |
2/2✓ Branch 8 → 9 taken 3 times.
✓ Branch 8 → 23 taken 10 times.
|
13 | if (response.status != "OK") { |
| 1362 | // Non-fatal: folder may not exist (e.g. dovecot/sieve) – log, don't alarm. | ||
| 1363 | // F-26 (SPRINT-104): report the folder so the sequential polling round | ||
| 1364 | // continues with the next folder instead of stopping at this NO. | ||
| 1365 |
6/6✓ Branch 9 → 10 taken 3 times.
✓ Branch 13 → 14 taken 3 times.
✓ Branch 14 → 15 taken 3 times.
✓ Branch 15 → 16 taken 3 times.
✓ Branch 21 → 11 taken 3 times.
✓ Branch 21 → 22 taken 3 times.
|
6 | qCWarning(lcImap) << "STATUS failed for" << folder |
| 1366 |
2/2✓ Branch 16 → 17 taken 3 times.
✓ Branch 17 → 18 taken 3 times.
|
3 | << ":" << response.message; |
| 1367 |
1/1✓ Branch 22 → 28 taken 3 times.
|
3 | emit folderStatusFailed(folder); |
| 1368 |
2/2✓ Branch 24 → 25 taken 8 times.
✓ Branch 24 → 28 taken 2 times.
|
10 | } else if (snapshot) { |
| 1369 | 8 | snapshot->requestStartedNs = startedNs; | |
| 1370 |
1/1✓ Branch 27 → 28 taken 8 times.
|
8 | emit folderStatusReceived(*snapshot); |
| 1371 | } | ||
| 1372 | 13 | } | |
| 1373 | |||
| 1374 | // T-281: CREATE folder | ||
| 1375 | 6 | void ImapService::handleTaggedCreate(const TaggedResponse &response, | |
| 1376 | ImapPendingCommand &command) { | ||
| 1377 |
2/2✓ Branch 3 → 4 taken 5 times.
✓ Branch 3 → 16 taken 1 time.
|
6 | if (response.status == "OK") { |
| 1378 |
6/6✓ Branch 4 → 5 taken 5 times.
✓ Branch 8 → 9 taken 5 times.
✓ Branch 9 → 10 taken 5 times.
✓ Branch 10 → 11 taken 5 times.
✓ Branch 14 → 6 taken 5 times.
✓ Branch 14 → 15 taken 5 times.
|
10 | qCInfo(lcImap) << "CREATE complete:" << command.folderPath; |
| 1379 | 5 | emit folderCreated(command.folderPath); | |
| 1380 | } else { | ||
| 1381 |
6/6✓ Branch 16 → 17 taken 1 time.
✓ Branch 20 → 21 taken 1 time.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 22 → 23 taken 1 time.
✓ Branch 26 → 18 taken 1 time.
✓ Branch 26 → 27 taken 1 time.
|
2 | qCWarning(lcImap) << "CREATE failed:" << response.message; |
| 1382 |
1/1✓ Branch 30 → 31 taken 1 time.
|
2 | emit folderOperationError(QStringLiteral("CREATE"), response.message); |
| 1383 | } | ||
| 1384 | 6 | command.folderPath.clear(); | |
| 1385 |
2/2✓ Branch 38 → 39 taken 2 times.
✓ Branch 38 → 41 taken 4 times.
|
6 | if (!m_commands->deferredCommands().isEmpty()) { |
| 1386 | 2 | m_commands->runNextDeferredCommand(); | |
| 1387 | } else { | ||
| 1388 | 4 | m_push->restartPush(); | |
| 1389 | } | ||
| 1390 | 6 | } | |
| 1391 | |||
| 1392 | // T-281: DELETE folder | ||
| 1393 | 5 | void ImapService::handleTaggedDelete(const TaggedResponse &response, | |
| 1394 | ImapPendingCommand &command) { | ||
| 1395 |
2/2✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 16 taken 2 times.
|
5 | if (response.status == "OK") { |
| 1396 |
6/6✓ Branch 4 → 5 taken 3 times.
✓ Branch 8 → 9 taken 3 times.
✓ Branch 9 → 10 taken 3 times.
✓ Branch 10 → 11 taken 3 times.
✓ Branch 14 → 6 taken 3 times.
✓ Branch 14 → 15 taken 3 times.
|
6 | qCInfo(lcImap) << "DELETE complete:" << command.folderPath; |
| 1397 | 3 | emit folderDeleted(command.folderPath); | |
| 1398 | } else { | ||
| 1399 |
6/6✓ Branch 16 → 17 taken 2 times.
✓ Branch 20 → 21 taken 2 times.
✓ Branch 21 → 22 taken 2 times.
✓ Branch 22 → 23 taken 2 times.
✓ Branch 26 → 18 taken 2 times.
✓ Branch 26 → 27 taken 2 times.
|
4 | qCWarning(lcImap) << "DELETE failed:" << response.message; |
| 1400 |
1/1✓ Branch 30 → 31 taken 2 times.
|
4 | emit folderOperationError(QStringLiteral("DELETE"), response.message); |
| 1401 | } | ||
| 1402 | 5 | command.folderPath.clear(); | |
| 1403 |
2/2✓ Branch 38 → 39 taken 2 times.
✓ Branch 38 → 41 taken 3 times.
|
5 | if (!m_commands->deferredCommands().isEmpty()) { |
| 1404 | 2 | m_commands->runNextDeferredCommand(); | |
| 1405 | } else { | ||
| 1406 | 3 | m_push->restartPush(); | |
| 1407 | } | ||
| 1408 | 5 | } | |
| 1409 | |||
| 1410 | // T-281: RENAME folder | ||
| 1411 | 4 | void ImapService::handleTaggedRename(const TaggedResponse &response, | |
| 1412 | ImapPendingCommand &command) { | ||
| 1413 |
2/2✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 18 taken 1 time.
|
4 | if (response.status == "OK") { |
| 1414 |
6/6✓ Branch 4 → 5 taken 3 times.
✓ Branch 8 → 9 taken 3 times.
✓ Branch 9 → 10 taken 3 times.
✓ Branch 10 → 11 taken 3 times.
✓ Branch 16 → 6 taken 3 times.
✓ Branch 16 → 17 taken 3 times.
|
6 | qCInfo(lcImap) << "RENAME complete:" << command.folderPath |
| 1415 |
2/2✓ Branch 11 → 12 taken 3 times.
✓ Branch 12 → 13 taken 3 times.
|
3 | << "->" << command.folderNewPath; |
| 1416 | 3 | emit folderRenamed(command.folderPath, command.folderNewPath); | |
| 1417 | } else { | ||
| 1418 |
6/6✓ Branch 18 → 19 taken 1 time.
✓ Branch 22 → 23 taken 1 time.
✓ Branch 23 → 24 taken 1 time.
✓ Branch 24 → 25 taken 1 time.
✓ Branch 28 → 20 taken 1 time.
✓ Branch 28 → 29 taken 1 time.
|
2 | qCWarning(lcImap) << "RENAME failed:" << response.message; |
| 1419 |
1/1✓ Branch 32 → 33 taken 1 time.
|
2 | emit folderOperationError(QStringLiteral("RENAME"), response.message); |
| 1420 | } | ||
| 1421 | 4 | command.folderPath.clear(); | |
| 1422 | 4 | command.folderNewPath.clear(); | |
| 1423 |
2/2✓ Branch 41 → 42 taken 2 times.
✓ Branch 41 → 44 taken 2 times.
|
4 | if (!m_commands->deferredCommands().isEmpty()) { |
| 1424 | 2 | m_commands->runNextDeferredCommand(); | |
| 1425 | } else { | ||
| 1426 | 2 | m_push->restartPush(); | |
| 1427 | } | ||
| 1428 | 4 | } | |
| 1429 | |||
| 1430 | |||
| 1431 | 7 | void ImapService::onSocketError(QAbstractSocket::SocketError error) { | |
| 1432 | Q_UNUSED(error) | ||
| 1433 | // An explicit teardown has no live request to fail. A queued socket error | ||
| 1434 | // from the cancelled attempt must not turn an offline session into Error. | ||
| 1435 |
2/2✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 6 times.
|
7 | if (m_state == State::Disconnected) |
| 1436 | 1 | return; | |
| 1437 |
1/1✓ Branch 4 → 5 taken 6 times.
|
6 | auto errorMsg = m_socket->errorString(); |
| 1438 |
6/6✓ Branch 5 → 6 taken 6 times.
✓ Branch 9 → 10 taken 6 times.
✓ Branch 10 → 11 taken 6 times.
✓ Branch 11 → 12 taken 6 times.
✓ Branch 15 → 7 taken 6 times.
✓ Branch 15 → 16 taken 6 times.
|
12 | qCWarning(lcImap) << "Socket error:" << errorMsg; |
| 1439 |
1/1✓ Branch 16 → 17 taken 6 times.
|
6 | failConnection(errorMsg, DisconnectReason::SocketError); |
| 1440 | 6 | } | |
| 1441 | |||
| 1442 | 1 | void ImapService::onTimeout() { | |
| 1443 |
1/1✓ Branch 5 → 6 taken 1 time.
|
1 | failConnection(QStringLiteral("Connection timeout"), |
| 1444 | DisconnectReason::CommandTimeout); | ||
| 1445 | 1 | } | |
| 1446 |