aboutsummaryrefslogtreecommitdiffstats
path: root/src/launchselectors.cpp
blob: b5adddea3177761fe81588affcda7ef61d2b9776 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
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;
}