File indexing completed on 2024-11-10 04:50:05
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"