Mailcommon

filteractionwithcommand.cpp
1/*
2 * SPDX-FileCopyrightText: 1996-1998 Stefan Taferner <taferner@kde.org>
3 *
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 *
6 */
7
8#include "filteractionwithcommand.h"
9#include "mailcommon_debug.h"
10#include <KProcess>
11#include <KShell>
12
13#include <QRegularExpression>
14#include <QTemporaryFile>
15
16using namespace MailCommon;
17
18FilterActionWithCommand::FilterActionWithCommand(const QString &name, const QString &label, QObject *parent)
19 : FilterActionWithUrl(name, label, parent)
20{
21}
22
23QWidget *FilterActionWithCommand::createParamWidget(QWidget *parent) const
24{
25 return FilterActionWithUrl::createParamWidget(parent);
26}
27
28void FilterActionWithCommand::applyParamWidgetValue(QWidget *paramWidget)
29{
30 FilterActionWithUrl::applyParamWidgetValue(paramWidget);
31}
32
33void FilterActionWithCommand::setParamWidgetValue(QWidget *paramWidget) const
34{
35 FilterActionWithUrl::setParamWidgetValue(paramWidget);
36}
37
38void FilterActionWithCommand::clearParamWidget(QWidget *paramWidget) const
39{
40 FilterActionWithUrl::clearParamWidget(paramWidget);
41}
42
43static KMime::Content *findMimeNodeForIndex(KMime::Content *node, int &index)
44{
45 if (index <= 0) {
46 return node;
47 }
48
49 const QList<KMime::Content *> lstContents = node->contents();
50 for (KMime::Content *child : lstContents) {
51 KMime::Content *result = findMimeNodeForIndex(child, --index);
52 if (result) {
53 return result;
54 }
55 }
56
57 return nullptr;
58}
59
60QString FilterActionWithCommand::substituteCommandLineArgsFor(const KMime::Message::Ptr &aMsg, QList<QTemporaryFile *> &aTempFileList) const
61{
62 QString result = mParameter;
63 QList<int> argList;
64 static const QRegularExpression re(QStringLiteral("%([0-9-]+)"));
65
66 // search for '%n'
67 QRegularExpressionMatchIterator iter = re.globalMatch(result);
68 while (iter.hasNext()) {
69 // and save the encountered 'n' in a list.
70 bool ok = false;
71 const int n = iter.next().captured(1).toInt(&ok);
72 if (ok) {
73 argList.append(n);
74 }
75 }
76
77 // sort the list of n's
78 std::sort(argList.begin(), argList.end());
79
80 // and use QString::arg to substitute filenames for the %n's.
81 int lastSeen = -2;
82 QString tempFileName;
84 for (QList<int>::ConstIterator it = argList.constBegin(); it != end; ++it) {
85 // setup temp files with check for duplicate %n's
86 if ((*it) != lastSeen) {
87 auto tempFile = new QTemporaryFile();
88 if (!tempFile->open()) {
89 delete tempFile;
90 qCDebug(MAILCOMMON_LOG) << "FilterActionWithCommand: Could not create temp file!";
91 return {};
92 }
93
94 aTempFileList.append(tempFile);
95 tempFileName = tempFile->fileName();
96
97 QFile file(tempFileName);
98 if (!file.open(QIODevice::WriteOnly)) {
99 qCWarning(MAILCOMMON_LOG) << "Failed to write message to file: " << file.errorString();
100 tempFile->close();
101 continue;
102 }
103
104 if ((*it) == -1) {
105 file.write(aMsg->encodedContent());
106 } else if (aMsg->contents().isEmpty()) {
107 file.write(aMsg->decodedContent());
108 } else {
109 int index = *it; // we pass by reference below, so this is not const
110 KMime::Content *content = findMimeNodeForIndex(aMsg.data(), index);
111 if (content) {
112 file.write(content->decodedContent());
113 }
114 }
115 file.close();
116 tempFile->close();
117 }
118
119 // QString( "%0 and %1 and %1" ).arg( 0 ).arg( 1 )
120 // returns "0 and 1 and %1", so we must call .arg as
121 // many times as there are %n's, regardless of their multiplicity.
122 if ((*it) == -1) {
123 result.replace(QLatin1StringView("%-1"), tempFileName);
124 } else {
125 result = result.arg(tempFileName);
126 }
127 }
128
129 return result;
130}
131
132namespace
133{
134/**
135 * Substitutes placeholders in the command line with the
136 * content of the corresponding header in the message.
137 * %{From} -> Joe Author <joe@acme.com>
138 */
139void substituteMessageHeaders(const KMime::Message::Ptr &aMsg, QString &result)
140{
141 // Replace the %{foo} with the content of the foo header field.
142 // If the header doesn't exist, remove the placeholder.
143 const QRegularExpression header_rx(QStringLiteral("%\\{([a-z0-9-]+)\\}"), QRegularExpression::CaseInsensitiveOption);
144 int offset = 0;
146 while (result.indexOf(header_rx, offset, &rmatch) != -1) {
147 const KMime::Headers::Base *header = aMsg->headerByType(rmatch.captured(1).toLatin1().constData());
148 QString replacement;
149 if (header) {
150 replacement = KShell::quoteArg(QString::fromLatin1(header->as7BitString()));
151 }
152 const int start = rmatch.capturedStart(0);
153 result.replace(start, rmatch.capturedLength(0), replacement);
154 offset = start + replacement.size();
155 }
156}
157
158/**
159 * Substitutes placeholders in the command line with the
160 * corresponding information from the item. Currently supported
161 * are %{itemid} and %{itemurl}.
162 */
163void substituteCommandLineArgsForItem(const Akonadi::Item &item, QString &commandLine)
164{
165 commandLine.replace(QLatin1StringView("%{itemurl}"), item.url(Akonadi::Item::UrlWithMimeType).url());
166 commandLine.replace(QLatin1StringView("%{itemid}"), QString::number(item.id()));
167}
168}
169
170FilterAction::ReturnCode FilterActionWithCommand::genericProcess(ItemContext &context, bool withOutput) const
171{
172 const auto aMsg = context.item().payload<KMime::Message::Ptr>();
173 Q_ASSERT(aMsg);
174
175 if (mParameter.isEmpty()) {
176 return ErrorButGoOn;
177 }
178
179 // KProcess doesn't support a QProcess::launch() equivalent, so
180 // we must use a temp file :-(
181 auto inFile = new QTemporaryFile;
182 if (!inFile->open()) {
183 delete inFile;
184 return ErrorButGoOn;
185 }
186
188 atmList.append(inFile);
189
190 QString commandLine = substituteCommandLineArgsFor(aMsg, atmList);
191 substituteCommandLineArgsForItem(context.item(), commandLine);
192 substituteMessageHeaders(aMsg, commandLine);
193
194 if (commandLine.isEmpty()) {
195 qDeleteAll(atmList);
196 atmList.clear();
197 return ErrorButGoOn;
198 }
199 // The parentheses force the creation of a subshell
200 // in which the user-specified command is executed.
201 // This is to really catch all output of the command as well
202 // as to avoid clashes of our redirection with the ones
203 // the user may have specified. In the long run, we
204 // shouldn't be using tempfiles at all for this class, due
205 // to security aspects. (mmutz)
206 commandLine = QLatin1Char('(') + commandLine + QLatin1StringView(") <") + inFile->fileName();
207
208 // write message to file
209 QString tempFileName = inFile->fileName();
210 QFile tempFile(tempFileName);
211 if (!tempFile.open(QIODevice::ReadWrite)) {
212 qCWarning(MAILCOMMON_LOG) << "Failed to write message to file: " << tempFile.errorString();
213 qDeleteAll(atmList);
214 atmList.clear();
215 return CriticalError;
216 }
217 tempFile.write(aMsg->encodedContent());
218 tempFile.close();
219 inFile->close();
220
221 KProcess shProc;
223 shProc.setShellCommand(commandLine);
224 int result = shProc.execute();
225
226 if (result != 0) {
227 qDeleteAll(atmList);
228 atmList.clear();
229 return ErrorButGoOn;
230 }
231
232 if (withOutput) {
233 // read altered message:
234 const QByteArray msgText = shProc.readAllStandardOutput();
235
236 if (!msgText.trimmed().isEmpty()) {
237 /* If the pipe through alters the message, it could very well
238 happen that it no longer has a X-UID header afterwards. That is
239 unfortunate, as we need to removed the original from the folder
240 using that, and look it up in the message. When the (new) message
241 is uploaded, the header is stripped anyhow. */
242 QString uid;
243 if (auto hrd = aMsg->headerByType("X-UID")) {
244 uid = hrd->asUnicodeString();
245 }
246 aMsg->setContent(KMime::CRLFtoLF(msgText));
247 aMsg->setFrozen(true);
248 aMsg->parse();
249
250 QString newUid;
251 if (auto hrd = aMsg->headerByType("X-UID")) {
252 newUid = hrd->asUnicodeString();
253 }
254 if (uid != newUid) {
255 aMsg->setFrozen(false);
256 auto header = new KMime::Headers::Generic("X-UID");
257 header->fromUnicodeString(uid, "utf-8");
258 aMsg->setHeader(header);
259 aMsg->assemble();
260 }
261
262 context.setNeedsPayloadStore();
263 } else {
264 qDeleteAll(atmList);
265 atmList.clear();
266 return ErrorButGoOn;
267 }
268 }
269
270 qDeleteAll(atmList);
271 atmList.clear();
272
273 return GoOn;
274}
275
276#include "moc_filteractionwithcommand.cpp"
QUrl url(UrlType type=UrlShort) const
Id id() const
T payload() const
QByteArray decodedContent()
QList< Content * > contents() const
virtual QByteArray as7BitString(bool withHeaderType=true) const=0
virtual void fromUnicodeString(const QString &s, const QByteArray &b)=0
void setShellCommand(const QString &cmd)
void setOutputChannelMode(OutputChannelMode mode)
int execute(int msecs=-1)
ReturnCode
Describes the possible return codes of filter processing:
@ ErrorButGoOn
A non-critical error occurred.
@ GoOn
Go on with applying filter actions.
A helper class for the filtering process.
Definition itemcontext.h:27
void setNeedsPayloadStore()
Marks that the item's payload has been changed and needs to be written back.
Akonadi::Item & item()
Returns the item of the context.
Q_SCRIPTABLE Q_NOREPLY void start()
KCOREADDONS_EXPORT QString quoteArg(const QString &arg)
QString label(StandardShortcut id)
const QList< QKeySequence > & end()
QString name(StandardShortcut id)
The filter dialog.
const char * constData() const const
bool isEmpty() const const
QByteArray trimmed() const const
void append(QList< T > &&value)
iterator begin()
void clear()
const_iterator constBegin() const const
const_iterator constEnd() const const
iterator end()
QObject * parent() const const
QByteArray readAllStandardOutput()
QString captured(QStringView name) const const
qsizetype capturedLength(QStringView name) const const
qsizetype capturedStart(QStringView name) const const
QRegularExpressionMatch next()
T * data() const const
QString arg(Args &&... args) const const
QString fromLatin1(QByteArrayView str)
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
QString number(double n, char format, int precision)
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
qsizetype size() const const
int toInt(bool *ok, int base) const const
QByteArray toLatin1() const const
QString url(FormattingOptions options) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:14:00 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.