aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/CMakeLists.txt1
-rw-r--r--src/launchselectors.cpp182
-rw-r--r--src/launchselectors.h89
3 files changed, 272 insertions, 0 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 0940e4f..ca26933 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -53,6 +53,7 @@ add_library(qtmaildir_lib STATIC
agendaview.cpp
eventpane.cpp
calendarwindow.cpp
+ launchselectors.cpp
)
target_include_directories(qtmaildir_lib
diff --git a/src/launchselectors.cpp b/src/launchselectors.cpp
new file mode 100644
index 0000000..b5addde
--- /dev/null
+++ b/src/launchselectors.cpp
@@ -0,0 +1,182 @@
+/*
+ * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs
+ * Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "launchselectors.h"
+
+#include <QCommandLineOption>
+#include <QCommandLineParser>
+#include <QCoreApplication>
+#include <QDataStream>
+#include <QIODevice>
+
+namespace {
+
+/// Bumped only if the payload's shape changes incompatibly. Both ends of the
+/// socket are the same binary in the ordinary case, but an upgrade can leave an
+/// old instance running while a new one is launched, and a version the reader
+/// does not know is refused rather than misread.
+constexpr quint16 kPayloadVersion = 1;
+
+} // namespace
+
+LaunchSelectors LaunchSelectors::parse(const QStringList &arguments,
+ QString *error)
+{
+ if (error)
+ error->clear();
+
+ QCommandLineParser parser;
+ // No addHelpOption()/addVersionOption(): those are handled in main() before
+ // QApplication exists, and Qt's own versions call exit() through
+ // QCoreApplication, which is not constructed at that point.
+ QCommandLineOption accountOption(
+ QStringLiteral("account"),
+ QCoreApplication::translate(
+ "LaunchSelectors", "Open this account's view."),
+ QStringLiteral("key"));
+ QCommandLineOption threadOption(
+ QStringLiteral("thread"),
+ QCoreApplication::translate(
+ "LaunchSelectors", "Open this thread."),
+ QStringLiteral("id"));
+ QCommandLineOption messageOption(
+ QStringLiteral("message"),
+ QCoreApplication::translate(
+ "LaunchSelectors", "Open this message, inside its thread."),
+ QStringLiteral("id"));
+
+ parser.addOption(accountOption);
+ parser.addOption(threadOption);
+ parser.addOption(messageOption);
+
+ // parse(), not process(): process() prints to stderr and calls exit() on an
+ // error, which would take the window down over a typo. The error is
+ // returned instead and main() decides.
+ if (!parser.parse(arguments)) {
+ if (error)
+ *error = parser.errorText();
+ return {};
+ }
+
+ // --version and --help are consumed in main() before this runs, so they
+ // never reach the parser. An unknown option does, and is an error rather
+ // than something to ignore: today's strcmp loop ignores everything it does
+ // not recognise, so a typo silently produces an ordinary window.
+ const QStringList unknown = parser.unknownOptionNames();
+ if (!unknown.isEmpty()) {
+ if (error) {
+ *error = QCoreApplication::translate(
+ "LaunchSelectors", "Unknown option: %1")
+ .arg(unknown.join(QStringLiteral(", ")));
+ }
+ return {};
+ }
+
+ LaunchSelectors selectors;
+ selectors.account = parser.value(accountOption);
+ selectors.threadId = parser.value(threadOption);
+ selectors.messageId = parser.value(messageOption);
+ return selectors;
+}
+
+QString LaunchSelectors::helpText(const QString &versionDisplay)
+{
+ // The option NAMES are wire format and are never translated; the prose
+ // beside them is. Kept as one block rather than assembled from pieces so a
+ // translator sees the layout they are translating.
+ return QCoreApplication::translate(
+ "LaunchSelectors",
+ "qtmaildir %1 - a Qt6 mail client for notmuch-indexed Maildirs\n"
+ "\n"
+ "Usage: qtmaildir [options]\n"
+ "\n"
+ " -h, --help Show this help and exit\n"
+ " -v, --version Show the version and exit\n"
+ " --account <key> Open this account's view\n"
+ " --thread <id> Open this thread\n"
+ " --message <id> Open this message, inside its thread\n"
+ "\n"
+ "The three selectors combine. When qtmaildir is already "
+ "running,\n"
+ "a second launch hands its selectors to that window and exits "
+ "rather\n"
+ "than opening a second one.\n"
+ "\n"
+ "Configuration: ~/.config/qtmaildir/qtmaildir.conf\n"
+ "qtmaildir reads a notmuch-indexed Maildir. It does no network\n"
+ "protocol work: fetching and sending are external commands.\n")
+ .arg(versionDisplay);
+}
+
+QByteArray LaunchSelectors::toPayload() const
+{
+ // QDataStream rather than a line-based format: a Message-ID can contain
+ // almost anything, a newline included, and a length-prefixed encoding does
+ // not care. The round-trip test carries an embedded newline for exactly
+ // this reason.
+ QByteArray payload;
+ QDataStream stream(&payload, QIODevice::WriteOnly);
+ stream.setVersion(QDataStream::Qt_6_0);
+ stream << kPayloadVersion << account << threadId << messageId;
+ return payload;
+}
+
+LaunchSelectors LaunchSelectors::fromPayload(const QByteArray &payload,
+ QString *error)
+{
+ if (error)
+ error->clear();
+
+ if (payload.size() > kMaxPayloadBytes) {
+ if (error) {
+ *error = QCoreApplication::translate(
+ "LaunchSelectors", "Launch payload too large");
+ }
+ return {};
+ }
+
+ QDataStream stream(payload);
+ stream.setVersion(QDataStream::Qt_6_0);
+
+ quint16 version = 0;
+ stream >> version;
+ if (stream.status() != QDataStream::Ok || version != kPayloadVersion) {
+ if (error) {
+ *error = QCoreApplication::translate(
+ "LaunchSelectors", "Unrecognised launch payload");
+ }
+ return {};
+ }
+
+ LaunchSelectors selectors;
+ stream >> selectors.account >> selectors.threadId >> selectors.messageId;
+
+ // Checked AFTER every read, which is what catches a truncated payload: a
+ // short read leaves the stream in ReadPastEnd and the fields
+ // default-constructed, so without this a half-written message id would be
+ // applied as an empty one.
+ if (stream.status() != QDataStream::Ok) {
+ if (error) {
+ *error = QCoreApplication::translate(
+ "LaunchSelectors", "Truncated launch payload");
+ }
+ return {};
+ }
+
+ return selectors;
+}
diff --git a/src/launchselectors.h b/src/launchselectors.h
new file mode 100644
index 0000000..b1ac043
--- /dev/null
+++ b/src/launchselectors.h
@@ -0,0 +1,89 @@
+/*
+ * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs
+ * Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#pragma once
+
+#include <QByteArray>
+#include <QMetaType>
+#include <QString>
+#include <QStringList>
+
+/// What a launch asked the window to show (item 200).
+///
+/// A value type with no Qt GUI dependency, deliberately: it is parsed before
+/// QApplication exists, it crosses a socket, and both halves need tests that
+/// no window has to be built for.
+///
+/// The three selectors COMPOSE rather than excluding each other. "Open this
+/// message, in this account's view" is one request, and nothing about it is
+/// contradictory.
+struct LaunchSelectors
+{
+ /// An account key as written in the config, e.g. `work` from
+ /// `[account.work]`. Validated against the configured accounts by the
+ /// window, not here: this unit knows nothing about a Config.
+ QString account;
+
+ /// A notmuch thread id.
+ QString threadId;
+
+ /// A Message-ID, with or without the angle brackets.
+ QString messageId;
+
+ bool isEmpty() const
+ {
+ return account.isEmpty() && threadId.isEmpty() && messageId.isEmpty();
+ }
+
+ /// Longest payload accepted off the socket.
+ ///
+ /// A local socket hands over whatever the peer sends, and the peer here is
+ /// another copy of this program running as the same user, so this is not a
+ /// defence against an attacker. It is what stops a confused or truncated
+ /// writer from being read as an unbounded selector. Three ids and an
+ /// account key are a few hundred bytes; 64 KiB is room to spare.
+ static constexpr int kMaxPayloadBytes = 64 * 1024;
+
+ /// Parses a command line, `arguments[0]` being the program name.
+ ///
+ /// Takes a QStringList rather than argc/argv so it can be called before
+ /// QCoreApplication exists, which is what lets --version keep answering on
+ /// a machine where the GUI cannot open.
+ ///
+ /// On an unknown option, returns an empty result and sets \p error. The
+ /// caller prints it; it is NOT fatal to the window, since a typo should
+ /// not cost the user their mail client.
+ static LaunchSelectors parse(const QStringList &arguments, QString *error);
+
+ /// The help text, for `--help`. Translatable prose; the option NAMES are
+ /// wire format and are never translated.
+ static QString helpText(const QString &versionDisplay);
+
+ /// Serialises for the socket. The inverse of fromPayload().
+ QByteArray toPayload() const;
+
+ /// Parses a socket payload. On anything malformed, oversized or truncated,
+ /// returns an empty result and sets \p error.
+ static LaunchSelectors fromPayload(const QByteArray &payload,
+ QString *error);
+};
+
+/// Declared in the header that DEFINES the type, as types.h and threaddigest.h
+/// do for theirs. A consumer declaring it instead would leave any other
+/// consumer without it, and QSignalSpy needs it to carry the type.
+Q_DECLARE_METATYPE(LaunchSelectors)