Mailcommon

filteractionwithcommand.cpp
1 /*
2  * SPDX-FileCopyrightText: 1996-1998 Stefan Taferner <[email protected]>
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 
16 using namespace MailCommon;
17 
18 FilterActionWithCommand::FilterActionWithCommand(const QString &name, const QString &label, QObject *parent)
19  : FilterActionWithUrl(name, label, parent)
20 {
21 }
22 
23 QWidget *FilterActionWithCommand::createParamWidget(QWidget *parent) const
24 {
25  return FilterActionWithUrl::createParamWidget(parent);
26 }
27 
28 void FilterActionWithCommand::applyParamWidgetValue(QWidget *paramWidget)
29 {
30  FilterActionWithUrl::applyParamWidgetValue(paramWidget);
31 }
32 
33 void FilterActionWithCommand::setParamWidgetValue(QWidget *paramWidget) const
34 {
35  FilterActionWithUrl::setParamWidgetValue(paramWidget);
36 }
37 
38 void FilterActionWithCommand::clearParamWidget(QWidget *paramWidget) const
39 {
40  FilterActionWithUrl::clearParamWidget(paramWidget);
41 }
42 
43 static KMime::Content *findMimeNodeForIndex(KMime::Content *node, int &index)
44 {
45  if (index <= 0) {
46  return node;
47  }
48 
49  const QVector<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 
60 QString FilterActionWithCommand::substituteCommandLineArgsFor(const KMime::Message::Ptr &aMsg, QVector<QTemporaryFile *> &aTempFileList) const
61 {
62  QString result = mParameter;
63  QList<int> argList;
64  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(QLatin1String("%-1"), tempFileName);
124  } else {
125  result = result.arg(tempFileName);
126  }
127  }
128 
129  return result;
130 }
131 
132 namespace
133 {
134 /**
135  * Substitutes placeholders in the command line with the
136  * content of the corresponding header in the message.
137  * %{From} -> Joe Author <[email protected]>
138  */
139 void 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  */
163 void substituteCommandLineArgsForItem(const Akonadi::Item &item, QString &commandLine)
164 {
165  commandLine.replace(QLatin1String("%{itemurl}"), item.url(Akonadi::Item::UrlWithMimeType).url());
166  commandLine.replace(QLatin1String("%{itemid}"), QString::number(item.id()));
167 }
168 }
169 
170 FilterAction::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 + QLatin1String(") <") + 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 }
void append(const T &value)
T * data() const const
QString number(int n, int base)
int size() const const
void setShellCommand(const QString &cmd)
QString url(QUrl::FormattingOptions options) const const
void append(const T &value)
Q_SCRIPTABLE Q_NOREPLY void start()
QByteArray readAllStandardOutput()
QByteArray trimmed() const const
QByteArray toLatin1() const const
QList::const_iterator constBegin() const const
QRegularExpressionMatch next()
void setOutputChannelMode(OutputChannelMode mode)
QUrl url(UrlType type=UrlShort) const
void clear()
bool isEmpty() const const
int toInt(bool *ok, int base) const const
int indexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
void setNeedsPayloadStore()
Marks that the item's payload has been changed and needs to be written back.
Definition: itemcontext.cpp:33
QString & replace(int position, int n, QChar after)
virtual void fromUnicodeString(const QString &s, const QByteArray &b)=0
QByteArray decodedContent()
A helper class for the filtering process.
Definition: itemcontext.h:26
QString label(StandardShortcut id)
ReturnCode
Describes the possible return codes of filter processing:
Definition: filteraction.h:45
bool isEmpty() const const
Id id() const
QList::const_iterator constEnd() const const
const char * constData() const const
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
KCOREADDONS_EXPORT QString quoteArg(const QString &arg)
QString fromLatin1(const char *str, int size)
QString name(StandardShortcut id)
virtual QByteArray as7BitString(bool withHeaderType=true) const=0
QList::iterator begin()
int execute(int msecs=-1)
QList::iterator end()
Akonadi::Item & item()
Returns the item of the context.
Definition: itemcontext.cpp:18
const QList< QKeySequence > & end()
QString captured(int nth) const const
int capturedLength(int nth) const const
The filter dialog.
int capturedStart(int nth) const const
QVector< Content * > contents() const
T payload() const
This file is part of the KDE documentation.
Documentation copyright © 1996-2022 The KDE developers.
Generated on Sat Oct 1 2022 04:00:52 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.