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"