File indexing completed on 2024-09-22 04:50:01

0001 /*
0002  * SPDX-FileCopyrightText: 1996-1998 Stefan Taferner <taferner@kde.org>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-or-later
0005  *
0006  */
0007 
0008 #include "filteractionwithcommand.h"
0009 #include "mailcommon_debug.h"
0010 #include <KProcess>
0011 #include <KShell>
0012 
0013 #include <QRegularExpression>
0014 #include <QTemporaryFile>
0015 
0016 using namespace MailCommon;
0017 
0018 FilterActionWithCommand::FilterActionWithCommand(const QString &name, const QString &label, QObject *parent)
0019     : FilterActionWithUrl(name, label, parent)
0020 {
0021 }
0022 
0023 QWidget *FilterActionWithCommand::createParamWidget(QWidget *parent) const
0024 {
0025     return FilterActionWithUrl::createParamWidget(parent);
0026 }
0027 
0028 void FilterActionWithCommand::applyParamWidgetValue(QWidget *paramWidget)
0029 {
0030     FilterActionWithUrl::applyParamWidgetValue(paramWidget);
0031 }
0032 
0033 void FilterActionWithCommand::setParamWidgetValue(QWidget *paramWidget) const
0034 {
0035     FilterActionWithUrl::setParamWidgetValue(paramWidget);
0036 }
0037 
0038 void FilterActionWithCommand::clearParamWidget(QWidget *paramWidget) const
0039 {
0040     FilterActionWithUrl::clearParamWidget(paramWidget);
0041 }
0042 
0043 static KMime::Content *findMimeNodeForIndex(KMime::Content *node, int &index)
0044 {
0045     if (index <= 0) {
0046         return node;
0047     }
0048 
0049     const QList<KMime::Content *> lstContents = node->contents();
0050     for (KMime::Content *child : lstContents) {
0051         KMime::Content *result = findMimeNodeForIndex(child, --index);
0052         if (result) {
0053             return result;
0054         }
0055     }
0056 
0057     return nullptr;
0058 }
0059 
0060 QString FilterActionWithCommand::substituteCommandLineArgsFor(const KMime::Message::Ptr &aMsg, QList<QTemporaryFile *> &aTempFileList) const
0061 {
0062     QString result = mParameter;
0063     QList<int> argList;
0064     static const QRegularExpression re(QStringLiteral("%([0-9-]+)"));
0065 
0066     // search for '%n'
0067     QRegularExpressionMatchIterator iter = re.globalMatch(result);
0068     while (iter.hasNext()) {
0069         // and save the encountered 'n' in a list.
0070         bool ok = false;
0071         const int n = iter.next().captured(1).toInt(&ok);
0072         if (ok) {
0073             argList.append(n);
0074         }
0075     }
0076 
0077     // sort the list of n's
0078     std::sort(argList.begin(), argList.end());
0079 
0080     // and use QString::arg to substitute filenames for the %n's.
0081     int lastSeen = -2;
0082     QString tempFileName;
0083     QList<int>::ConstIterator end(argList.constEnd());
0084     for (QList<int>::ConstIterator it = argList.constBegin(); it != end; ++it) {
0085         // setup temp files with check for duplicate %n's
0086         if ((*it) != lastSeen) {
0087             auto tempFile = new QTemporaryFile();
0088             if (!tempFile->open()) {
0089                 delete tempFile;
0090                 qCDebug(MAILCOMMON_LOG) << "FilterActionWithCommand: Could not create temp file!";
0091                 return {};
0092             }
0093 
0094             aTempFileList.append(tempFile);
0095             tempFileName = tempFile->fileName();
0096 
0097             QFile file(tempFileName);
0098             if (!file.open(QIODevice::WriteOnly)) {
0099                 qCWarning(MAILCOMMON_LOG) << "Failed to write message to file: " << file.errorString();
0100                 tempFile->close();
0101                 continue;
0102             }
0103 
0104             if ((*it) == -1) {
0105                 file.write(aMsg->encodedContent());
0106             } else if (aMsg->contents().isEmpty()) {
0107                 file.write(aMsg->decodedContent());
0108             } else {
0109                 int index = *it; // we pass by reference below, so this is not const
0110                 KMime::Content *content = findMimeNodeForIndex(aMsg.data(), index);
0111                 if (content) {
0112                     file.write(content->decodedContent());
0113                 }
0114             }
0115             file.close();
0116             tempFile->close();
0117         }
0118 
0119         // QString( "%0 and %1 and %1" ).arg( 0 ).arg( 1 )
0120         // returns "0 and 1 and %1", so we must call .arg as
0121         // many times as there are %n's, regardless of their multiplicity.
0122         if ((*it) == -1) {
0123             result.replace(QLatin1StringView("%-1"), tempFileName);
0124         } else {
0125             result = result.arg(tempFileName);
0126         }
0127     }
0128 
0129     return result;
0130 }
0131 
0132 namespace
0133 {
0134 /**
0135  * Substitutes placeholders in the command line with the
0136  * content of the corresponding header in the message.
0137  * %{From} -> Joe Author <joe@acme.com>
0138  */
0139 void substituteMessageHeaders(const KMime::Message::Ptr &aMsg, QString &result)
0140 {
0141     // Replace the %{foo} with the content of the foo header field.
0142     // If the header doesn't exist, remove the placeholder.
0143     const QRegularExpression header_rx(QStringLiteral("%\\{([a-z0-9-]+)\\}"), QRegularExpression::CaseInsensitiveOption);
0144     int offset = 0;
0145     QRegularExpressionMatch rmatch;
0146     while (result.indexOf(header_rx, offset, &rmatch) != -1) {
0147         const KMime::Headers::Base *header = aMsg->headerByType(rmatch.captured(1).toLatin1().constData());
0148         QString replacement;
0149         if (header) {
0150             replacement = KShell::quoteArg(QString::fromLatin1(header->as7BitString()));
0151         }
0152         const int start = rmatch.capturedStart(0);
0153         result.replace(start, rmatch.capturedLength(0), replacement);
0154         offset = start + replacement.size();
0155     }
0156 }
0157 
0158 /**
0159  * Substitutes placeholders in the command line with the
0160  * corresponding information from the item. Currently supported
0161  * are %{itemid} and %{itemurl}.
0162  */
0163 void substituteCommandLineArgsForItem(const Akonadi::Item &item, QString &commandLine)
0164 {
0165     commandLine.replace(QLatin1StringView("%{itemurl}"), item.url(Akonadi::Item::UrlWithMimeType).url());
0166     commandLine.replace(QLatin1StringView("%{itemid}"), QString::number(item.id()));
0167 }
0168 }
0169 
0170 FilterAction::ReturnCode FilterActionWithCommand::genericProcess(ItemContext &context, bool withOutput) const
0171 {
0172     const auto aMsg = context.item().payload<KMime::Message::Ptr>();
0173     Q_ASSERT(aMsg);
0174 
0175     if (mParameter.isEmpty()) {
0176         return ErrorButGoOn;
0177     }
0178 
0179     // KProcess doesn't support a QProcess::launch() equivalent, so
0180     // we must use a temp file :-(
0181     auto inFile = new QTemporaryFile;
0182     if (!inFile->open()) {
0183         delete inFile;
0184         return ErrorButGoOn;
0185     }
0186 
0187     QList<QTemporaryFile *> atmList;
0188     atmList.append(inFile);
0189 
0190     QString commandLine = substituteCommandLineArgsFor(aMsg, atmList);
0191     substituteCommandLineArgsForItem(context.item(), commandLine);
0192     substituteMessageHeaders(aMsg, commandLine);
0193 
0194     if (commandLine.isEmpty()) {
0195         qDeleteAll(atmList);
0196         atmList.clear();
0197         return ErrorButGoOn;
0198     }
0199     // The parentheses force the creation of a subshell
0200     // in which the user-specified command is executed.
0201     // This is to really catch all output of the command as well
0202     // as to avoid clashes of our redirection with the ones
0203     // the user may have specified. In the long run, we
0204     // shouldn't be using tempfiles at all for this class, due
0205     // to security aspects. (mmutz)
0206     commandLine = QLatin1Char('(') + commandLine + QLatin1StringView(") <") + inFile->fileName();
0207 
0208     // write message to file
0209     QString tempFileName = inFile->fileName();
0210     QFile tempFile(tempFileName);
0211     if (!tempFile.open(QIODevice::ReadWrite)) {
0212         qCWarning(MAILCOMMON_LOG) << "Failed to write message to file: " << tempFile.errorString();
0213         qDeleteAll(atmList);
0214         atmList.clear();
0215         return CriticalError;
0216     }
0217     tempFile.write(aMsg->encodedContent());
0218     tempFile.close();
0219     inFile->close();
0220 
0221     KProcess shProc;
0222     shProc.setOutputChannelMode(KProcess::SeparateChannels);
0223     shProc.setShellCommand(commandLine);
0224     int result = shProc.execute();
0225 
0226     if (result != 0) {
0227         qDeleteAll(atmList);
0228         atmList.clear();
0229         return ErrorButGoOn;
0230     }
0231 
0232     if (withOutput) {
0233         // read altered message:
0234         const QByteArray msgText = shProc.readAllStandardOutput();
0235 
0236         if (!msgText.trimmed().isEmpty()) {
0237             /* If the pipe through alters the message, it could very well
0238             happen that it no longer has a X-UID header afterwards. That is
0239             unfortunate, as we need to removed the original from the folder
0240             using that, and look it up in the message. When the (new) message
0241             is uploaded, the header is stripped anyhow. */
0242             QString uid;
0243             if (auto hrd = aMsg->headerByType("X-UID")) {
0244                 uid = hrd->asUnicodeString();
0245             }
0246             aMsg->setContent(KMime::CRLFtoLF(msgText));
0247             aMsg->setFrozen(true);
0248             aMsg->parse();
0249 
0250             QString newUid;
0251             if (auto hrd = aMsg->headerByType("X-UID")) {
0252                 newUid = hrd->asUnicodeString();
0253             }
0254             if (uid != newUid) {
0255                 aMsg->setFrozen(false);
0256                 auto header = new KMime::Headers::Generic("X-UID");
0257                 header->fromUnicodeString(uid, "utf-8");
0258                 aMsg->setHeader(header);
0259                 aMsg->assemble();
0260             }
0261 
0262             context.setNeedsPayloadStore();
0263         } else {
0264             qDeleteAll(atmList);
0265             atmList.clear();
0266             return ErrorButGoOn;
0267         }
0268     }
0269 
0270     qDeleteAll(atmList);
0271     atmList.clear();
0272 
0273     return GoOn;
0274 }
0275 
0276 #include "moc_filteractionwithcommand.cpp"