aboutsummaryrefslogtreecommitdiffstats
path: root/src/mailsync.cpp
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-03 09:01:32 +0200
committerDanilo M. <danix@danix.xyz>2026-08-03 09:01:32 +0200
commit376323043a6b7a02e23cd9b6232bc25ec5fc1ce6 (patch)
tree4140c48b8c1b493c2998bd094dca76613f0d2917 /src/mailsync.cpp
parentc6c1011222361b446fb3cdee1ad324fe54931abc (diff)
downloadqtmaildir-376323043a6b7a02e23cd9b6232bc25ec5fc1ce6.tar.gz
qtmaildir-376323043a6b7a02e23cd9b6232bc25ec5fc1ce6.zip
feat: add MailSync process wrapper
Runs the configured sync script through QProcess, merging stdout and stderr into one log so a failing mbsync run has something to show. The script is never run through a shell: the command is a config value, and splitCommand keeps its arguments literal. Two fixes against the drafted version: - start() no longer calls waitForStarted(). It blocked the UI thread for up to five seconds, which contradicts the spec's requirement that the UI stay usable during sync, and it swallowed launch failures into a bare false return. A missing script now surfaces asynchronously through errorOccurred as finished(false, -1) with an explanatory log line, so the spinner cannot hang with nothing to explain it. - Removed a double-emit guard I had added on the assumption that QProcess follows errorOccurred(FailedToStart) with finished(). Verified it does not: FailedToStart is emitted instead of finished, never before it. The guard was dead state and the comment justifying it was wrong. Also corrects the sync interval throughout: the user's cron runs every 10 minutes, not hourly. The shorter interval strengthens the flock rationale rather than weakening it, since collisions with a manual sync are that much more likely. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'src/mailsync.cpp')
-rw-r--r--src/mailsync.cpp83
1 files changed, 83 insertions, 0 deletions
diff --git a/src/mailsync.cpp b/src/mailsync.cpp
new file mode 100644
index 0000000..fdc433f
--- /dev/null
+++ b/src/mailsync.cpp
@@ -0,0 +1,83 @@
+#include "mailsync.h"
+
+MailSync::MailSync(const QString &command, QObject *parent)
+ : QObject(parent), m_command(command)
+{
+ // mbsync reports failures on stderr, so both channels go into one log:
+ // splitting them would leave the pane empty for the runs worth reading.
+ m_process.setProcessChannelMode(QProcess::MergedChannels);
+
+ connect(&m_process, &QProcess::readyRead,
+ this, &MailSync::handleReadyRead);
+ connect(&m_process, &QProcess::finished,
+ this, &MailSync::handleFinished);
+ connect(&m_process, &QProcess::errorOccurred,
+ this, &MailSync::handleError);
+}
+
+bool MailSync::isRunning() const
+{
+ return m_process.state() != QProcess::NotRunning;
+}
+
+bool MailSync::start()
+{
+ if (!isAvailable() || isRunning())
+ return false;
+
+ // splitCommand handles quoted arguments; running through a shell would make
+ // a config value into an injection point.
+ const QStringList parts = QProcess::splitCommand(m_command);
+ if (parts.isEmpty())
+ return false;
+
+ m_log.clear();
+
+ m_process.setProgram(parts.first());
+ m_process.setArguments(parts.mid(1));
+
+ // Deliberately no waitForStarted(): the spec requires the UI stay usable
+ // during sync, and a failed launch arrives via errorOccurred() instead.
+ m_process.start();
+
+ emit started();
+ return true;
+}
+
+void MailSync::handleReadyRead()
+{
+ const QByteArray data = m_process.readAll();
+ if (data.isEmpty())
+ return;
+
+ const QString chunk = QString::fromUtf8(data);
+ m_log += chunk;
+ emit outputReceived(chunk);
+}
+
+void MailSync::handleFinished(int exitCode, QProcess::ExitStatus status)
+{
+ // Drain anything buffered at exit.
+ handleReadyRead();
+
+ // No guard against a preceding launch failure is needed: verified that
+ // QProcess emits errorOccurred(FailedToStart) *instead of* finished(),
+ // not before it.
+ const bool success = status == QProcess::NormalExit && exitCode == 0;
+ emit finished(success, exitCode);
+}
+
+void MailSync::handleError(QProcess::ProcessError error)
+{
+ // Config validates the path at load time, but the script can be deleted or
+ // its filesystem unmounted afterwards. Without this the spinner would stay
+ // up forever with nothing explaining why.
+ if (error != QProcess::FailedToStart)
+ return;
+
+ const QString message =
+ QStringLiteral("Failed to start sync command: %1\n").arg(m_command);
+ m_log += message;
+ emit outputReceived(message);
+ emit finished(false, -1);
+}