File indexing completed on 2025-03-09 04:53:56

0001 /*
0002   SPDX-FileCopyrightText: 2019 Glen Ditchfield <GJDitchfield@acm.org>
0003 
0004   SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "replystrategytest.h"
0008 
0009 #include <QSignalSpy>
0010 #include <QStandardPaths>
0011 #include <QStringLiteral>
0012 #include <QTest>
0013 
0014 #include <KIdentityManagementCore/Identity>
0015 #include <KIdentityManagementCore/IdentityManager>
0016 
0017 #include <MessageComposer/Composer>
0018 #include <MessageComposer/GlobalPart>
0019 #include <MessageComposer/InfoPart>
0020 #include <MessageComposer/TextPart>
0021 
0022 const auto defaultAddress{QStringLiteral("default@example.org")};
0023 const auto nondefaultAddress{QStringLiteral("nondefault@example.com")};
0024 const auto friend1Address{QStringLiteral("friend1@example.net")};
0025 const auto friend2Address{QStringLiteral("friend2@example.net")};
0026 const auto replyAddress{QStringLiteral("reply@example.com")};
0027 const auto followupAddress{QStringLiteral("followup@example.org")};
0028 const auto listAddress{QStringLiteral("list@example.com")};
0029 const auto mailReplyAddress{QStringLiteral("mailreply@example.com")};
0030 const QStringList nobody{};
0031 
0032 static inline const QStringList only(const QString &address)
0033 {
0034     return QStringList{address};
0035 }
0036 
0037 static inline const QStringList both(const QString &address1, const QString &address2)
0038 {
0039     return QStringList{address1, address2};
0040 }
0041 
0042 using namespace MessageComposer;
0043 
0044 static KMime::Message::Ptr basicMessage(const QString &fromAddress, const QStringList &toAddresses)
0045 {
0046     Composer composer;
0047     composer.infoPart()->setFrom(fromAddress);
0048     composer.infoPart()->setTo(toAddresses);
0049     composer.infoPart()->setSubject(QStringLiteral("Test Email Subject"));
0050     composer.textPart()->setWrappedPlainText(QStringLiteral("Test email body."));
0051     composer.exec();
0052 
0053     return composer.resultMessages().first();
0054 }
0055 
0056 #define COMPARE_ADDRESSES(actual, expected)                                                                                                                    \
0057     if (!compareAddresses(actual, expected)) {                                                                                                                 \
0058         QFAIL(qPrintable(QStringLiteral("%1 is \"%2\"").arg(QString::fromLatin1(#actual), actual->displayString())));                                          \
0059         return;                                                                                                                                                \
0060     }
0061 
0062 template<class T>
0063 bool compareAddresses(const T *actual, const QStringList &expected)
0064 {
0065     auto addresses{actual->addresses()};
0066     if (addresses.length() != expected.length()) {
0067         return false;
0068     }
0069     for (const auto &e : expected) {
0070         if (!addresses.contains(e.toLatin1())) {
0071             return false;
0072         }
0073     }
0074     return true;
0075 }
0076 
0077 ReplyStrategyTest::ReplyStrategyTest(QObject *parent)
0078     : QObject(parent)
0079 {
0080     QStandardPaths::setTestModeEnabled(true);
0081 }
0082 
0083 ReplyStrategyTest::~ReplyStrategyTest()
0084 {
0085     // Workaround QTestLib not flushing deleteLater()s on exit, which
0086     // leads to WebEngine asserts (view not deleted)
0087     QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
0088 }
0089 
0090 void ReplyStrategyTest::initTestCase()
0091 {
0092     QFile::remove(QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/emailidentities"));
0093     QFile::remove(QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/emaildefaults"));
0094 
0095     mIdentityManager = new KIdentityManagementCore::IdentityManager;
0096 
0097     auto homeIdentity =
0098         mIdentityManager->newFromExisting(KIdentityManagementCore::Identity{QStringLiteral("Home Identity"), QStringLiteral("Full Home Name"), defaultAddress});
0099     QVERIFY(mIdentityManager->setAsDefault(homeIdentity.uoid()));
0100 
0101     auto workIdentity = mIdentityManager->newFromExisting(
0102         KIdentityManagementCore::Identity{QStringLiteral("Work Identity"), QStringLiteral("Full Work Name"), nondefaultAddress});
0103 
0104     mIdentityManager->commit();
0105 }
0106 
0107 void ReplyStrategyTest::cleanupTestCase()
0108 {
0109     delete mIdentityManager;
0110 }
0111 
0112 KMime::Message::Ptr ReplyStrategyTest::makeReply(const KMime::Message::Ptr &original, const ReplyStrategy strategy)
0113 {
0114     MessageFactoryNG factory{original, 0};
0115     factory.setReplyStrategy(strategy);
0116     factory.setIdentityManager(mIdentityManager);
0117     QSignalSpy spy{&factory, &MessageFactoryNG::createReplyDone};
0118     factory.createReplyAsync();
0119     KMime::Message::Ptr result{nullptr};
0120     [&] {
0121         QVERIFY(spy.wait());
0122         QCOMPARE(spy.count(), 1);
0123         result = spy.at(0).at(0).value<MessageFactoryNG::MessageReply>().msg;
0124     }();
0125     return result;
0126 }
0127 
0128 void ReplyStrategyTest::testReply_data()
0129 {
0130     QTest::addColumn<QString>("oFrom"); // Original message's From address.
0131     QTest::addColumn<QStringList>("oTo"); // Original message's To addresses.
0132     QTest::addColumn<QStringList>("oCc"); // Original message's CC addresses.
0133     QTest::addColumn<QStringList>("oRT"); // Original message's Reply-To addresses.
0134     QTest::addColumn<QStringList>("oMFT"); // Original message's Mail-Followup-To addresses.
0135     QTest::addColumn<QString>("oLP"); // Original message's List-Post address.
0136     QTest::addColumn<QStringList>("oMRT"); // Original message's Mail-Reply-To addresses.
0137     QTest::addColumn<int>("strategy"); // ReplyStrategy (passed as an int).
0138     QTest::addColumn<QString>("rFrom"); // Reply's expected From address.
0139     QTest::addColumn<QStringList>("rTo"); // Reply's expected To addresses.
0140     QTest::addColumn<QStringList>("rCc"); // Reply's expected CC addresses.
0141 
0142     // Smart Replies
0143     // -------------
0144     // Smart Reply does not set CC headers.  (Compare ReplyAll.)
0145     // ReplySmart uses Mail-Reply-To, Reply-To, or From (in that order)
0146     // for the original's author's address, if List-Post is absent.
0147     QTest::newRow("ReplySmart, from someone to default identity") << friend1Address << only(defaultAddress) << only(friend2Address) << nobody << nobody
0148                                                                   << QString() << nobody << (int)ReplySmart << defaultAddress << only(friend1Address) << nobody;
0149     QTest::newRow("ReplySmart, from someone to non-default identity")
0150         << friend1Address << both(friend2Address, nondefaultAddress) << only(defaultAddress) << nobody << nobody << QString() << nobody << (int)ReplySmart
0151         << nondefaultAddress << only(friend1Address) << nobody;
0152     QTest::newRow("ReplySmart, from someone with Reply-To")
0153         << friend1Address << only(defaultAddress) << only(friend2Address) << both(replyAddress, friend2Address) << nobody << QString() << nobody
0154         << (int)ReplySmart << defaultAddress << both(friend2Address, replyAddress) << nobody;
0155     QTest::newRow("ReplySmart, from someone with Mail-Reply-To")
0156         << friend1Address << only(defaultAddress) << only(friend2Address) << only(replyAddress) << nobody << QString() << only(mailReplyAddress)
0157         << (int)ReplySmart << defaultAddress << only(mailReplyAddress) << nobody;
0158 
0159     // If the original message was _from_ the user _to_ another person (the
0160     // reverse of the usual direction), a smart reply goes to the other person.
0161     // Therefore Mail-Reply-To and Reply-To are ignored.
0162     // The reply is assumed to add to the original message.
0163     QTest::newRow("ReplySmart, from default identity to someone") << defaultAddress << only(friend1Address) << only(friend2Address) << nobody << nobody
0164                                                                   << QString() << nobody << (int)ReplySmart << defaultAddress << only(friend1Address) << nobody;
0165     QTest::newRow("ReplySmart, from default identity with Reply-To to someone")
0166         << defaultAddress << only(friend1Address) << only(friend2Address) << only(replyAddress) << nobody << QString() << only(mailReplyAddress)
0167         << (int)ReplySmart << defaultAddress << only(friend1Address) << nobody;
0168 
0169     // If the original message was from one of the user's identities to another
0170     // identity (i.e., between two of the user's mail accounts), a smart reply
0171     // goes back to the sending identity.
0172     QTest::newRow("ReplySmart, between identities") << defaultAddress << only(nondefaultAddress) << only(friend2Address) << nobody << nobody << QString()
0173                                                     << nobody << (int)ReplySmart << nondefaultAddress << only(defaultAddress) << nobody;
0174 
0175     // If the original message appears to be from a mailing list, smart replies
0176     // go to the Mail-Followup-To, Reply-To, or List-Post addresses, in that
0177     // order of preference.
0178     QTest::newRow("ReplySmart, from list with Mail-Followup-To")
0179         << friend1Address << only(defaultAddress) << only(friend2Address) << only(replyAddress) << only(followupAddress) << listAddress << nobody
0180         << (int)ReplySmart << defaultAddress << only(followupAddress) << nobody;
0181     QTest::newRow("ReplySmart, from list with Reply-To") << friend1Address << only(defaultAddress) << only(friend2Address) << only(replyAddress) << nobody
0182                                                          << listAddress << nobody << (int)ReplySmart << defaultAddress << only(replyAddress) << nobody;
0183     QTest::newRow("ReplySmart, from list with List-Post")
0184         << friend1Address << only(nondefaultAddress) << only(friend2Address) << nobody << nobody << listAddress << only(mailReplyAddress) << (int)ReplySmart
0185         << nondefaultAddress << only(listAddress) << nobody;
0186 
0187     // Replies to Mailing Lists
0188     // ------------------------
0189     // If the original message has a Mail-Followup-To header, replies to the list
0190     // go to the followup address, in preference to List-Post and Reply-To.
0191     // Cc and Mail-Reply-To are ignored.
0192     QTest::newRow("ReplyList, from list with Mail-Followup-To")
0193         << friend1Address << only(defaultAddress) << only(friend2Address) << only(replyAddress) << only(followupAddress) << listAddress
0194         << only(mailReplyAddress) << (int)ReplyList << defaultAddress << only(followupAddress) << nobody;
0195 
0196     // If the original message has a List-Post header, replies to the list
0197     // go to that address, in preference to Reply-To.
0198     QTest::newRow("ReplyList, from list with List-Post") << friend1Address << only(defaultAddress) << nobody << only(replyAddress) << nobody << listAddress
0199                                                          << nobody << (int)ReplyList << defaultAddress << only(listAddress) << nobody;
0200 
0201     // If the original message has just a Reply-To header, assume the list
0202     // [munges Reply-To](https://www.gnu.org/software/mailman/mailman-admin/node11.html)
0203     /// and send the reply to that address.
0204     QTest::newRow("ReplyList, from list with Reply-To") << friend1Address << only(defaultAddress) << nobody << only(replyAddress) << nobody << QString()
0205                                                         << nobody << (int)ReplyList << defaultAddress << only(replyAddress) << nobody;
0206 
0207     // If the original message has neither Mail-Followup-To, List-Post, nor
0208     // Reply-To headers, replies to the list do not choose a To address.
0209     QTest::newRow("ReplyList, from list with no headers")
0210         << friend1Address << only(defaultAddress) << nobody << nobody << nobody << QString() << nobody << (int)ReplyList << defaultAddress << nobody << nobody;
0211 
0212     // Replies to All
0213     // --------------
0214     // ReplyAll adds CC addresses to the reply for the original's recipients,
0215     // except for the user's identities.
0216     QTest::newRow("ReplyAll, with Cc in original") << friend1Address << only(defaultAddress) << both(friend2Address, nondefaultAddress) << nobody << nobody
0217                                                    << QString() << nobody << (int)ReplyAll << defaultAddress << only(friend1Address) << only(friend2Address);
0218     QTest::newRow("ReplyAll, with multiple To addresses in original")
0219         << friend1Address << both(friend2Address, nondefaultAddress) << only(defaultAddress) << nobody << nobody << QString() << nobody << (int)ReplyAll
0220         << nondefaultAddress << both(friend1Address, friend2Address) << nobody;
0221     QTest::newRow("ReplyAll, with Reply-To in original")
0222         << friend1Address << only(defaultAddress) << only(friend2Address) << only(replyAddress) << nobody << QString() << nobody << (int)ReplyAll
0223         << defaultAddress << only(replyAddress) << only(friend2Address);
0224     QTest::newRow("ReplyAll, with Mail-Reply-To in original")
0225         << friend1Address << only(defaultAddress) << only(friend2Address) << only(replyAddress) << nobody << QString() << only(mailReplyAddress)
0226         << (int)ReplyAll << defaultAddress << only(mailReplyAddress) << only(friend2Address);
0227 
0228     // If the original message was _from_ the user _to_ another person (the
0229     // reverse of the usual direction), reply to all goes to the other person.
0230     // Therefore Mail-Reply-To and Reply-To are ignored.
0231     // The reply is assumed to add to the original message.
0232     QTest::newRow("ReplyAll, from default identity to someone")
0233         << defaultAddress << only(friend1Address) << only(friend2Address) << only(replyAddress) << nobody << QString() << only(mailReplyAddress)
0234         << (int)ReplyAll << defaultAddress << only(friend1Address) << only(friend2Address);
0235 
0236     // If the original message was from one of the user's identities to another
0237     // identity (i.e., between two of the user's mail accounts), reply to all
0238     // goes back to the sending identity.
0239     QTest::newRow("ReplyAll, between identities") << defaultAddress << only(nondefaultAddress) << only(friend2Address) << nobody << nobody << QString()
0240                                                   << nobody << (int)ReplyAll << nondefaultAddress << only(defaultAddress) << only(friend2Address);
0241 
0242     // If the original passed through a mailing list, ReplyAll replies to the
0243     // list.
0244     // It CCs the author, using Mail-Reply-To, Reply-To, or From (in that order).
0245     QTest::newRow("ReplyAll, from list with List-Post")
0246         << friend1Address << only(nondefaultAddress) << only(friend2Address) << nobody << nobody << listAddress << nobody << (int)ReplyAll << nondefaultAddress
0247         << only(listAddress) << both(friend1Address, friend2Address);
0248     QTest::newRow("ReplyAll, from list with Reply-To")
0249         << friend1Address << only(defaultAddress) << only(friend2Address) << only(replyAddress) << nobody << listAddress << nobody << (int)ReplyAll
0250         << defaultAddress << only(listAddress) << both(replyAddress, friend2Address);
0251     QTest::newRow("ReplyAll, from list with Mail-Reply-To")
0252         << friend1Address << only(defaultAddress) << only(friend2Address) << only(replyAddress) << nobody << listAddress << only(mailReplyAddress)
0253         << (int)ReplyAll << defaultAddress << only(listAddress) << both(mailReplyAddress, friend2Address);
0254 
0255     // If Reply-To is the same as List-Post, ReplyAll ignores it and uses
0256     // From for the author's address, because the mailing list munged Reply-To.
0257     QTest::newRow("ReplyAll, from list that munges Reply-To") << friend1Address << only(defaultAddress) << nobody << only(listAddress) << nobody << listAddress
0258                                                               << nobody << (int)ReplyAll << defaultAddress << only(listAddress) << only(friend1Address);
0259 
0260     // If Reply-To contains List-Post, ReplyAll uses the other reply
0261     // addresses, because the mailing list didn't completely munge Reply-To.
0262     QTest::newRow("ReplyAll, from list that lightly munges Reply-To")
0263         << friend1Address << only(defaultAddress) << nobody << both(replyAddress, listAddress) << nobody << listAddress << nobody << (int)ReplyAll
0264         << defaultAddress << only(listAddress) << only(replyAddress);
0265 
0266     // If Mail-Followup-To header is present, use it for To and ignore other
0267     // headers.  Cc is empty.
0268     QTest::newRow("ReplyAll, from list with Reply-To and Mail-Followup-To")
0269         << friend1Address << only(defaultAddress) << only(friend2Address) << only(replyAddress) << only(followupAddress) << listAddress
0270         << only(mailReplyAddress) << (int)ReplyAll << defaultAddress << only(followupAddress) << nobody;
0271 
0272     // Reply to Author
0273     // ---------------
0274     // ReplyAuthor ignores Cc, and replies to the Mail-Reply-To, Reply-To, or
0275     // From addresses, in that order of preference, if List-Post is absent.
0276     QTest::newRow("ReplyAuthor, no special headers") << friend1Address << only(defaultAddress) << only(friend2Address) << nobody << nobody << QString()
0277                                                      << nobody << (int)ReplyAuthor << defaultAddress << only(friend1Address) << nobody;
0278     QTest::newRow("ReplyAuthor, from someone with Reply-To") << friend1Address << only(defaultAddress) << only(friend2Address) << only(replyAddress) << nobody
0279                                                              << QString() << nobody << (int)ReplyAuthor << defaultAddress << only(replyAddress) << nobody;
0280     QTest::newRow("ReplyAuthor, from someone with Mail-Reply-To")
0281         << friend1Address << only(defaultAddress) << only(friend2Address) << only(replyAddress) << nobody << QString() << only(mailReplyAddress)
0282         << (int)ReplyAuthor << defaultAddress << only(mailReplyAddress) << nobody;
0283 
0284     // If Reply-To is the same as List-Post, ReplyAuthor ignores it and uses
0285     // From, because the mailing list munged Reply-To.
0286     QTest::newRow("ReplyAuthor, from list that munges Reply-To")
0287         << friend1Address << only(defaultAddress) << only(friend2Address) << only(listAddress) << nobody << listAddress << nobody << (int)ReplyAuthor
0288         << defaultAddress << only(friend1Address) << nobody;
0289 
0290     // If Reply-To contains List-Post, ReplyAuthor uses the other reply
0291     // addresses, because the mailing list didn't completely munge Reply-To.
0292     QTest::newRow("ReplyAuthor, from list that lightly munges Reply-To")
0293         << friend1Address << only(defaultAddress) << only(friend2Address) << both(listAddress, replyAddress) << nobody << listAddress << nobody
0294         << (int)ReplyAuthor << defaultAddress << only(replyAddress) << nobody;
0295 
0296     // Reply to None
0297     // -------------
0298     // ReplyNone ignores all possible headers and does not choose a To address.
0299     QTest::newRow("ReplyNone") << friend1Address << only(defaultAddress) << only(friend2Address) << only(replyAddress) << only(followupAddress) << listAddress
0300                                << only(mailReplyAddress) << (int)ReplyNone << defaultAddress << nobody << nobody;
0301 }
0302 
0303 void ReplyStrategyTest::testReply()
0304 {
0305     QFETCH(const QString, oFrom);
0306     QFETCH(const QStringList, oTo);
0307     QFETCH(const QStringList, oCc);
0308     QFETCH(const QStringList, oRT);
0309     QFETCH(const QStringList, oMFT);
0310     QFETCH(const QString, oLP);
0311     QFETCH(const QStringList, oMRT);
0312     QFETCH(const int, strategy);
0313     QFETCH(const QString, rFrom);
0314     QFETCH(const QStringList, rTo);
0315     QFETCH(const QStringList, rCc);
0316 
0317     auto original{basicMessage(oFrom, oTo)};
0318     if (!oCc.isEmpty()) {
0319         auto cc{new KMime::Headers::Cc};
0320         for (const auto &a : oCc) {
0321             cc->addAddress(a.toLatin1());
0322         }
0323         original->setHeader(cc);
0324     }
0325     if (!oRT.isEmpty()) {
0326         auto replyTo{new KMime::Headers::ReplyTo};
0327         for (const auto &a : oRT) {
0328             replyTo->addAddress(a.toLatin1());
0329         }
0330         original->setHeader(replyTo);
0331     }
0332     if (!oMFT.isEmpty()) {
0333         auto mailFollowupTo = new KMime::Headers::Generic("Mail-Followup-To");
0334         mailFollowupTo->from7BitString(oMFT.join(QLatin1Char(',')).toLatin1());
0335         original->setHeader(mailFollowupTo);
0336     }
0337     if (!oLP.isEmpty()) {
0338         auto listPost = new KMime::Headers::Generic("List-Post");
0339         listPost->from7BitString("<mailto:" + oLP.toLatin1() + ">");
0340         original->setHeader(listPost);
0341     }
0342     if (!oMRT.isEmpty()) {
0343         auto mailReplyTo = new KMime::Headers::Generic("Mail-Reply-To");
0344         mailReplyTo->from7BitString(oMRT.join(QLatin1Char(',')).toLatin1());
0345         original->setHeader(mailReplyTo);
0346     }
0347 
0348     if (auto reply = makeReply(original, (ReplyStrategy)strategy)) {
0349         COMPARE_ADDRESSES(reply->from(), only(rFrom));
0350         COMPARE_ADDRESSES(reply->to(), rTo);
0351         COMPARE_ADDRESSES(reply->cc(), rCc);
0352     }
0353     original.clear();
0354 }
0355 
0356 QTEST_MAIN(ReplyStrategyTest)
0357 
0358 #include "moc_replystrategytest.cpp"